You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by st...@apache.org on 2013/04/30 23:35:35 UTC
[19/53] Merge branch 'master' of
https://git-wip-us.apache.org/repos/asf/cordova-android into gutting
http://git-wip-us.apache.org/repos/asf/cordova-android/blob/7ab30348/framework/src/org/apache/cordova/core/FileUtils.java
----------------------------------------------------------------------
diff --cc framework/src/org/apache/cordova/core/FileUtils.java
index 88b667b,0000000..8078b57
mode 100755,000000..100755
--- a/framework/src/org/apache/cordova/core/FileUtils.java
+++ b/framework/src/org/apache/cordova/core/FileUtils.java
@@@ -1,1061 -1,0 +1,1059 @@@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+package org.apache.cordova.core;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.cordova.api.CallbackContext;
+import org.apache.cordova.api.CordovaPlugin;
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.file.EncodingException;
+import org.apache.cordova.file.FileExistsException;
+import org.apache.cordova.file.InvalidModificationException;
+import org.apache.cordova.file.NoModificationAllowedException;
+import org.apache.cordova.file.TypeMismatchException;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
- import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
- import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
++import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.channels.FileChannel;
+
+/**
+ * This class provides SD card file and directory services to JavaScript.
+ * Only files on the SD card can be accessed.
+ */
+public class FileUtils extends CordovaPlugin {
- @SuppressWarnings("unused")
- private static final String LOG_TAG = "FilePlugin";
++ private static final String LOG_TAG = "FileUtils";
+
+ public static int NOT_FOUND_ERR = 1;
+ public static int SECURITY_ERR = 2;
+ public static int ABORT_ERR = 3;
+
+ public static int NOT_READABLE_ERR = 4;
+ public static int ENCODING_ERR = 5;
+ public static int NO_MODIFICATION_ALLOWED_ERR = 6;
+ public static int INVALID_STATE_ERR = 7;
+ public static int SYNTAX_ERR = 8;
+ public static int INVALID_MODIFICATION_ERR = 9;
+ public static int QUOTA_EXCEEDED_ERR = 10;
+ public static int TYPE_MISMATCH_ERR = 11;
+ public static int PATH_EXISTS_ERR = 12;
+
+ public static int TEMPORARY = 0;
+ public static int PERSISTENT = 1;
+ public static int RESOURCE = 2;
+ public static int APPLICATION = 3;
+
+ /**
+ * Constructor.
+ */
+ public FileUtils() {
+ }
+
+ /**
+ * Executes the request and returns whether the action was valid.
+ *
+ * @param action The action to execute.
+ * @param args JSONArray of arguments for the plugin.
+ * @param callbackContext The callback context used when calling back into JavaScript.
+ * @return True if the action was valid, false otherwise.
+ */
+ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+ try {
+ if (action.equals("testSaveLocationExists")) {
+ boolean b = DirectoryManager.testSaveLocationExists();
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+ }
+ else if (action.equals("getFreeDiskSpace")) {
+ long l = DirectoryManager.getFreeDiskSpace(false);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, l));
+ }
+ else if (action.equals("testFileExists")) {
+ boolean b = DirectoryManager.testFileExists(args.getString(0));
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+ }
+ else if (action.equals("testDirectoryExists")) {
+ boolean b = DirectoryManager.testFileExists(args.getString(0));
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+ }
+ else if (action.equals("readAsText")) {
+ String encoding = args.getString(1);
+ int start = args.getInt(2);
+ int end = args.getInt(3);
+
+ this.readFileAs(args.getString(0), start, end, callbackContext, encoding, PluginResult.MESSAGE_TYPE_STRING);
+ }
+ else if (action.equals("readAsDataURL")) {
+ int start = args.getInt(1);
+ int end = args.getInt(2);
+
+ this.readFileAs(args.getString(0), start, end, callbackContext, null, -1);
+ }
+ else if (action.equals("readAsArrayBuffer")) {
+ int start = args.getInt(1);
+ int end = args.getInt(2);
+
+ this.readFileAs(args.getString(0), start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_ARRAYBUFFER);
+ }
+ else if (action.equals("readAsBinaryString")) {
+ int start = args.getInt(1);
+ int end = args.getInt(2);
+
+ this.readFileAs(args.getString(0), start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_BINARYSTRING);
+ }
+ else if (action.equals("write")) {
+ long fileSize = this.write(args.getString(0), args.getString(1), args.getInt(2));
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+ }
+ else if (action.equals("truncate")) {
+ long fileSize = this.truncateFile(args.getString(0), args.getLong(1));
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+ }
+ else if (action.equals("requestFileSystem")) {
+ long size = args.optLong(1);
+ if (size != 0 && size > (DirectoryManager.getFreeDiskSpace(true) * 1024)) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.QUOTA_EXCEEDED_ERR));
+ } else {
+ JSONObject obj = requestFileSystem(args.getInt(0));
+ callbackContext.success(obj);
+ }
+ }
+ else if (action.equals("resolveLocalFileSystemURI")) {
+ JSONObject obj = resolveLocalFileSystemURI(args.getString(0));
+ callbackContext.success(obj);
+ }
+ else if (action.equals("getMetadata")) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, getMetadata(args.getString(0))));
+ }
+ else if (action.equals("getFileMetadata")) {
+ JSONObject obj = getFileMetadata(args.getString(0));
+ callbackContext.success(obj);
+ }
+ else if (action.equals("getParent")) {
+ JSONObject obj = getParent(args.getString(0));
+ callbackContext.success(obj);
+ }
+ else if (action.equals("getDirectory")) {
+ JSONObject obj = getFile(args.getString(0), args.getString(1), args.optJSONObject(2), true);
+ callbackContext.success(obj);
+ }
+ else if (action.equals("getFile")) {
+ JSONObject obj = getFile(args.getString(0), args.getString(1), args.optJSONObject(2), false);
+ callbackContext.success(obj);
+ }
+ else if (action.equals("remove")) {
+ boolean success;
+
+ success = remove(args.getString(0));
+
+ if (success) {
+ notifyDelete(args.getString(0));
+ callbackContext.success();
+ } else {
+ callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ }
+ }
+ else if (action.equals("removeRecursively")) {
+ boolean success = removeRecursively(args.getString(0));
+ if (success) {
+ callbackContext.success();
+ } else {
+ callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ }
+ }
+ else if (action.equals("moveTo")) {
+ JSONObject entry = transferTo(args.getString(0), args.getString(1), args.getString(2), true);
+ callbackContext.success(entry);
+ }
+ else if (action.equals("copyTo")) {
+ JSONObject entry = transferTo(args.getString(0), args.getString(1), args.getString(2), false);
+ callbackContext.success(entry);
+ }
+ else if (action.equals("readEntries")) {
+ JSONArray entries = readEntries(args.getString(0));
+ callbackContext.success(entries);
+ }
+ else {
+ return false;
+ }
+ } catch (FileNotFoundException e) {
+ callbackContext.error(FileUtils.NOT_FOUND_ERR);
+ } catch (FileExistsException e) {
+ callbackContext.error(FileUtils.PATH_EXISTS_ERR);
+ } catch (NoModificationAllowedException e) {
+ callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ } catch (InvalidModificationException e) {
+ callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
+ } catch (MalformedURLException e) {
+ callbackContext.error(FileUtils.ENCODING_ERR);
+ } catch (IOException e) {
+ callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
+ } catch (EncodingException e) {
+ callbackContext.error(FileUtils.ENCODING_ERR);
+ } catch (TypeMismatchException e) {
+ callbackContext.error(FileUtils.TYPE_MISMATCH_ERR);
+ }
+ return true;
+ }
+
+ /**
+ * Need to check to see if we need to clean up the content store
+ *
+ * @param filePath the path to check
+ */
+ private void notifyDelete(String filePath) {
+ String newFilePath = FileHelper.getRealPath(filePath, cordova);
+ try {
+ this.cordova.getActivity().getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Images.Media.DATA + " = ?",
+ new String[] { newFilePath });
+ } catch (UnsupportedOperationException t) {
+ // Was seeing this on the File mobile-spec tests on 4.0.3 x86 emulator.
+ // The ContentResolver applies only when the file was registered in the
+ // first case, which is generally only the case with images.
+ }
+ }
+
+ /**
+ * Allows the user to look up the Entry for a file or directory referred to by a local URI.
+ *
+ * @param url of the file/directory to look up
+ * @return a JSONObject representing a Entry from the filesystem
+ * @throws MalformedURLException if the url is not valid
+ * @throws FileNotFoundException if the file does not exist
+ * @throws IOException if the user can't read the file
+ * @throws JSONException
+ */
+ @SuppressWarnings("deprecation")
+ private JSONObject resolveLocalFileSystemURI(String url) throws IOException, JSONException {
+ String decoded = URLDecoder.decode(url, "UTF-8");
+
+ File fp = null;
+
+ // Handle the special case where you get an Android content:// uri.
+ if (decoded.startsWith("content:")) {
+ Cursor cursor = this.cordova.getActivity().managedQuery(Uri.parse(decoded), new String[] { MediaStore.Images.Media.DATA }, null, null, null);
+ // Note: MediaStore.Images/Audio/Video.Media.DATA is always "_data"
+ int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
+ cursor.moveToFirst();
+ fp = new File(cursor.getString(column_index));
+ } else {
+ // Test to see if this is a valid URL first
+ @SuppressWarnings("unused")
+ URL testUrl = new URL(decoded);
+
+ if (decoded.startsWith("file://")) {
+ int questionMark = decoded.indexOf("?");
+ if (questionMark < 0) {
+ fp = new File(decoded.substring(7, decoded.length()));
+ } else {
+ fp = new File(decoded.substring(7, questionMark));
+ }
+ } else {
+ fp = new File(decoded);
+ }
+ }
+
+ if (!fp.exists()) {
+ throw new FileNotFoundException();
+ }
+ if (!fp.canRead()) {
+ throw new IOException();
+ }
+ return getEntry(fp);
+ }
+
+ /**
+ * Read the list of files from this directory.
+ *
+ * @param fileName the directory to read from
+ * @return a JSONArray containing JSONObjects that represent Entry objects.
+ * @throws FileNotFoundException if the directory is not found.
+ * @throws JSONException
+ */
+ private JSONArray readEntries(String fileName) throws FileNotFoundException, JSONException {
+ File fp = createFileObject(fileName);
+
+ if (!fp.exists()) {
+ // The directory we are listing doesn't exist so we should fail.
+ throw new FileNotFoundException();
+ }
+
+ JSONArray entries = new JSONArray();
+
+ if (fp.isDirectory()) {
+ File[] files = fp.listFiles();
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].canRead()) {
+ entries.put(getEntry(files[i]));
+ }
+ }
+ }
+
+ return entries;
+ }
+
+ /**
+ * A setup method that handles the move/copy of files/directories
+ *
+ * @param fileName to be copied/moved
+ * @param newParent is the location where the file will be copied/moved to
+ * @param newName for the file directory to be called, if null use existing file name
+ * @param move if false do a copy, if true do a move
+ * @return a Entry object
+ * @throws NoModificationAllowedException
+ * @throws IOException
+ * @throws InvalidModificationException
+ * @throws EncodingException
+ * @throws JSONException
+ * @throws FileExistsException
+ */
+ private JSONObject transferTo(String fileName, String newParent, String newName, boolean move) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
+ String newFileName = FileHelper.getRealPath(fileName, cordova);
+ newParent = FileHelper.getRealPath(newParent, cordova);
+
+ // Check for invalid file name
+ if (newName != null && newName.contains(":")) {
+ throw new EncodingException("Bad file name");
+ }
+
+ File source = new File(newFileName);
+
+ if (!source.exists()) {
+ // The file/directory we are copying doesn't exist so we should fail.
+ throw new FileNotFoundException("The source does not exist");
+ }
+
+ File destinationDir = new File(newParent);
+ if (!destinationDir.exists()) {
+ // The destination does not exist so we should fail.
+ throw new FileNotFoundException("The source does not exist");
+ }
+
+ // Figure out where we should be copying to
+ File destination = createDestination(newName, source, destinationDir);
+
+ //Log.d(LOG_TAG, "Source: " + source.getAbsolutePath());
+ //Log.d(LOG_TAG, "Destin: " + destination.getAbsolutePath());
+
+ // Check to see if source and destination are the same file
+ if (source.getAbsolutePath().equals(destination.getAbsolutePath())) {
+ throw new InvalidModificationException("Can't copy a file onto itself");
+ }
+
+ if (source.isDirectory()) {
+ if (move) {
+ return moveDirectory(source, destination);
+ } else {
+ return copyDirectory(source, destination);
+ }
+ } else {
+ if (move) {
+ JSONObject newFileEntry = moveFile(source, destination);
+
+ // If we've moved a file given its content URI, we need to clean up.
+ if (fileName.startsWith("content://")) {
+ notifyDelete(fileName);
+ }
+
+ return newFileEntry;
+ } else {
+ return copyFile(source, destination);
+ }
+ }
+ }
+
+ /**
+ * Creates the destination File object based on name passed in
+ *
+ * @param newName for the file directory to be called, if null use existing file name
+ * @param fp represents the source file
+ * @param destination represents the destination file
+ * @return a File object that represents the destination
+ */
+ private File createDestination(String newName, File fp, File destination) {
+ File destFile = null;
+
+ // I know this looks weird but it is to work around a JSON bug.
+ if ("null".equals(newName) || "".equals(newName)) {
+ newName = null;
+ }
+
+ if (newName != null) {
+ destFile = new File(destination.getAbsolutePath() + File.separator + newName);
+ } else {
+ destFile = new File(destination.getAbsolutePath() + File.separator + fp.getName());
+ }
+ return destFile;
+ }
+
+ /**
+ * Copy a file
+ *
+ * @param srcFile file to be copied
+ * @param destFile destination to be copied to
+ * @return a FileEntry object
+ * @throws IOException
+ * @throws InvalidModificationException
+ * @throws JSONException
+ */
+ private JSONObject copyFile(File srcFile, File destFile) throws IOException, InvalidModificationException, JSONException {
+ // Renaming a file to an existing directory should fail
+ if (destFile.exists() && destFile.isDirectory()) {
+ throw new InvalidModificationException("Can't rename a file to a directory");
+ }
+
+ copyAction(srcFile, destFile);
+
+ return getEntry(destFile);
+ }
+
+ /**
+ * Moved this code into it's own method so moveTo could use it when the move is across file systems
+ */
+ private void copyAction(File srcFile, File destFile)
+ throws FileNotFoundException, IOException {
+ FileInputStream istream = new FileInputStream(srcFile);
+ FileOutputStream ostream = new FileOutputStream(destFile);
+ FileChannel input = istream.getChannel();
+ FileChannel output = ostream.getChannel();
+
+ try {
+ input.transferTo(0, input.size(), output);
+ } finally {
+ istream.close();
+ ostream.close();
+ input.close();
+ output.close();
+ }
+ }
+
+ /**
+ * Copy a directory
+ *
+ * @param srcDir directory to be copied
+ * @param destinationDir destination to be copied to
+ * @return a DirectoryEntry object
+ * @throws JSONException
+ * @throws IOException
+ * @throws NoModificationAllowedException
+ * @throws InvalidModificationException
+ */
+ private JSONObject copyDirectory(File srcDir, File destinationDir) throws JSONException, IOException, NoModificationAllowedException, InvalidModificationException {
+ // Renaming a file to an existing directory should fail
+ if (destinationDir.exists() && destinationDir.isFile()) {
+ throw new InvalidModificationException("Can't rename a file to a directory");
+ }
+
+ // Check to make sure we are not copying the directory into itself
+ if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) {
+ throw new InvalidModificationException("Can't copy itself into itself");
+ }
+
+ // See if the destination directory exists. If not create it.
+ if (!destinationDir.exists()) {
+ if (!destinationDir.mkdir()) {
+ // If we can't create the directory then fail
+ throw new NoModificationAllowedException("Couldn't create the destination directory");
+ }
+ }
+
+ for (File file : srcDir.listFiles()) {
+ if (file.isDirectory()) {
+ copyDirectory(file, destinationDir);
+ } else {
+ File destination = new File(destinationDir.getAbsoluteFile() + File.separator + file.getName());
+ copyFile(file, destination);
+ }
+ }
+
+ return getEntry(destinationDir);
+ }
+
+ /**
+ * Check to see if the user attempted to copy an entry into its parent without changing its name,
+ * or attempted to copy a directory into a directory that it contains directly or indirectly.
+ *
+ * @param srcDir
+ * @param destinationDir
+ * @return
+ */
+ private boolean isCopyOnItself(String src, String dest) {
+
+ // This weird test is to determine if we are copying or moving a directory into itself.
+ // Copy /sdcard/myDir to /sdcard/myDir-backup is okay but
+ // Copy /sdcard/myDir to /sdcard/myDir/backup should throw an INVALID_MODIFICATION_ERR
+ if (dest.startsWith(src) && dest.indexOf(File.separator, src.length() - 1) != -1) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Move a file
+ *
+ * @param srcFile file to be copied
+ * @param destFile destination to be copied to
+ * @return a FileEntry object
+ * @throws IOException
+ * @throws InvalidModificationException
+ * @throws JSONException
+ */
+ private JSONObject moveFile(File srcFile, File destFile) throws IOException, JSONException, InvalidModificationException {
+ // Renaming a file to an existing directory should fail
+ if (destFile.exists() && destFile.isDirectory()) {
+ throw new InvalidModificationException("Can't rename a file to a directory");
+ }
+
+ // Try to rename the file
+ if (!srcFile.renameTo(destFile)) {
+ // Trying to rename the file failed. Possibly because we moved across file system on the device.
+ // Now we have to do things the hard way
+ // 1) Copy all the old file
+ // 2) delete the src file
+ copyAction(srcFile, destFile);
+ if (destFile.exists()) {
+ srcFile.delete();
+ } else {
+ throw new IOException("moved failed");
+ }
+ }
+
+ return getEntry(destFile);
+ }
+
+ /**
+ * Move a directory
+ *
+ * @param srcDir directory to be copied
+ * @param destinationDir destination to be copied to
+ * @return a DirectoryEntry object
+ * @throws JSONException
+ * @throws IOException
+ * @throws InvalidModificationException
+ * @throws NoModificationAllowedException
+ * @throws FileExistsException
+ */
+ private JSONObject moveDirectory(File srcDir, File destinationDir) throws IOException, JSONException, InvalidModificationException, NoModificationAllowedException, FileExistsException {
+ // Renaming a file to an existing directory should fail
+ if (destinationDir.exists() && destinationDir.isFile()) {
+ throw new InvalidModificationException("Can't rename a file to a directory");
+ }
+
+ // Check to make sure we are not copying the directory into itself
+ if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) {
+ throw new InvalidModificationException("Can't move itself into itself");
+ }
+
+ // If the destination directory already exists and is empty then delete it. This is according to spec.
+ if (destinationDir.exists()) {
+ if (destinationDir.list().length > 0) {
+ throw new InvalidModificationException("directory is not empty");
+ }
+ }
+
+ // Try to rename the directory
+ if (!srcDir.renameTo(destinationDir)) {
+ // Trying to rename the directory failed. Possibly because we moved across file system on the device.
+ // Now we have to do things the hard way
+ // 1) Copy all the old files
+ // 2) delete the src directory
+ copyDirectory(srcDir, destinationDir);
+ if (destinationDir.exists()) {
+ removeDirRecursively(srcDir);
+ } else {
+ throw new IOException("moved failed");
+ }
+ }
+
+ return getEntry(destinationDir);
+ }
+
+ /**
+ * Deletes a directory and all of its contents, if any. In the event of an error
+ * [e.g. trying to delete a directory that contains a file that cannot be removed],
+ * some of the contents of the directory may be deleted.
+ * It is an error to attempt to delete the root directory of a filesystem.
+ *
+ * @param filePath the directory to be removed
+ * @return a boolean representing success of failure
+ * @throws FileExistsException
+ */
+ private boolean removeRecursively(String filePath) throws FileExistsException {
+ File fp = createFileObject(filePath);
+
+ // You can't delete the root directory.
+ if (atRootDirectory(filePath)) {
+ return false;
+ }
+
+ return removeDirRecursively(fp);
+ }
+
+ /**
+ * Loops through a directory deleting all the files.
+ *
+ * @param directory to be removed
+ * @return a boolean representing success of failure
+ * @throws FileExistsException
+ */
+ private boolean removeDirRecursively(File directory) throws FileExistsException {
+ if (directory.isDirectory()) {
+ for (File file : directory.listFiles()) {
+ removeDirRecursively(file);
+ }
+ }
+
+ if (!directory.delete()) {
+ throw new FileExistsException("could not delete: " + directory.getName());
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Deletes a file or directory. It is an error to attempt to delete a directory that is not empty.
+ * It is an error to attempt to delete the root directory of a filesystem.
+ *
+ * @param filePath file or directory to be removed
+ * @return a boolean representing success of failure
+ * @throws NoModificationAllowedException
+ * @throws InvalidModificationException
+ */
+ private boolean remove(String filePath) throws NoModificationAllowedException, InvalidModificationException {
+ File fp = createFileObject(filePath);
+
+ // You can't delete the root directory.
+ if (atRootDirectory(filePath)) {
+ throw new NoModificationAllowedException("You can't delete the root directory");
+ }
+
+ // You can't delete a directory that is not empty
+ if (fp.isDirectory() && fp.list().length > 0) {
+ throw new InvalidModificationException("You can't delete a directory that is not empty.");
+ }
+
+ return fp.delete();
+ }
+
+ /**
+ * Creates or looks up a file.
+ *
+ * @param dirPath base directory
+ * @param fileName file/directory to lookup or create
+ * @param options specify whether to create or not
+ * @param directory if true look up directory, if false look up file
+ * @return a Entry object
+ * @throws FileExistsException
+ * @throws IOException
+ * @throws TypeMismatchException
+ * @throws EncodingException
+ * @throws JSONException
+ */
+ private JSONObject getFile(String dirPath, String fileName, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+ boolean create = false;
+ boolean exclusive = false;
+ if (options != null) {
+ create = options.optBoolean("create");
+ if (create) {
+ exclusive = options.optBoolean("exclusive");
+ }
+ }
+
+ // Check for a ":" character in the file to line up with BB and iOS
+ if (fileName.contains(":")) {
+ throw new EncodingException("This file has a : in it's name");
+ }
+
+ File fp = createFileObject(dirPath, fileName);
+
+ if (create) {
+ if (exclusive && fp.exists()) {
+ throw new FileExistsException("create/exclusive fails");
+ }
+ if (directory) {
+ fp.mkdir();
+ } else {
+ fp.createNewFile();
+ }
+ if (!fp.exists()) {
+ throw new FileExistsException("create fails");
+ }
+ }
+ else {
+ if (!fp.exists()) {
+ throw new FileNotFoundException("path does not exist");
+ }
+ if (directory) {
+ if (fp.isFile()) {
+ throw new TypeMismatchException("path doesn't exist or is file");
+ }
+ } else {
+ if (fp.isDirectory()) {
+ throw new TypeMismatchException("path doesn't exist or is directory");
+ }
+ }
+ }
+
+ // Return the directory
+ return getEntry(fp);
+ }
+
+ /**
+ * If the path starts with a '/' just return that file object. If not construct the file
+ * object from the path passed in and the file name.
+ *
+ * @param dirPath root directory
+ * @param fileName new file name
+ * @return
+ */
+ private File createFileObject(String dirPath, String fileName) {
+ File fp = null;
+ if (fileName.startsWith("/")) {
+ fp = new File(fileName);
+ } else {
+ dirPath = FileHelper.getRealPath(dirPath, cordova);
+ fp = new File(dirPath + File.separator + fileName);
+ }
+ return fp;
+ }
+
+ /**
+ * Look up the parent DirectoryEntry containing this Entry.
+ * If this Entry is the root of its filesystem, its parent is itself.
+ *
+ * @param filePath
+ * @return
+ * @throws JSONException
+ */
+ private JSONObject getParent(String filePath) throws JSONException {
+ filePath = FileHelper.getRealPath(filePath, cordova);
+
+ if (atRootDirectory(filePath)) {
+ return getEntry(filePath);
+ }
+ return getEntry(new File(filePath).getParent());
+ }
+
+ /**
+ * Checks to see if we are at the root directory. Useful since we are
+ * not allow to delete this directory.
+ *
+ * @param filePath to directory
+ * @return true if we are at the root, false otherwise.
+ */
+ private boolean atRootDirectory(String filePath) {
+ filePath = FileHelper.getRealPath(filePath, cordova);
+
+ if (filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + cordova.getActivity().getPackageName() + "/cache") ||
+ filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath()) ||
+ filePath.equals("/data/data/" + cordova.getActivity().getPackageName())) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Create a File object from the passed in path
+ *
+ * @param filePath
+ * @return
+ */
+ private File createFileObject(String filePath) {
+ filePath = FileHelper.getRealPath(filePath, cordova);
+
+ File file = new File(filePath);
+ return file;
+ }
+
+ /**
+ * Look up metadata about this entry.
+ *
+ * @param filePath to entry
+ * @return a long
+ * @throws FileNotFoundException
+ */
+ private long getMetadata(String filePath) throws FileNotFoundException {
+ File file = createFileObject(filePath);
+
+ if (!file.exists()) {
+ throw new FileNotFoundException("Failed to find file in getMetadata");
+ }
+
+ return file.lastModified();
+ }
+
+ /**
+ * Returns a File that represents the current state of the file that this FileEntry represents.
+ *
+ * @param filePath to entry
+ * @return returns a JSONObject represent a W3C File object
+ * @throws FileNotFoundException
+ * @throws JSONException
+ */
+ private JSONObject getFileMetadata(String filePath) throws FileNotFoundException, JSONException {
+ File file = createFileObject(filePath);
+
+ if (!file.exists()) {
+ throw new FileNotFoundException("File: " + filePath + " does not exist.");
+ }
+
+ JSONObject metadata = new JSONObject();
+ metadata.put("size", file.length());
+ metadata.put("type", FileHelper.getMimeType(filePath, cordova));
+ metadata.put("name", file.getName());
+ metadata.put("fullPath", filePath);
+ metadata.put("lastModifiedDate", file.lastModified());
+
+ return metadata;
+ }
+
+ /**
+ * Requests a filesystem in which to store application data.
+ *
+ * @param type of file system requested
+ * @return a JSONObject representing the file system
+ * @throws IOException
+ * @throws JSONException
+ */
+ private JSONObject requestFileSystem(int type) throws IOException, JSONException {
+ JSONObject fs = new JSONObject();
+ if (type == TEMPORARY) {
+ File fp;
+ fs.put("name", "temporary");
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ fp = new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
+ "/Android/data/" + cordova.getActivity().getPackageName() + "/cache/");
+ // Create the cache dir if it doesn't exist.
+ fp.mkdirs();
+ fs.put("root", getEntry(Environment.getExternalStorageDirectory().getAbsolutePath() +
+ "/Android/data/" + cordova.getActivity().getPackageName() + "/cache/"));
+ } else {
+ fp = new File("/data/data/" + cordova.getActivity().getPackageName() + "/cache/");
+ // Create the cache dir if it doesn't exist.
+ fp.mkdirs();
+ fs.put("root", getEntry("/data/data/" + cordova.getActivity().getPackageName() + "/cache/"));
+ }
+ }
+ else if (type == PERSISTENT) {
+ fs.put("name", "persistent");
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ fs.put("root", getEntry(Environment.getExternalStorageDirectory()));
+ } else {
+ fs.put("root", getEntry("/data/data/" + cordova.getActivity().getPackageName()));
+ }
+ }
+ else {
+ throw new IOException("No filesystem of type requested");
+ }
+
+ return fs;
+ }
+
+ /**
+ * Returns a JSON object representing the given File.
+ *
+ * @param file the File to convert
+ * @return a JSON representation of the given File
+ * @throws JSONException
+ */
+ public static JSONObject getEntry(File file) throws JSONException {
+ JSONObject entry = new JSONObject();
+
+ entry.put("isFile", file.isFile());
+ entry.put("isDirectory", file.isDirectory());
+ entry.put("name", file.getName());
+ entry.put("fullPath", "file://" + file.getAbsolutePath());
+ // The file system can't be specified, as it would lead to an infinite loop.
+ // entry.put("filesystem", null);
+
+ return entry;
+ }
+
+ /**
+ * Returns a JSON Object representing a directory on the device's file system
+ *
+ * @param path to the directory
+ * @return
+ * @throws JSONException
+ */
+ private JSONObject getEntry(String path) throws JSONException {
+ return getEntry(new File(path));
+ }
+
+
+ //--------------------------------------------------------------------------
+ // LOCAL METHODS
+ //--------------------------------------------------------------------------
+
+ /**
- * Read the contents of a file as text.
++ * Read the contents of a file.
+ * This is done in a background thread; the result is sent to the callback.
+ *
+ * @param filename The name of the file.
- * @param encoding The encoding to return contents as. Typical value is UTF-8. (see http://www.iana.org/assignments/character-sets)
+ * @param start Start position in the file.
+ * @param end End position to stop at (exclusive).
++ * @param callbackContext The context through which to send the result.
++ * @param encoding The encoding to return contents as. Typical value is UTF-8. (see http://www.iana.org/assignments/character-sets)
++ * @param resultType The desired type of data to send to the callback.
+ * @return Contents of file.
+ */
+ public void readFileAs(final String filename, final int start, final int end, final CallbackContext callbackContext, final String encoding, final int resultType) {
+ this.cordova.getThreadPool().execute(new Runnable() {
+ public void run() {
+ try {
+ byte[] bytes = readAsBinaryHelper(filename, start, end);
+
+ PluginResult result;
+ switch (resultType) {
+ case PluginResult.MESSAGE_TYPE_STRING:
+ result = new PluginResult(PluginResult.Status.OK, new String(bytes, encoding));
+ break;
+ case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
+ result = new PluginResult(PluginResult.Status.OK, bytes);
+ break;
+ case PluginResult.MESSAGE_TYPE_BINARYSTRING:
+ result = new PluginResult(PluginResult.Status.OK, bytes, true);
+ break;
+ default: // Base64.
+ String contentType = FileHelper.getMimeType(filename, cordova);
+ byte[] base64 = Base64.encodeBase64(bytes);
+ String s = "data:" + contentType + ";base64," + new String(base64, "US-ASCII");
+ result = new PluginResult(PluginResult.Status.OK, s);
+ }
+
+ callbackContext.sendPluginResult(result);
+ } catch (FileNotFoundException e) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR));
+ } catch (IOException e) {
+ Log.d(LOG_TAG, e.getLocalizedMessage());
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR));
+ }
+ }
+ });
+ }
+
+ /**
+ * Read the contents of a file as binary.
+ * This is done synchronously; the result is returned.
+ *
+ * @param filename The name of the file.
+ * @param start Start position in the file.
+ * @param end End position to stop at (exclusive).
+ * @return Contents of the file as a byte[].
+ * @throws IOException
+ */
+ private byte[] readAsBinaryHelper(String filename, int start, int end) throws IOException {
- int diff = end - start;
- byte[] bytes = new byte[1000];
- BufferedInputStream bis = new BufferedInputStream(FileHelper.getInputStreamFromUriString(filename, cordova), 1024);
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- int numRead = 0;
++ int numBytesToRead = end - start;
++ byte[] bytes = new byte[numBytesToRead];
++ InputStream inputStream = FileHelper.getInputStreamFromUriString(filename, cordova);
++ int numBytesRead = 0;
+
+ if (start > 0) {
- bis.skip(start);
++ inputStream.skip(start);
+ }
+
- while (diff > 0 && (numRead = bis.read(bytes, 0, Math.min(1000, diff))) >= 0) {
- diff -= numRead;
- bos.write(bytes, 0, numRead);
++ while (numBytesToRead > 0 && (numBytesRead = inputStream.read(bytes, numBytesRead, numBytesToRead)) >= 0) {
++ numBytesToRead -= numBytesRead;
+ }
+
- return bos.toByteArray();
++ return bytes;
+ }
+
+ /**
+ * Write contents of file.
+ *
+ * @param filename The name of the file.
+ * @param data The contents of the file.
+ * @param offset The position to begin writing the file.
+ * @throws FileNotFoundException, IOException
+ * @throws NoModificationAllowedException
+ */
+ /**/
+ public long write(String filename, String data, int offset) throws FileNotFoundException, IOException, NoModificationAllowedException {
+ if (filename.startsWith("content://")) {
+ throw new NoModificationAllowedException("Couldn't write to file given its content URI");
+ }
+
+ filename = FileHelper.getRealPath(filename, cordova);
+
+ boolean append = false;
+ if (offset > 0) {
+ this.truncateFile(filename, offset);
+ append = true;
+ }
+
+ byte[] rawData = data.getBytes();
+ ByteArrayInputStream in = new ByteArrayInputStream(rawData);
+ FileOutputStream out = new FileOutputStream(filename, append);
+ byte buff[] = new byte[rawData.length];
+ in.read(buff, 0, buff.length);
+ out.write(buff, 0, rawData.length);
+ out.flush();
+ out.close();
+
+ return rawData.length;
+ }
+
+ /**
+ * Truncate the file to size
+ *
+ * @param filename
+ * @param size
+ * @throws FileNotFoundException, IOException
+ * @throws NoModificationAllowedException
+ */
+ private long truncateFile(String filename, long size) throws FileNotFoundException, IOException, NoModificationAllowedException {
+ if (filename.startsWith("content://")) {
+ throw new NoModificationAllowedException("Couldn't truncate file given its content URI");
+ }
+
+ filename = FileHelper.getRealPath(filename, cordova);
+
+ RandomAccessFile raf = new RandomAccessFile(filename, "rw");
+ try {
+ if (raf.length() >= size) {
+ FileChannel channel = raf.getChannel();
+ channel.truncate(size);
+ return size;
+ }
+
+ return raf.length();
+ } finally {
+ raf.close();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-android/blob/7ab30348/framework/src/org/apache/cordova/core/InAppBrowser.java
----------------------------------------------------------------------
diff --cc framework/src/org/apache/cordova/core/InAppBrowser.java
index 6b7193f,0000000..a90abd5
mode 100644,000000..100644
--- a/framework/src/org/apache/cordova/core/InAppBrowser.java
+++ b/framework/src/org/apache/cordova/core/InAppBrowser.java
@@@ -1,668 -1,0 +1,710 @@@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+package org.apache.cordova.core;
+
+import java.util.HashMap;
+import java.util.StringTokenizer;
+
+import org.apache.cordova.Config;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.api.CallbackContext;
+import org.apache.cordova.api.CordovaPlugin;
+import org.apache.cordova.api.LOG;
+import org.apache.cordova.api.PluginResult;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.InputType;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.WebChromeClient;
+import android.webkit.GeolocationPermissions.Callback;
+import android.webkit.WebSettings;
+import android.webkit.WebStorage;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+
+@SuppressLint("SetJavaScriptEnabled")
+public class InAppBrowser extends CordovaPlugin {
+
+ private static final String NULL = "null";
+ protected static final String LOG_TAG = "InAppBrowser";
+ private static final String SELF = "_self";
+ private static final String SYSTEM = "_system";
+ // private static final String BLANK = "_blank";
+ private static final String LOCATION = "location";
+ private static final String EXIT_EVENT = "exit";
+ private static final String LOAD_START_EVENT = "loadstart";
+ private static final String LOAD_STOP_EVENT = "loadstop";
++ private static final String LOAD_ERROR_EVENT = "loaderror";
+ private static final String CLOSE_BUTTON_CAPTION = "closebuttoncaption";
+ private long MAX_QUOTA = 100 * 1024 * 1024;
+
+ private Dialog dialog;
+ private WebView inAppWebView;
+ private EditText edittext;
+ private boolean showLocationBar = true;
+ private CallbackContext callbackContext;
+ private String buttonLabel = "Done";
+
+ /**
+ * Executes the request and returns PluginResult.
+ *
+ * @param action The action to execute.
+ * @param args JSONArry of arguments for the plugin.
+ * @param callbackId The callback id used when calling back into JavaScript.
+ * @return A PluginResult object with a status and message.
+ */
+ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+ PluginResult.Status status = PluginResult.Status.OK;
+ String result = "";
+ this.callbackContext = callbackContext;
+
+ try {
+ if (action.equals("open")) {
+ String url = args.getString(0);
+ String target = args.optString(1);
+ if (target == null || target.equals("") || target.equals(NULL)) {
+ target = SELF;
+ }
+ HashMap<String, Boolean> features = parseFeature(args.optString(2));
+
+ Log.d(LOG_TAG, "target = " + target);
+
+ url = updateUrl(url);
+
+ // SELF
+ if (SELF.equals(target)) {
+ Log.d(LOG_TAG, "in self");
+ // load in webview
+ if (url.startsWith("file://") || url.startsWith("javascript:")
+ || Config.isUrlWhiteListed(url)) {
+ this.webView.loadUrl(url);
+ }
+ //Load the dialer
+ else if (url.startsWith(WebView.SCHEME_TEL))
+ {
+ try {
+ Intent intent = new Intent(Intent.ACTION_DIAL);
+ intent.setData(Uri.parse(url));
+ this.cordova.getActivity().startActivity(intent);
+ } catch (android.content.ActivityNotFoundException e) {
+ LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString());
+ }
+ }
+ // load in InAppBrowser
+ else {
+ result = this.showWebPage(url, features);
+ }
+ }
+ // SYSTEM
+ else if (SYSTEM.equals(target)) {
+ Log.d(LOG_TAG, "in system");
+ result = this.openExternal(url);
+ }
+ // BLANK - or anything else
+ else {
+ Log.d(LOG_TAG, "in blank");
+ result = this.showWebPage(url, features);
+ }
+ }
+ else if (action.equals("close")) {
+ closeDialog();
+
+ PluginResult pluginResult = new PluginResult(PluginResult.Status.OK);
+ pluginResult.setKeepCallback(false);
+ this.callbackContext.sendPluginResult(pluginResult);
+ }
++ else if (action.equals("injectScriptCode")) {
++ String source = args.getString(0);
++
++ org.json.JSONArray jsonEsc = new org.json.JSONArray();
++ jsonEsc.put(source);
++ String jsonRepr = jsonEsc.toString();
++ String jsonSourceString = jsonRepr.substring(1, jsonRepr.length()-1);
++ String scriptEnclosure = "(function(d){var c=d.createElement('script');c.type='text/javascript';c.innerText="
++ + jsonSourceString
++ + ";d.getElementsByTagName('head')[0].appendChild(c);})(document)";
++ this.inAppWebView.loadUrl("javascript:" + scriptEnclosure);
++
++ PluginResult pluginResult = new PluginResult(PluginResult.Status.OK);
++ this.callbackContext.sendPluginResult(pluginResult);
++ }
+ else {
+ status = PluginResult.Status.INVALID_ACTION;
+ }
+ PluginResult pluginResult = new PluginResult(status, result);
+ pluginResult.setKeepCallback(true);
+ this.callbackContext.sendPluginResult(pluginResult);
+ } catch (JSONException e) {
+ this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
+ }
+ return true;
+ }
+
+ /**
+ * Put the list of features into a hash map
+ *
+ * @param optString
+ * @return
+ */
+ private HashMap<String, Boolean> parseFeature(String optString) {
+ if (optString.equals(NULL)) {
+ return null;
+ } else {
+ HashMap<String, Boolean> map = new HashMap<String, Boolean>();
+ StringTokenizer features = new StringTokenizer(optString, ",");
+ StringTokenizer option;
+ while(features.hasMoreElements()) {
+ option = new StringTokenizer(features.nextToken(), "=");
+ if (option.hasMoreElements()) {
+ String key = option.nextToken();
+ if (key.equalsIgnoreCase(CLOSE_BUTTON_CAPTION)) {
+ this.buttonLabel = option.nextToken();
+ } else {
+ Boolean value = option.nextToken().equals("no") ? Boolean.FALSE : Boolean.TRUE;
+ map.put(key, value);
+ }
+ }
+ }
+ return map;
+ }
+ }
+
+ /**
+ * Convert relative URL to full path
+ *
+ * @param url
+ * @return
+ */
+ private String updateUrl(String url) {
+ Uri newUrl = Uri.parse(url);
+ if (newUrl.isRelative()) {
+ url = this.webView.getUrl().substring(0, this.webView.getUrl().lastIndexOf("/")+1) + url;
+ }
+ return url;
+ }
+
+ /**
+ * Display a new browser with the specified URL.
+ *
+ * @param url The url to load.
+ * @param usePhoneGap Load url in PhoneGap webview
+ * @return "" if ok, or error message.
+ */
+ public String openExternal(String url) {
+ try {
+ Intent intent = null;
+ intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(url));
+ this.cordova.getActivity().startActivity(intent);
+ return "";
+ } catch (android.content.ActivityNotFoundException e) {
+ Log.d(LOG_TAG, "InAppBrowser: Error loading url "+url+":"+ e.toString());
+ return e.toString();
+ }
+ }
+
+ /**
+ * Closes the dialog
+ */
+ private void closeDialog() {
+ try {
++ this.inAppWebView.loadUrl("about:blank");
+ JSONObject obj = new JSONObject();
+ obj.put("type", EXIT_EVENT);
+
+ sendUpdate(obj, false);
+ } catch (JSONException ex) {
+ Log.d(LOG_TAG, "Should never happen");
+ }
+
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+
+ /**
+ * Checks to see if it is possible to go back one page in history, then does so.
+ */
+ private void goBack() {
+ if (this.inAppWebView.canGoBack()) {
+ this.inAppWebView.goBack();
+ }
+ }
+
+ /**
+ * Checks to see if it is possible to go forward one page in history, then does so.
+ */
+ private void goForward() {
+ if (this.inAppWebView.canGoForward()) {
+ this.inAppWebView.goForward();
+ }
+ }
+
+ /**
+ * Navigate to the new page
+ *
+ * @param url to load
+ */
+ private void navigate(String url) {
+ InputMethodManager imm = (InputMethodManager)this.cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(edittext.getWindowToken(), 0);
+
+ if (!url.startsWith("http") && !url.startsWith("file:")) {
+ this.inAppWebView.loadUrl("http://" + url);
+ } else {
+ this.inAppWebView.loadUrl(url);
+ }
+ this.inAppWebView.requestFocus();
+ }
+
+
+ /**
+ * Should we show the location bar?
+ *
+ * @return boolean
+ */
+ private boolean getShowLocationBar() {
+ return this.showLocationBar;
+ }
+
+ /**
+ * Display a new browser with the specified URL.
+ *
+ * @param url The url to load.
+ * @param jsonObject
+ */
+ public String showWebPage(final String url, HashMap<String, Boolean> features) {
+ // Determine if we should hide the location bar.
+ showLocationBar = true;
+ if (features != null) {
+ Boolean show = features.get(LOCATION);
+ if (show != null) {
+ showLocationBar = show.booleanValue();
+ }
+ }
+
+ final CordovaWebView thatWebView = this.webView;
+
+ // Create dialog in new thread
+ Runnable runnable = new Runnable() {
+ /**
+ * Convert our DIP units to Pixels
+ *
+ * @return int
+ */
+ private int dpToPixels(int dipValue) {
+ int value = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP,
+ (float) dipValue,
+ cordova.getActivity().getResources().getDisplayMetrics()
+ );
+
+ return value;
+ }
+
+ public void run() {
+ // Let's create the main dialog
+ dialog = new Dialog(cordova.getActivity(), android.R.style.Theme_NoTitleBar);
+ dialog.getWindow().getAttributes().windowAnimations = android.R.style.Animation_Dialog;
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ dialog.setCancelable(true);
+ dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ public void onDismiss(DialogInterface dialog) {
+ try {
+ JSONObject obj = new JSONObject();
+ obj.put("type", EXIT_EVENT);
+
+ sendUpdate(obj, false);
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Should never happen");
+ }
+ }
+ });
+
+ // Main container layout
+ LinearLayout main = new LinearLayout(cordova.getActivity());
+ main.setOrientation(LinearLayout.VERTICAL);
+
+ // Toolbar layout
+ RelativeLayout toolbar = new RelativeLayout(cordova.getActivity());
+ toolbar.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, this.dpToPixels(44)));
+ toolbar.setPadding(this.dpToPixels(2), this.dpToPixels(2), this.dpToPixels(2), this.dpToPixels(2));
+ toolbar.setHorizontalGravity(Gravity.LEFT);
+ toolbar.setVerticalGravity(Gravity.TOP);
+
+ // Action Button Container layout
+ RelativeLayout actionButtonContainer = new RelativeLayout(cordova.getActivity());
+ actionButtonContainer.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ actionButtonContainer.setHorizontalGravity(Gravity.LEFT);
+ actionButtonContainer.setVerticalGravity(Gravity.CENTER_VERTICAL);
+ actionButtonContainer.setId(1);
+
+ // Back button
+ Button back = new Button(cordova.getActivity());
+ RelativeLayout.LayoutParams backLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ backLayoutParams.addRule(RelativeLayout.ALIGN_LEFT);
+ back.setLayoutParams(backLayoutParams);
+ back.setContentDescription("Back Button");
+ back.setId(2);
+ back.setText("<");
+ back.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ goBack();
+ }
+ });
+
+ // Forward button
+ Button forward = new Button(cordova.getActivity());
+ RelativeLayout.LayoutParams forwardLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ forwardLayoutParams.addRule(RelativeLayout.RIGHT_OF, 2);
+ forward.setLayoutParams(forwardLayoutParams);
+ forward.setContentDescription("Forward Button");
+ forward.setId(3);
+ forward.setText(">");
+ forward.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ goForward();
+ }
+ });
+
+ // Edit Text Box
+ edittext = new EditText(cordova.getActivity());
+ RelativeLayout.LayoutParams textLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ textLayoutParams.addRule(RelativeLayout.RIGHT_OF, 1);
+ textLayoutParams.addRule(RelativeLayout.LEFT_OF, 5);
+ edittext.setLayoutParams(textLayoutParams);
+ edittext.setId(4);
+ edittext.setSingleLine(true);
+ edittext.setText(url);
+ edittext.setInputType(InputType.TYPE_TEXT_VARIATION_URI);
+ edittext.setImeOptions(EditorInfo.IME_ACTION_GO);
+ edittext.setInputType(InputType.TYPE_NULL); // Will not except input... Makes the text NON-EDITABLE
+ edittext.setOnKeyListener(new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // If the event is a key-down event on the "enter" button
+ if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
+ navigate(edittext.getText().toString());
+ return true;
+ }
+ return false;
+ }
+ });
+
+ // Close button
+ Button close = new Button(cordova.getActivity());
+ RelativeLayout.LayoutParams closeLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ closeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+ close.setLayoutParams(closeLayoutParams);
+ forward.setContentDescription("Close Button");
+ close.setId(5);
+ close.setText(buttonLabel);
+ close.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ closeDialog();
+ }
+ });
+
+ // WebView
+ inAppWebView = new WebView(cordova.getActivity());
+ inAppWebView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ inAppWebView.setWebChromeClient(new InAppChromeClient());
+ WebViewClient client = new InAppBrowserClient(thatWebView, edittext);
+ inAppWebView.setWebViewClient(client);
+ WebSettings settings = inAppWebView.getSettings();
+ settings.setJavaScriptEnabled(true);
+ settings.setJavaScriptCanOpenWindowsAutomatically(true);
+ settings.setBuiltInZoomControls(true);
+ /**
+ * We need to be careful of this line as a future Android release may deprecate it out of existence.
+ * Can't replace it with the API 8 level call right now as our minimum SDK is 7 until May 2013
+ */
+ // @TODO: replace with settings.setPluginState(android.webkit.WebSettings.PluginState.ON)
+ settings.setPluginsEnabled(true);
+
+ //Toggle whether this is enabled or not!
+ Bundle appSettings = cordova.getActivity().getIntent().getExtras();
- boolean enableDatabase = appSettings.getBoolean("InAppBrowserStorageEnabled", true);
++ boolean enableDatabase = appSettings == null ? true : appSettings.getBoolean("InAppBrowserStorageEnabled", true);
+ if(enableDatabase)
+ {
+ String databasePath = cordova.getActivity().getApplicationContext().getDir("inAppBrowserDB", Context.MODE_PRIVATE).getPath();
+ settings.setDatabasePath(databasePath);
+ settings.setDatabaseEnabled(true);
+ }
+ settings.setDomStorageEnabled(true);
+
+ inAppWebView.loadUrl(url);
+ inAppWebView.setId(6);
+ inAppWebView.getSettings().setLoadWithOverviewMode(true);
+ inAppWebView.getSettings().setUseWideViewPort(true);
+ inAppWebView.requestFocus();
+ inAppWebView.requestFocusFromTouch();
+
+ // Add the back and forward buttons to our action button container layout
+ actionButtonContainer.addView(back);
+ actionButtonContainer.addView(forward);
+
+ // Add the views to our toolbar
+ toolbar.addView(actionButtonContainer);
+ toolbar.addView(edittext);
+ toolbar.addView(close);
+
+ // Don't add the toolbar if its been disabled
+ if (getShowLocationBar()) {
+ // Add our toolbar to our main view/layout
+ main.addView(toolbar);
+ }
+
+ // Add our webview to our main view/layout
+ main.addView(inAppWebView);
+
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
+ lp.copyFrom(dialog.getWindow().getAttributes());
+ lp.width = WindowManager.LayoutParams.MATCH_PARENT;
+ lp.height = WindowManager.LayoutParams.MATCH_PARENT;
+
+ dialog.setContentView(main);
+ dialog.show();
+ dialog.getWindow().setAttributes(lp);
+ }
+ };
+ this.cordova.getActivity().runOnUiThread(runnable);
+ return "";
+ }
+
+ /**
- * Create a new plugin result and send it back to JavaScript
++ * Create a new plugin success result and send it back to JavaScript
+ *
+ * @param obj a JSONObject contain event payload information
+ */
+ private void sendUpdate(JSONObject obj, boolean keepCallback) {
- PluginResult result = new PluginResult(PluginResult.Status.OK, obj);
++ sendUpdate(obj, keepCallback, PluginResult.Status.OK);
++ }
++
++ /**
++ * Create a new plugin result and send it back to JavaScript
++ *
++ * @param obj a JSONObject contain event payload information
++ * @param status the status code to return to the JavaScript environment
++ */ private void sendUpdate(JSONObject obj, boolean keepCallback, PluginResult.Status status) {
++ PluginResult result = new PluginResult(status, obj);
+ result.setKeepCallback(keepCallback);
+ this.callbackContext.sendPluginResult(result);
+ }
-
+ public class InAppChromeClient extends WebChromeClient {
+
+ /**
+ * Handle database quota exceeded notification.
+ *
+ * @param url
+ * @param databaseIdentifier
+ * @param currentQuota
+ * @param estimatedSize
+ * @param totalUsedQuota
+ * @param quotaUpdater
+ */
+ @Override
+ public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize,
+ long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
+ {
+ LOG.d(LOG_TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota);
+
+ if (estimatedSize < MAX_QUOTA)
+ {
+ //increase for 1Mb
+ long newQuota = estimatedSize;
+ LOG.d(LOG_TAG, "calling quotaUpdater.updateQuota newQuota: %d", newQuota);
+ quotaUpdater.updateQuota(newQuota);
+ }
+ else
+ {
+ // Set the quota to whatever it is and force an error
+ // TODO: get docs on how to handle this properly
+ quotaUpdater.updateQuota(currentQuota);
+ }
+ }
+
+ /**
+ * Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin.
+ *
+ * @param origin
+ * @param callback
+ */
+ @Override
+ public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
+ super.onGeolocationPermissionsShowPrompt(origin, callback);
+ callback.invoke(origin, true, false);
+ }
+ }
+
+ /**
+ * The webview client receives notifications about appView
+ */
+ public class InAppBrowserClient extends WebViewClient {
+ EditText edittext;
+ CordovaWebView webView;
+
+ /**
+ * Constructor.
+ *
+ * @param mContext
+ * @param edittext
+ */
+ public InAppBrowserClient(CordovaWebView webView, EditText mEditText) {
+ this.webView = webView;
+ this.edittext = mEditText;
+ }
+
+ /**
+ * Notify the host application that a page has started loading.
+ *
+ * @param view The webview initiating the callback.
+ * @param url The url of the page.
+ */
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ super.onPageStarted(view, url, favicon);
+ String newloc = "";
+ if (url.startsWith("http:") || url.startsWith("https:") || url.startsWith("file:")) {
+ newloc = url;
+ }
+ // If dialing phone (tel:5551212)
+ else if (url.startsWith(WebView.SCHEME_TEL)) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_DIAL);
+ intent.setData(Uri.parse(url));
+ cordova.getActivity().startActivity(intent);
+ } catch (android.content.ActivityNotFoundException e) {
+ LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString());
+ }
+ }
+
+ else if (url.startsWith("geo:") || url.startsWith(WebView.SCHEME_MAILTO) || url.startsWith("market:")) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(url));
+ cordova.getActivity().startActivity(intent);
+ } catch (android.content.ActivityNotFoundException e) {
+ LOG.e(LOG_TAG, "Error with " + url + ": " + e.toString());
+ }
+ }
+ // If sms:5551212?body=This is the message
+ else if (url.startsWith("sms:")) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+
+ // Get address
+ String address = null;
+ int parmIndex = url.indexOf('?');
+ if (parmIndex == -1) {
+ address = url.substring(4);
+ }
+ else {
+ address = url.substring(4, parmIndex);
+
+ // If body, then set sms body
+ Uri uri = Uri.parse(url);
+ String query = uri.getQuery();
+ if (query != null) {
+ if (query.startsWith("body=")) {
+ intent.putExtra("sms_body", query.substring(5));
+ }
+ }
+ }
+ intent.setData(Uri.parse("sms:" + address));
+ intent.putExtra("address", address);
+ intent.setType("vnd.android-dir/mms-sms");
+ cordova.getActivity().startActivity(intent);
+ } catch (android.content.ActivityNotFoundException e) {
+ LOG.e(LOG_TAG, "Error sending sms " + url + ":" + e.toString());
+ }
+ }
+ else {
+ newloc = "http://" + url;
+ }
+
+ if (!newloc.equals(edittext.getText().toString())) {
+ edittext.setText(newloc);
+ }
+
+ try {
+ JSONObject obj = new JSONObject();
+ obj.put("type", LOAD_START_EVENT);
+ obj.put("url", newloc);
+
+ sendUpdate(obj, true);
+ } catch (JSONException ex) {
+ Log.d(LOG_TAG, "Should never happen");
+ }
+ }
+
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+
+ try {
+ JSONObject obj = new JSONObject();
+ obj.put("type", LOAD_STOP_EVENT);
+ obj.put("url", url);
+
+ sendUpdate(obj, true);
+ } catch (JSONException ex) {
+ Log.d(LOG_TAG, "Should never happen");
+ }
+ }
++
++ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
++ super.onReceivedError(view, errorCode, description, failingUrl);
++
++ try {
++ JSONObject obj = new JSONObject();
++ obj.put("type", LOAD_ERROR_EVENT);
++ obj.put("url", failingUrl);
++ obj.put("code", errorCode);
++ obj.put("message", description);
++
++ sendUpdate(obj, true, PluginResult.Status.ERROR);
++ } catch (JSONException ex) {
++ Log.d(LOG_TAG, "Should never happen");
++ }
++
++ }
+ }
+}