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");
++            }
++        	
++        }
 +    }
 +}