You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by fi...@apache.org on 2013/01/22 02:57:58 UTC

[41/52] [partial] support for 2.4.0rc1. "vendored" the platform libs in. added Gord and Braden as contributors. removed dependency on unzip and axed the old download-cordova code.

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/ContactManager.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/ContactManager.java b/lib/cordova-android/framework/src/org/apache/cordova/ContactManager.java
new file mode 100755
index 0000000..8ea64a6
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/ContactManager.java
@@ -0,0 +1,122 @@
+/*
+       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;
+
+import org.apache.cordova.api.CallbackContext;
+import org.apache.cordova.api.CordovaPlugin;
+import org.apache.cordova.api.PluginResult;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import android.util.Log;
+
+public class ContactManager extends CordovaPlugin {
+
+    private ContactAccessor contactAccessor;
+    private static final String LOG_TAG = "Contact Query";
+
+    public static final int UNKNOWN_ERROR = 0;
+    public static final int INVALID_ARGUMENT_ERROR = 1;
+    public static final int TIMEOUT_ERROR = 2;
+    public static final int PENDING_OPERATION_ERROR = 3;
+    public static final int IO_ERROR = 4;
+    public static final int NOT_SUPPORTED_ERROR = 5;
+    public static final int PERMISSION_DENIED_ERROR = 20;
+
+    /**
+     * Constructor.
+     */
+    public ContactManager() {
+    }
+
+    /**
+     * Executes the request and returns PluginResult.
+     *
+     * @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, final CallbackContext callbackContext) throws JSONException {
+        /**
+         * Check to see if we are on an Android 1.X device.  If we are return an error as we
+         * do not support this as of Cordova 1.0.
+         */
+        if (android.os.Build.VERSION.RELEASE.startsWith("1.")) {
+            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, ContactManager.NOT_SUPPORTED_ERROR));
+            return true;
+        }
+
+        /**
+         * Only create the contactAccessor after we check the Android version or the program will crash
+         * older phones.
+         */
+        if (this.contactAccessor == null) {
+            this.contactAccessor = new ContactAccessorSdk5(this.webView, this.cordova);
+        }
+
+        if (action.equals("search")) {
+            final JSONArray filter = args.getJSONArray(0);
+            final JSONObject options = args.getJSONObject(1);
+            this.cordova.getThreadPool().execute(new Runnable() {
+                public void run() {
+                    JSONArray res = contactAccessor.search(filter, options);
+                    callbackContext.success(res);
+                }
+            });
+        }
+        else if (action.equals("save")) {
+            final JSONObject contact = args.getJSONObject(0);
+            this.cordova.getThreadPool().execute(new Runnable() {
+                public void run() {
+                    JSONObject res = null;
+                    String id = contactAccessor.save(contact);
+                    if (id != null) {
+                        try {
+                            res = contactAccessor.getContactById(id);
+                        } catch (JSONException e) {
+                            Log.e(LOG_TAG, "JSON fail.", e);
+                        }
+                    }
+                    if (res != null) {
+                        callbackContext.success(res);
+                    } else {
+                        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, UNKNOWN_ERROR));
+                    }
+                }
+            });
+        }
+        else if (action.equals("remove")) {
+            final String contactId = args.getString(0);
+            this.cordova.getThreadPool().execute(new Runnable() {
+                public void run() {
+                    if (contactAccessor.remove(contactId)) {
+                        callbackContext.success();
+                    } else {
+                        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, UNKNOWN_ERROR));
+                    }
+                }
+            });
+        }
+        else {
+            return false;
+        }
+        return true;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/CordovaArgs.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/CordovaArgs.java b/lib/cordova-android/framework/src/org/apache/cordova/CordovaArgs.java
new file mode 100644
index 0000000..3a8b7c0
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/CordovaArgs.java
@@ -0,0 +1,112 @@
+/*
+       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;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.util.Base64;
+
+public class CordovaArgs {
+    private JSONArray baseArgs;
+
+    public CordovaArgs(JSONArray args) {
+        this.baseArgs = args;
+    }
+
+
+    // Pass through the basics to the base args.
+    public Object get(int index) throws JSONException {
+        return baseArgs.get(index);
+    }
+
+    public boolean getBoolean(int index) throws JSONException {
+        return baseArgs.getBoolean(index);
+    }
+
+    public double getDouble(int index) throws JSONException {
+        return baseArgs.getDouble(index);
+    }
+
+    public int getInt(int index) throws JSONException {
+        return baseArgs.getInt(index);
+    }
+
+    public JSONArray getJSONArray(int index) throws JSONException {
+        return baseArgs.getJSONArray(index);
+    }
+
+    public Object getJSONObject(int index) throws JSONException {
+        return baseArgs.getJSONObject(index);
+    }
+
+    public long getLong(int index) throws JSONException {
+        return baseArgs.getLong(index);
+    }
+
+    public String getString(int index) throws JSONException {
+        return baseArgs.getString(index);
+    }
+
+
+    public Object opt(int index) {
+        return baseArgs.opt(index);
+    }
+
+    public boolean optBoolean(int index) {
+        return baseArgs.optBoolean(index);
+    }
+
+    public double optDouble(int index) {
+        return baseArgs.optDouble(index);
+    }
+
+    public int optInt(int index) {
+        return baseArgs.optInt(index);
+    }
+
+    public JSONArray optJSONArray(int index) {
+        return baseArgs.optJSONArray(index);
+    }
+
+    public Object optJSONObject(int index) {
+        return baseArgs.optJSONObject(index);
+    }
+
+    public long optLong(int index) {
+        return baseArgs.optLong(index);
+    }
+
+    public String optString(int index) {
+        return baseArgs.optString(index);
+    }
+
+    public boolean isNull(int index) {
+        return baseArgs.isNull(index);
+    }
+
+
+    // The interesting custom helpers.
+    public byte[] getArrayBuffer(int index) throws JSONException {
+        String encoded = baseArgs.getString(index);
+        return Base64.decode(encoded, Base64.DEFAULT);
+    }
+}
+
+

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/CordovaChromeClient.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/CordovaChromeClient.java b/lib/cordova-android/framework/src/org/apache/cordova/CordovaChromeClient.java
new file mode 100755
index 0000000..4b30894
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/CordovaChromeClient.java
@@ -0,0 +1,404 @@
+/*
+       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;
+
+import org.apache.cordova.api.CordovaInterface;
+import org.apache.cordova.api.LOG;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.webkit.ConsoleMessage;
+import android.webkit.JsPromptResult;
+import android.webkit.JsResult;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
+import android.webkit.WebStorage;
+import android.webkit.WebView;
+import android.webkit.GeolocationPermissions.Callback;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+
+/**
+ * This class is the WebChromeClient that implements callbacks for our web view.
+ */
+public class CordovaChromeClient extends WebChromeClient {
+
+    public static final int FILECHOOSER_RESULTCODE = 5173;
+    private static final String LOG_TAG = "CordovaChromeClient";
+    private String TAG = "CordovaLog";
+    private long MAX_QUOTA = 100 * 1024 * 1024;
+    private CordovaInterface cordova;
+    private CordovaWebView appView;
+
+    // the video progress view
+    private View mVideoProgressView;
+    
+    // File Chooser
+    public ValueCallback<Uri> mUploadMessage;
+    
+    /**
+     * Constructor.
+     *
+     * @param cordova
+     */
+    public CordovaChromeClient(CordovaInterface cordova) {
+        this.cordova = cordova;
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param ctx
+     * @param app
+     */
+    public CordovaChromeClient(CordovaInterface ctx, CordovaWebView app) {
+        this.cordova = ctx;
+        this.appView = app;
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param view
+     */
+    public void setWebView(CordovaWebView view) {
+        this.appView = view;
+    }
+
+    /**
+     * Tell the client to display a javascript alert dialog.
+     *
+     * @param view
+     * @param url
+     * @param message
+     * @param result
+     */
+    @Override
+    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
+        AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity());
+        dlg.setMessage(message);
+        dlg.setTitle("Alert");
+        //Don't let alerts break the back button
+        dlg.setCancelable(true);
+        dlg.setPositiveButton(android.R.string.ok,
+                new AlertDialog.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        result.confirm();
+                    }
+                });
+        dlg.setOnCancelListener(
+                new DialogInterface.OnCancelListener() {
+                    public void onCancel(DialogInterface dialog) {
+                        result.cancel();
+                    }
+                });
+        dlg.setOnKeyListener(new DialogInterface.OnKeyListener() {
+            //DO NOTHING
+            public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+                if (keyCode == KeyEvent.KEYCODE_BACK)
+                {
+                    result.confirm();
+                    return false;
+                }
+                else
+                    return true;
+            }
+        });
+        dlg.create();
+        dlg.show();
+        return true;
+    }
+
+    /**
+     * Tell the client to display a confirm dialog to the user.
+     *
+     * @param view
+     * @param url
+     * @param message
+     * @param result
+     */
+    @Override
+    public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
+        AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity());
+        dlg.setMessage(message);
+        dlg.setTitle("Confirm");
+        dlg.setCancelable(true);
+        dlg.setPositiveButton(android.R.string.ok,
+                new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        result.confirm();
+                    }
+                });
+        dlg.setNegativeButton(android.R.string.cancel,
+                new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        result.cancel();
+                    }
+                });
+        dlg.setOnCancelListener(
+                new DialogInterface.OnCancelListener() {
+                    public void onCancel(DialogInterface dialog) {
+                        result.cancel();
+                    }
+                });
+        dlg.setOnKeyListener(new DialogInterface.OnKeyListener() {
+            //DO NOTHING
+            public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+                if (keyCode == KeyEvent.KEYCODE_BACK)
+                {
+                    result.cancel();
+                    return false;
+                }
+                else
+                    return true;
+            }
+        });
+        dlg.create();
+        dlg.show();
+        return true;
+    }
+
+    /**
+     * Tell the client to display a prompt dialog to the user.
+     * If the client returns true, WebView will assume that the client will
+     * handle the prompt dialog and call the appropriate JsPromptResult method.
+     *
+     * Since we are hacking prompts for our own purposes, we should not be using them for
+     * this purpose, perhaps we should hack console.log to do this instead!
+     *
+     * @param view
+     * @param url
+     * @param message
+     * @param defaultValue
+     * @param result
+     */
+    @Override
+    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
+
+        // Security check to make sure any requests are coming from the page initially
+        // loaded in webview and not another loaded in an iframe.
+        boolean reqOk = false;
+        if (url.startsWith("file://") || url.indexOf(this.appView.baseUrl) == 0 || Config.isUrlWhiteListed(url)) {
+            reqOk = true;
+        }
+
+        // Calling PluginManager.exec() to call a native service using 
+        // prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true]));
+        if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) {
+            JSONArray array;
+            try {
+                array = new JSONArray(defaultValue.substring(4));
+                String service = array.getString(0);
+                String action = array.getString(1);
+                String callbackId = array.getString(2);
+                String r = this.appView.exposedJsApi.exec(service, action, callbackId, message);
+                result.confirm(r == null ? "" : r);
+            } catch (JSONException e) {
+                e.printStackTrace();
+            }
+        }
+
+        // Sets the native->JS bridge mode. 
+        else if (reqOk && defaultValue != null && defaultValue.equals("gap_bridge_mode:")) {
+            this.appView.exposedJsApi.setNativeToJsBridgeMode(Integer.parseInt(message));
+            result.confirm("");
+        }
+
+        // Polling for JavaScript messages 
+        else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) {
+            String r = this.appView.exposedJsApi.retrieveJsMessages();
+            result.confirm(r == null ? "" : r);
+        }
+
+        // Do NO-OP so older code doesn't display dialog
+        else if (defaultValue != null && defaultValue.equals("gap_init:")) {
+            result.confirm("OK");
+        }
+
+        // Show dialog
+        else {
+            final JsPromptResult res = result;
+            AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity());
+            dlg.setMessage(message);
+            final EditText input = new EditText(this.cordova.getActivity());
+            if (defaultValue != null) {
+                input.setText(defaultValue);
+            }
+            dlg.setView(input);
+            dlg.setCancelable(false);
+            dlg.setPositiveButton(android.R.string.ok,
+                    new DialogInterface.OnClickListener() {
+                        public void onClick(DialogInterface dialog, int which) {
+                            String usertext = input.getText().toString();
+                            res.confirm(usertext);
+                        }
+                    });
+            dlg.setNegativeButton(android.R.string.cancel,
+                    new DialogInterface.OnClickListener() {
+                        public void onClick(DialogInterface dialog, int which) {
+                            res.cancel();
+                        }
+                    });
+            dlg.create();
+            dlg.show();
+        }
+        return true;
+    }
+
+    /**
+     * 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(TAG, "DroidGap:  onExceededDatabaseQuota estimatedSize: %d  currentQuota: %d  totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota);
+
+        if (estimatedSize < MAX_QUOTA)
+        {
+            //increase for 1Mb
+            long newQuota = estimatedSize;
+            LOG.d(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);
+        }
+    }
+
+    // console.log in api level 7: http://developer.android.com/guide/developing/debug-tasks.html
+    // Expect this to not compile in a future Android release!
+    @SuppressWarnings("deprecation")
+    @Override
+    public void onConsoleMessage(String message, int lineNumber, String sourceID)
+    {
+        //This is only for Android 2.1
+        if(android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.ECLAIR_MR1)
+        {
+            LOG.d(TAG, "%s: Line %d : %s", sourceID, lineNumber, message);
+            super.onConsoleMessage(message, lineNumber, sourceID);
+        }
+    }
+
+    @TargetApi(8)
+    @Override
+    public boolean onConsoleMessage(ConsoleMessage consoleMessage)
+    {
+        if (consoleMessage.message() != null)
+            LOG.d(TAG, consoleMessage.message());
+        return super.onConsoleMessage(consoleMessage);
+    }
+
+    @Override
+    /**
+     * 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
+     */
+    public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
+        super.onGeolocationPermissionsShowPrompt(origin, callback);
+        callback.invoke(origin, true, false);
+    }
+    
+    // API level 7 is required for this, see if we could lower this using something else
+    @Override
+    public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) {
+        this.appView.showCustomView(view, callback);
+    }
+
+	@Override
+	public void onHideCustomView() {
+    	this.appView.hideCustomView();
+	}
+    
+    @Override
+    /**
+     * Ask the host application for a custom progress view to show while
+     * a <video> is loading.
+     * @return View The progress view.
+     */
+    public View getVideoLoadingProgressView() {
+
+	    if (mVideoProgressView == null) {	        
+	    	// Create a new Loading view programmatically.
+	    	
+	    	// create the linear layout
+	    	LinearLayout layout = new LinearLayout(this.appView.getContext());
+	        layout.setOrientation(LinearLayout.VERTICAL);
+	        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+	        layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
+	        layout.setLayoutParams(layoutParams);
+	        // the proress bar
+	        ProgressBar bar = new ProgressBar(this.appView.getContext());
+	        LinearLayout.LayoutParams barLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+	        barLayoutParams.gravity = Gravity.CENTER;
+	        bar.setLayoutParams(barLayoutParams);   
+	        layout.addView(bar);
+	        
+	        mVideoProgressView = layout;
+	    }
+    return mVideoProgressView; 
+    }
+    
+    public void openFileChooser(ValueCallback<Uri> uploadMsg) {
+        this.openFileChooser(uploadMsg, "*/*");
+    }
+
+    public void openFileChooser( ValueCallback<Uri> uploadMsg, String acceptType ) {
+        this.openFileChooser(uploadMsg, acceptType, null);
+    }
+    
+    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture)
+    {
+        mUploadMessage = uploadMsg;
+        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+        i.addCategory(Intent.CATEGORY_OPENABLE);
+        i.setType("*/*");
+        this.cordova.getActivity().startActivityForResult(Intent.createChooser(i, "File Browser"),
+                FILECHOOSER_RESULTCODE);
+    }
+    
+    public ValueCallback<Uri> getValueCallback() {
+        return this.mUploadMessage;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/CordovaLocationListener.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/CordovaLocationListener.java b/lib/cordova-android/framework/src/org/apache/cordova/CordovaLocationListener.java
new file mode 100755
index 0000000..7b7a9f7
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/CordovaLocationListener.java
@@ -0,0 +1,207 @@
+/*
+       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;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.cordova.api.CallbackContext;
+
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.util.Log;
+
+public class CordovaLocationListener implements LocationListener {
+    public static int PERMISSION_DENIED = 1;
+    public static int POSITION_UNAVAILABLE = 2;
+    public static int TIMEOUT = 3;
+
+    protected LocationManager locationManager;
+    private GeoBroker owner;
+    protected boolean running = false;
+
+    public HashMap<String, CallbackContext> watches = new HashMap<String, CallbackContext>();
+    private List<CallbackContext> callbacks = new ArrayList<CallbackContext>();
+
+    private String TAG = "[Cordova Location Listener]";
+
+    public CordovaLocationListener(LocationManager manager, GeoBroker broker, String tag) {
+        this.locationManager = manager;
+        this.owner = broker;
+        this.TAG = tag;
+    }
+
+    protected void fail(int code, String message) {
+        for (CallbackContext callbackContext: this.callbacks)
+        {
+            this.owner.fail(code, message, callbackContext);
+        }
+        if(this.owner.isGlobalListener(this))
+        {
+        	Log.d(TAG, "Stopping global listener");
+        	this.stop();
+        }
+        this.callbacks.clear();
+
+        Iterator<CallbackContext> it = this.watches.values().iterator();
+        while (it.hasNext()) {
+            this.owner.fail(code, message, it.next());
+        }
+    }
+
+    private void win(Location loc) {
+        for (CallbackContext callbackContext: this.callbacks)
+        {
+            this.owner.win(loc, callbackContext);
+        }
+        if(this.owner.isGlobalListener(this))
+        {
+        	Log.d(TAG, "Stopping global listener");
+        	this.stop();
+        }
+        this.callbacks.clear();
+
+        Iterator<CallbackContext> it = this.watches.values().iterator();
+        while (it.hasNext()) {
+            this.owner.win(loc, it.next());
+        }
+    }
+
+    /**
+     * Location Listener Methods
+     */
+
+    /**
+     * Called when the provider is disabled by the user.
+     *
+     * @param provider
+     */
+    public void onProviderDisabled(String provider) {
+        Log.d(TAG, "Location provider '" + provider + "' disabled.");
+        this.fail(POSITION_UNAVAILABLE, "GPS provider disabled.");
+    }
+
+    /**
+     * Called when the provider is enabled by the user.
+     *
+     * @param provider
+     */
+    public void onProviderEnabled(String provider) {
+        Log.d(TAG, "Location provider "+ provider + " has been enabled");
+    }
+
+    /**
+     * Called when the provider status changes. This method is called when a
+     * provider is unable to fetch a location or if the provider has recently
+     * become available after a period of unavailability.
+     *
+     * @param provider
+     * @param status
+     * @param extras
+     */
+    public void onStatusChanged(String provider, int status, Bundle extras) {
+        Log.d(TAG, "The status of the provider " + provider + " has changed");
+        if (status == 0) {
+            Log.d(TAG, provider + " is OUT OF SERVICE");
+            this.fail(CordovaLocationListener.POSITION_UNAVAILABLE, "Provider " + provider + " is out of service.");
+        }
+        else if (status == 1) {
+            Log.d(TAG, provider + " is TEMPORARILY_UNAVAILABLE");
+        }
+        else {
+            Log.d(TAG, provider + " is AVAILABLE");
+        }
+    }
+
+    /**
+     * Called when the location has changed.
+     *
+     * @param location
+     */
+    public void onLocationChanged(Location location) {
+        Log.d(TAG, "The location has been updated!");
+        this.win(location);
+    }
+
+    // PUBLIC
+
+    public int size() {
+        return this.watches.size() + this.callbacks.size();
+    }
+
+    public void addWatch(String timerId, CallbackContext callbackContext) {
+        this.watches.put(timerId, callbackContext);
+        if (this.size() == 1) {
+            this.start();
+        }
+    }
+    public void addCallback(CallbackContext callbackContext) {
+        this.callbacks.add(callbackContext);
+        if (this.size() == 1) {
+            this.start();
+        }
+    }
+    public void clearWatch(String timerId) {
+        if (this.watches.containsKey(timerId)) {
+            this.watches.remove(timerId);
+        }
+        if (this.size() == 0) {
+            this.stop();
+        }
+    }
+
+    /**
+     * Destroy listener.
+     */
+    public void destroy() {
+        this.stop();
+    }
+
+    // LOCAL
+
+    /**
+     * Start requesting location updates.
+     *
+     * @param interval
+     */
+    protected void start() {
+        if (!this.running) {
+            if (this.locationManager.getProvider(LocationManager.NETWORK_PROVIDER) != null) {
+                this.running = true;
+                this.locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 60000, 10, this);
+            } else {
+                this.fail(CordovaLocationListener.POSITION_UNAVAILABLE, "Network provider is not available.");
+            }
+        }
+    }
+
+    /**
+     * Stop receiving location updates.
+     */
+    private void stop() {
+        if (this.running) {
+            this.locationManager.removeUpdates(this);
+            this.running = false;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/CordovaWebView.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/CordovaWebView.java b/lib/cordova-android/framework/src/org/apache/cordova/CordovaWebView.java
new file mode 100755
index 0000000..44c9299
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/CordovaWebView.java
@@ -0,0 +1,985 @@
+/*
+       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;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Stack;
+
+import org.apache.cordova.Config;
+import org.apache.cordova.api.CordovaInterface;
+import org.apache.cordova.api.CordovaPlugin;
+import org.apache.cordova.api.LOG;
+import org.apache.cordova.api.PluginManager;
+import org.apache.cordova.api.PluginResult;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.webkit.WebBackForwardList;
+import android.webkit.WebHistoryItem;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebSettings.LayoutAlgorithm;
+import android.widget.FrameLayout;
+
+public class CordovaWebView extends WebView {
+
+    public static final String TAG = "CordovaWebView";
+
+    private ArrayList<Integer> keyDownCodes = new ArrayList<Integer>();
+    private ArrayList<Integer> keyUpCodes = new ArrayList<Integer>();
+
+    public PluginManager pluginManager;
+    private boolean paused;
+
+    private BroadcastReceiver receiver;
+
+
+    /** Activities and other important classes **/
+    private CordovaInterface cordova;
+    CordovaWebViewClient viewClient;
+    @SuppressWarnings("unused")
+    private CordovaChromeClient chromeClient;
+
+    //This is for the polyfill history
+    private String url;
+    String baseUrl;
+    private Stack<String> urls = new Stack<String>();
+
+    boolean useBrowserHistory = true;
+
+    // Flag to track that a loadUrl timeout occurred
+    int loadUrlTimeout = 0;
+
+    private boolean bound;
+
+    private boolean handleButton = false;
+    
+    private long lastMenuEventTime = 0;
+
+    NativeToJsMessageQueue jsMessageQueue;
+    ExposedJsApi exposedJsApi;
+
+    /** custom view created by the browser (a video player for example) */
+    private View mCustomView;
+    private WebChromeClient.CustomViewCallback mCustomViewCallback;
+
+    private ActivityResult mResult = null;
+
+    class ActivityResult {
+        
+        int request;
+        int result;
+        Intent incoming;
+        
+        public ActivityResult(int req, int res, Intent intent) {
+            request = req;
+            result = res;
+            incoming = intent;
+        }
+
+        
+    }
+    
+    static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER =
+            new FrameLayout.LayoutParams(
+            ViewGroup.LayoutParams.MATCH_PARENT,
+            ViewGroup.LayoutParams.MATCH_PARENT,
+            Gravity.CENTER);
+    
+    /**
+     * Constructor.
+     *
+     * @param context
+     */
+    public CordovaWebView(Context context) {
+        super(context);
+        if (CordovaInterface.class.isInstance(context))
+        {
+            this.cordova = (CordovaInterface) context;
+        }
+        else
+        {
+            Log.d(TAG, "Your activity must implement CordovaInterface to work");
+        }
+        this.loadConfiguration();
+        this.setup();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param context
+     * @param attrs
+     */
+    public CordovaWebView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        if (CordovaInterface.class.isInstance(context))
+        {
+            this.cordova = (CordovaInterface) context;
+        }
+        else
+        {
+            Log.d(TAG, "Your activity must implement CordovaInterface to work");
+        }
+        this.setWebChromeClient(new CordovaChromeClient(this.cordova, this));
+        this.initWebViewClient(this.cordova);
+        this.loadConfiguration();
+        this.setup();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param context
+     * @param attrs
+     * @param defStyle
+     *
+     */
+    public CordovaWebView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        if (CordovaInterface.class.isInstance(context))
+        {
+            this.cordova = (CordovaInterface) context;
+        }
+        else
+        {
+            Log.d(TAG, "Your activity must implement CordovaInterface to work");
+        }
+        this.setWebChromeClient(new CordovaChromeClient(this.cordova, this));
+        this.loadConfiguration();
+        this.setup();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param context
+     * @param attrs
+     * @param defStyle
+     * @param privateBrowsing
+     */
+    @TargetApi(11)
+    public CordovaWebView(Context context, AttributeSet attrs, int defStyle, boolean privateBrowsing) {
+        super(context, attrs, defStyle, privateBrowsing);
+        if (CordovaInterface.class.isInstance(context))
+        {
+            this.cordova = (CordovaInterface) context;
+        }
+        else
+        {
+            Log.d(TAG, "Your activity must implement CordovaInterface to work");
+        }
+        this.setWebChromeClient(new CordovaChromeClient(this.cordova));
+        this.initWebViewClient(this.cordova);
+        this.loadConfiguration();
+        this.setup();
+    }
+
+
+    private void initWebViewClient(CordovaInterface cordova) {
+        if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB)
+        {
+            this.setWebViewClient(new CordovaWebViewClient(this.cordova, this));
+        }
+        else
+        {
+            this.setWebViewClient(new IceCreamCordovaWebViewClient(this.cordova, this));
+        }
+    }
+
+    /**
+     * Initialize webview.
+     */
+    @SuppressWarnings("deprecation")
+    @SuppressLint("NewApi")
+    private void setup() {
+        this.setInitialScale(0);
+        this.setVerticalScrollBarEnabled(false);
+        this.requestFocusFromTouch();
+
+        // Enable JavaScript
+        WebSettings settings = this.getSettings();
+        settings.setJavaScriptEnabled(true);
+        settings.setJavaScriptCanOpenWindowsAutomatically(true);
+        settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
+        
+        // Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2)
+        try {
+            Method gingerbread_getMethod =  WebSettings.class.getMethod("setNavDump", new Class[] { boolean.class });
+            if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB)
+            {
+                gingerbread_getMethod.invoke(settings, true);
+            }
+        } catch (NoSuchMethodException e) {
+            Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8");
+        } catch (IllegalArgumentException e) {
+            Log.d(TAG, "Doing the NavDump failed with bad arguments");
+        } catch (IllegalAccessException e) {
+            Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore");
+        } catch (InvocationTargetException e) {
+            Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore.");
+        }
+
+        // Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist
+        // while we do this
+        if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+            Level16Apis.enableUniversalAccess(settings);
+        // Enable database
+        settings.setDatabaseEnabled(true);
+        String databasePath = this.cordova.getActivity().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
+        settings.setDatabasePath(databasePath);
+
+        // Enable DOM storage
+        settings.setDomStorageEnabled(true);
+
+        // Enable built-in geolocation
+        settings.setGeolocationEnabled(true);
+        
+        // Fix for CB-1405
+        // Google issue 4641
+        this.updateUserAgentString();
+        
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        if (this.receiver == null) {
+            this.receiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    updateUserAgentString();
+                }
+            };
+            this.cordova.getActivity().registerReceiver(this.receiver, intentFilter);
+        }
+        // end CB-1405
+
+        pluginManager = new PluginManager(this, this.cordova);
+        jsMessageQueue = new NativeToJsMessageQueue(this, cordova);
+        exposedJsApi = new ExposedJsApi(pluginManager, jsMessageQueue);
+        exposeJsInterface();
+    }
+    
+    private void updateUserAgentString() {
+        this.getSettings().getUserAgentString();
+    }
+
+    private void exposeJsInterface() {
+        int SDK_INT = Build.VERSION.SDK_INT;
+        boolean isHoneycomb = (SDK_INT >= Build.VERSION_CODES.HONEYCOMB && SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2);
+        if (isHoneycomb || (SDK_INT < Build.VERSION_CODES.GINGERBREAD)) {
+            Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");
+            // Bug being that Java Strings do not get converted to JS strings automatically.
+            // This isn't hard to work-around on the JS side, but it's easier to just
+            // use the prompt bridge instead.
+            return;            
+        } else if (SDK_INT < Build.VERSION_CODES.HONEYCOMB && Build.MANUFACTURER.equals("unknown")) {
+            // addJavascriptInterface crashes on the 2.3 emulator.
+            Log.i(TAG, "Disabled addJavascriptInterface() bridge callback due to a bug on the 2.3 emulator");
+            return;
+        }
+        this.addJavascriptInterface(exposedJsApi, "_cordovaNative");
+    }
+
+    /**
+     * Set the WebViewClient.
+     *
+     * @param client
+     */
+    public void setWebViewClient(CordovaWebViewClient client) {
+        this.viewClient = client;
+        super.setWebViewClient(client);
+    }
+
+    /**
+     * Set the WebChromeClient.
+     *
+     * @param client
+     */
+    public void setWebChromeClient(CordovaChromeClient client) {
+        this.chromeClient = client;
+        super.setWebChromeClient(client);
+    }
+    
+    public CordovaChromeClient getWebChromeClient() {
+        return this.chromeClient;
+    }
+
+    /**
+     * Load the url into the webview.
+     *
+     * @param url
+     */
+    @Override
+    public void loadUrl(String url) {
+        if (url.equals("about:blank") || url.startsWith("javascript:")) {
+            this.loadUrlNow(url);
+        }
+        else {
+
+            String initUrl = this.getProperty("url", null);
+
+            // If first page of app, then set URL to load to be the one passed in
+            if (initUrl == null || (this.urls.size() > 0)) {
+                this.loadUrlIntoView(url);
+            }
+            // Otherwise use the URL specified in the activity's extras bundle
+            else {
+                this.loadUrlIntoView(initUrl);
+            }
+        }
+    }
+
+    /**
+     * Load the url into the webview after waiting for period of time.
+     * This is used to display the splashscreen for certain amount of time.
+     *
+     * @param url
+     * @param time              The number of ms to wait before loading webview
+     */
+    public void loadUrl(final String url, int time) {
+        String initUrl = this.getProperty("url", null);
+
+        // If first page of app, then set URL to load to be the one passed in
+        if (initUrl == null || (this.urls.size() > 0)) {
+            this.loadUrlIntoView(url, time);
+        }
+        // Otherwise use the URL specified in the activity's extras bundle
+        else {
+            this.loadUrlIntoView(initUrl);
+        }
+    }
+
+    /**
+     * Load the url into the webview.
+     *
+     * @param url
+     */
+    public void loadUrlIntoView(final String url) {
+        LOG.d(TAG, ">>> loadUrl(" + url + ")");
+
+        this.url = url;
+        if (this.baseUrl == null) {
+            int i = url.lastIndexOf('/');
+            if (i > 0) {
+                this.baseUrl = url.substring(0, i + 1);
+            }
+            else {
+                this.baseUrl = this.url + "/";
+            }
+
+            this.pluginManager.init();
+
+            if (!this.useBrowserHistory) {
+                this.urls.push(url);
+            }
+        }
+
+        // Create a timeout timer for loadUrl
+        final CordovaWebView me = this;
+        final int currentLoadUrlTimeout = me.loadUrlTimeout;
+        final int loadUrlTimeoutValue = Integer.parseInt(this.getProperty("loadUrlTimeoutValue", "20000"));
+
+        // Timeout error method
+        final Runnable loadError = new Runnable() {
+            public void run() {
+                me.stopLoading();
+                LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!");
+                if (viewClient != null) {
+                    viewClient.onReceivedError(me, -6, "The connection to the server was unsuccessful.", url);
+                }
+            }
+        };
+
+        // Timeout timer method
+        final Runnable timeoutCheck = new Runnable() {
+            public void run() {
+                try {
+                    synchronized (this) {
+                        wait(loadUrlTimeoutValue);
+                    }
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+
+                // If timeout, then stop loading and handle error
+                if (me.loadUrlTimeout == currentLoadUrlTimeout) {
+                    me.cordova.getActivity().runOnUiThread(loadError);
+                }
+            }
+        };
+
+        // Load url
+        this.cordova.getActivity().runOnUiThread(new Runnable() {
+            public void run() {
+                Thread thread = new Thread(timeoutCheck);
+                thread.start();
+                me.loadUrlNow(url);
+            }
+        });
+    }
+
+    /**
+     * Load URL in webview.
+     *
+     * @param url
+     */
+    void loadUrlNow(String url) {
+        if (LOG.isLoggable(LOG.DEBUG) && !url.startsWith("javascript:")) {
+            LOG.d(TAG, ">>> loadUrlNow()");
+        }
+        if (url.startsWith("file://") || url.indexOf(this.baseUrl) == 0 || url.startsWith("javascript:") || Config.isUrlWhiteListed(url)) {
+            super.loadUrl(url);
+        }
+    }
+
+    /**
+     * Load the url into the webview after waiting for period of time.
+     * This is used to display the splashscreen for certain amount of time.
+     *
+     * @param url
+     * @param time              The number of ms to wait before loading webview
+     */
+    public void loadUrlIntoView(final String url, final int time) {
+
+        // If not first page of app, then load immediately
+        // Add support for browser history if we use it.
+        if ((url.startsWith("javascript:")) || this.urls.size() > 0 || this.canGoBack()) {
+        }
+
+        // If first page, then show splashscreen
+        else {
+
+            LOG.d(TAG, "DroidGap.loadUrl(%s, %d)", url, time);
+
+            // Send message to show splashscreen now if desired
+            this.postMessage("splashscreen", "show");
+        }
+
+        // Load url
+        this.loadUrlIntoView(url);
+    }
+    
+    /**
+     * Send JavaScript statement back to JavaScript.
+     * (This is a convenience method)
+     *
+     * @param message
+     */
+    public void sendJavascript(String statement) {
+        this.jsMessageQueue.addJavaScript(statement);
+    }
+
+    /**
+     * Send a plugin result back to JavaScript.
+     * (This is a convenience method)
+     *
+     * @param message
+     */
+    public void sendPluginResult(PluginResult result, String callbackId) {
+        this.jsMessageQueue.addPluginResult(result, callbackId);
+    }
+
+    /**
+     * Send a message to all plugins.
+     *
+     * @param id            The message id
+     * @param data          The message data
+     */
+    public void postMessage(String id, Object data) {
+        if (this.pluginManager != null) {
+            this.pluginManager.postMessage(id, data);
+        }
+    }
+
+    /**
+     * Returns the top url on the stack without removing it from
+     * the stack.
+     */
+    public String peekAtUrlStack() {
+        if (this.urls.size() > 0) {
+            return this.urls.peek();
+        }
+        return "";
+    }
+
+    /**
+     * Add a url to the stack
+     *
+     * @param url
+     */
+    public void pushUrl(String url) {
+        this.urls.push(url);
+    }
+
+    /**
+     * Go to previous page in history.  (We manage our own history)
+     *
+     * @return true if we went back, false if we are already at top
+     */
+    public boolean backHistory() {
+
+        // Check webview first to see if there is a history
+        // This is needed to support curPage#diffLink, since they are added to appView's history, but not our history url array (JQMobile behavior)
+        if (super.canGoBack()) {
+            printBackForwardList();
+            super.goBack();
+            
+            return true;
+        }
+
+        // If our managed history has prev url
+        if (this.urls.size() > 1 && !this.useBrowserHistory) {
+            this.urls.pop();                // Pop current url
+            String url = this.urls.pop();   // Pop prev url that we want to load, since it will be added back by loadUrl()
+            this.loadUrl(url);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Return true if there is a history item.
+     *
+     * @return
+     */
+    public boolean canGoBack() {
+        if (super.canGoBack()) {
+            return true;
+        }
+        if (this.urls.size() > 1) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Load the specified URL in the Cordova webview or a new browser instance.
+     *
+     * NOTE: If openExternal is false, only URLs listed in whitelist can be loaded.
+     *
+     * @param url           The url to load.
+     * @param openExternal  Load url in browser instead of Cordova webview.
+     * @param clearHistory  Clear the history stack, so new page becomes top of history
+     * @param params        DroidGap parameters for new app
+     */
+    public void showWebPage(String url, boolean openExternal, boolean clearHistory, HashMap<String, Object> params) {
+        LOG.d(TAG, "showWebPage(%s, %b, %b, HashMap", url, openExternal, clearHistory);
+
+        // If clearing history
+        if (clearHistory) {
+            this.clearHistory();
+        }
+
+        // If loading into our webview
+        if (!openExternal) {
+
+            // Make sure url is in whitelist
+            if (url.startsWith("file://") || url.indexOf(this.baseUrl) == 0 || Config.isUrlWhiteListed(url)) {
+                // TODO: What about params?
+
+                // Clear out current url from history, since it will be replacing it
+                if (clearHistory) {
+                    this.urls.clear();
+                }
+
+                // Load new URL
+                this.loadUrl(url);
+            }
+            // Load in default viewer if not
+            else {
+                LOG.w(TAG, "showWebPage: Cannot load URL into webview since it is not in white list.  Loading into browser instead. (URL=" + url + ")");
+                try {
+                    Intent intent = new Intent(Intent.ACTION_VIEW);
+                    intent.setData(Uri.parse(url));
+                    cordova.getActivity().startActivity(intent);
+                } catch (android.content.ActivityNotFoundException e) {
+                    LOG.e(TAG, "Error loading url " + url, e);
+                }
+            }
+        }
+
+        // Load in default view intent
+        else {
+            try {
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setData(Uri.parse(url));
+                cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error loading url " + url, e);
+            }
+        }
+    }
+
+    /**
+     * Check configuration parameters from Config.
+     * Approved list of URLs that can be loaded into DroidGap
+     *      <access origin="http://server regexp" subdomains="true" />
+     * Log level: ERROR, WARN, INFO, DEBUG, VERBOSE (default=ERROR)
+     *      <log level="DEBUG" />
+     */
+    private void loadConfiguration() {
+        // Config has already been loaded, and it stores these preferences on the Intent.
+        if("false".equals(this.getProperty("useBrowserHistory", "true")))
+        {
+            //Switch back to the old browser history and state the six month policy
+            this.useBrowserHistory = false;
+            Log.w(TAG, "useBrowserHistory=false is deprecated as of Cordova 2.2.0 and will be removed six months after the 2.2.0 release.  Please use the browser history and use history.back().");
+        }
+ 
+        if ("true".equals(this.getProperty("fullscreen", "false"))) {
+            this.cordova.getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
+            this.cordova.getActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        }
+    }
+
+    /**
+     * Get string property for activity.
+     *
+     * @param name
+     * @param defaultValue
+     * @return
+     */
+    public String getProperty(String name, String defaultValue) {
+        Bundle bundle = this.cordova.getActivity().getIntent().getExtras();
+        if (bundle == null) {
+            return defaultValue;
+        }
+        Object p = bundle.get(name);
+        if (p == null) {
+            return defaultValue;
+        }
+        return p.toString();
+    }
+
+    /*
+     * onKeyDown
+     */
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event)
+    {
+        if(keyDownCodes.contains(keyCode))
+        {
+            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
+                    // only override default behavior is event bound
+                    LOG.d(TAG, "Down Key Hit");
+                    this.loadUrl("javascript:cordova.fireDocumentEvent('volumedownbutton');");
+                    return true;
+            }
+            // If volumeup key
+            else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+                    LOG.d(TAG, "Up Key Hit");
+                    this.loadUrl("javascript:cordova.fireDocumentEvent('volumeupbutton');");
+                    return true;
+            }
+            else
+            {
+                return super.onKeyDown(keyCode, event);
+            }
+        }
+        else if(keyCode == KeyEvent.KEYCODE_BACK)
+        {
+            //Because exit is fired on the keyDown and not the key up on Android 4.x
+            //we need to check for this.
+            //Also, I really wished "canGoBack" worked!
+            if(this.useBrowserHistory)
+                return !(this.startOfHistory()) || this.bound;
+            else
+                return this.urls.size() > 1 || this.bound;
+        }
+        
+        return super.onKeyDown(keyCode, event);
+    }
+    
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event)
+    {
+        // If back key
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            // A custom view is currently displayed  (e.g. playing a video)
+            if(mCustomView != null) {
+                this.hideCustomView();
+            } else {
+                // The webview is currently displayed
+                // If back key is bound, then send event to JavaScript
+                if (this.bound) {
+                    this.loadUrl("javascript:cordova.fireDocumentEvent('backbutton');");
+                    return true;
+                } else {
+                    // If not bound
+                    // Go to previous page in webview if it is possible to go back
+                    if (this.backHistory()) {
+                        return true;
+                    }
+                    // If not, then invoke default behaviour
+                    else {
+                        //this.activityState = ACTIVITY_EXITING;
+                        return false;
+                    }
+                }
+            }
+        }
+        // Legacy
+        else if (keyCode == KeyEvent.KEYCODE_MENU) {
+            if (this.lastMenuEventTime < event.getEventTime()) {
+                this.loadUrl("javascript:cordova.fireDocumentEvent('menubutton');");
+            }
+            this.lastMenuEventTime = event.getEventTime();
+            return super.onKeyUp(keyCode, event);
+        }
+        // If search key
+        else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
+            this.loadUrl("javascript:cordova.fireDocumentEvent('searchbutton');");
+            return true;
+        }
+        else if(keyUpCodes.contains(keyCode))
+        {
+            //What the hell should this do?
+            return super.onKeyUp(keyCode, event);
+        }
+
+        //Does webkit change this behavior?
+        return super.onKeyUp(keyCode, event);
+    }
+
+    
+    public void bindButton(boolean override)
+    {
+        this.bound = override;
+    }
+
+    public void bindButton(String button, boolean override) {
+        // TODO Auto-generated method stub
+        if (button.compareTo("volumeup")==0) {
+          keyDownCodes.add(KeyEvent.KEYCODE_VOLUME_UP);
+        }
+        else if (button.compareTo("volumedown")==0) {
+          keyDownCodes.add(KeyEvent.KEYCODE_VOLUME_DOWN);
+        }
+      }
+
+    public void bindButton(int keyCode, boolean keyDown, boolean override) {
+       if(keyDown)
+       {
+           keyDownCodes.add(keyCode);
+       }
+       else
+       {
+           keyUpCodes.add(keyCode);
+       }
+    }
+
+    public boolean isBackButtonBound()
+    {
+        return this.bound;
+    }
+    
+    public void handlePause(boolean keepRunning)
+    {
+        LOG.d(TAG, "Handle the pause");
+        // Send pause event to JavaScript
+        this.loadUrl("javascript:try{cordova.fireDocumentEvent('pause');}catch(e){console.log('exception firing pause event from native');};");
+
+        // Forward to plugins
+        if (this.pluginManager != null) {
+            this.pluginManager.onPause(keepRunning);
+        }
+
+        // If app doesn't want to run in background
+        if (!keepRunning) {
+            // Pause JavaScript timers (including setInterval)
+            this.pauseTimers();
+        }
+        paused = true;
+   
+    }
+    
+    public void handleResume(boolean keepRunning, boolean activityResultKeepRunning)
+    {
+
+        this.loadUrl("javascript:try{cordova.fireDocumentEvent('resume');}catch(e){console.log('exception firing resume event from native');};");
+        
+        // Forward to plugins
+        if (this.pluginManager != null) {
+            this.pluginManager.onResume(keepRunning);
+        }
+
+        // Resume JavaScript timers (including setInterval)
+        this.resumeTimers();
+        paused = false;
+    }
+    
+    public void handleDestroy()
+    {
+        // Send destroy event to JavaScript
+        this.loadUrl("javascript:try{cordova.require('cordova/channel').onDestroy.fire();}catch(e){console.log('exception firing destroy event from native');};");
+
+        // Load blank page so that JavaScript onunload is called
+        this.loadUrl("about:blank");
+
+        // Forward to plugins
+        if (this.pluginManager != null) {
+            this.pluginManager.onDestroy();
+        }
+        
+        // unregister the receiver
+        if (this.receiver != null) {
+            try {
+                this.cordova.getActivity().unregisterReceiver(this.receiver);
+            } catch (Exception e) {
+                Log.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e);
+            }
+        }
+    }
+    
+    public void onNewIntent(Intent intent)
+    {
+        //Forward to plugins
+        if (this.pluginManager != null) {
+            this.pluginManager.onNewIntent(intent);
+        }
+    }
+    
+    public boolean isPaused()
+    {
+        return paused;
+    }
+
+    public boolean hadKeyEvent() {
+        return handleButton;
+    }
+
+    // Wrapping these functions in their own class prevents warnings in adb like:
+    // VFY: unable to resolve virtual method 285: Landroid/webkit/WebSettings;.setAllowUniversalAccessFromFileURLs
+    @TargetApi(16)
+    private static class Level16Apis {
+        static void enableUniversalAccess(WebSettings settings) {
+            settings.setAllowUniversalAccessFromFileURLs(true);
+        }
+    }
+    
+    public void printBackForwardList() {
+        WebBackForwardList currentList = this.copyBackForwardList();
+        int currentSize = currentList.getSize();
+        for(int i = 0; i < currentSize; ++i)
+        {
+            WebHistoryItem item = currentList.getItemAtIndex(i);
+            String url = item.getUrl();
+            LOG.d(TAG, "The URL at index: " + Integer.toString(i) + "is " + url );
+        }
+    }
+    
+    
+    //Can Go Back is BROKEN!
+    public boolean startOfHistory()
+    {
+        WebBackForwardList currentList = this.copyBackForwardList();
+        WebHistoryItem item = currentList.getItemAtIndex(0);
+        String url = item.getUrl();
+        String currentUrl = this.getUrl();
+        LOG.d(TAG, "The current URL is: " + currentUrl);
+        LOG.d(TAG, "The URL at item 0 is:" + url);
+        return currentUrl.equals(url);
+    }
+
+    public void showCustomView(View view, WebChromeClient.CustomViewCallback callback) {
+        // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
+        Log.d(TAG, "showing Custom View");
+        // if a view already exists then immediately terminate the new one
+        if (mCustomView != null) {
+            callback.onCustomViewHidden();
+            return;
+        }
+        
+        // Store the view and its callback for later (to kill it properly)
+        mCustomView = view;
+        mCustomViewCallback = callback;
+        
+        // Add the custom view to its container.
+        ViewGroup parent = (ViewGroup) this.getParent();
+        parent.addView(view, COVER_SCREEN_GRAVITY_CENTER);
+        
+        // Hide the content view.
+        this.setVisibility(View.GONE);
+        
+        // Finally show the custom view container.
+        parent.setVisibility(View.VISIBLE);
+        parent.bringToFront();
+    }
+
+    public void hideCustomView() {
+        // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
+        Log.d(TAG, "Hidding Custom View");
+        if (mCustomView == null) return;
+
+        // Hide the custom view.
+        mCustomView.setVisibility(View.GONE);
+        
+        // Remove the custom view from its container.
+        ViewGroup parent = (ViewGroup) this.getParent();
+        parent.removeView(mCustomView);
+        mCustomView = null;
+        mCustomViewCallback.onCustomViewHidden();
+        
+        // Show the content view.
+        this.setVisibility(View.VISIBLE);
+    }
+    
+    /**
+     * if the video overlay is showing then we need to know 
+     * as it effects back button handling
+     * 
+     * @return
+     */
+    public boolean isCustomViewShowing() {
+        return mCustomView != null;
+    }
+    
+    public WebBackForwardList restoreState(Bundle savedInstanceState)
+    {
+        WebBackForwardList myList = super.restoreState(savedInstanceState);
+        Log.d(TAG, "WebView restoration crew now restoring!");
+        //Initialize the plugin manager once more
+        this.pluginManager.init();
+        return myList;
+    }
+
+    public void storeResult(int requestCode, int resultCode, Intent intent) {
+        mResult = new ActivityResult(requestCode, resultCode, intent);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/CordovaWebViewClient.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/CordovaWebViewClient.java b/lib/cordova-android/framework/src/org/apache/cordova/CordovaWebViewClient.java
new file mode 100755
index 0000000..7a7e6ed
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/CordovaWebViewClient.java
@@ -0,0 +1,481 @@
+/*
+       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;
+
+import java.util.Hashtable;
+
+import org.apache.cordova.api.CordovaInterface;
+
+import org.apache.cordova.api.LOG;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.util.Log;
+import android.view.View;
+import android.webkit.HttpAuthHandler;
+import android.webkit.SslErrorHandler;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+/**
+ * This class is the WebViewClient that implements callbacks for our web view.
+ */
+public class CordovaWebViewClient extends WebViewClient {
+
+	private static final String TAG = "Cordova";
+	private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/";
+    CordovaInterface cordova;
+    CordovaWebView appView;
+    private boolean doClearHistory = false;
+
+    /** The authorization tokens. */
+    private Hashtable<String, AuthenticationToken> authenticationTokens = new Hashtable<String, AuthenticationToken>();
+
+    /**
+     * Constructor.
+     *
+     * @param cordova
+     */
+    public CordovaWebViewClient(CordovaInterface cordova) {
+        this.cordova = cordova;
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param cordova
+     * @param view
+     */
+    public CordovaWebViewClient(CordovaInterface cordova, CordovaWebView view) {
+        this.cordova = cordova;
+        this.appView = view;
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param view
+     */
+    public void setWebView(CordovaWebView view) {
+        this.appView = view;
+    }
+
+
+    // Parses commands sent by setting the webView's URL to:
+    // cdvbrg:service/action/callbackId#jsonArgs
+	private void handleExecUrl(String url) {
+		int idx1 = CORDOVA_EXEC_URL_PREFIX.length();
+		int idx2 = url.indexOf('#', idx1 + 1);
+		int idx3 = url.indexOf('#', idx2 + 1);
+		int idx4 = url.indexOf('#', idx3 + 1);
+		if (idx1 == -1 || idx2 == -1 || idx3 == -1 || idx4 == -1) {
+			Log.e(TAG, "Could not decode URL command: " + url);
+			return;
+		}
+		String service    = url.substring(idx1, idx2);
+		String action     = url.substring(idx2 + 1, idx3);
+		String callbackId = url.substring(idx3 + 1, idx4);
+		String jsonArgs   = url.substring(idx4 + 1);
+        appView.pluginManager.exec(service, action, callbackId, jsonArgs);
+	}    
+	
+    /**
+     * Give the host application a chance to take over the control when a new url
+     * is about to be loaded in the current WebView.
+     *
+     * @param view          The WebView that is initiating the callback.
+     * @param url           The url to be loaded.
+     * @return              true to override, false for default behavior
+     */
+	@Override
+    public boolean shouldOverrideUrlLoading(WebView view, String url) {
+    	// Check if it's an exec() bridge command message.
+    	if (NativeToJsMessageQueue.ENABLE_LOCATION_CHANGE_EXEC_MODE && url.startsWith(CORDOVA_EXEC_URL_PREFIX)) {
+    		handleExecUrl(url);
+    	}
+
+        // Give plugins the chance to handle the url
+    	else if ((this.appView.pluginManager != null) && this.appView.pluginManager.onOverrideUrlLoading(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));
+                this.cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error dialing " + url + ": " + e.toString());
+            }
+        }
+
+        // If displaying map (geo:0,0?q=address)
+        else if (url.startsWith("geo:")) {
+            try {
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setData(Uri.parse(url));
+                this.cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error showing map " + url + ": " + e.toString());
+            }
+        }
+
+        // If sending email (mailto:abc@corp.com)
+        else if (url.startsWith(WebView.SCHEME_MAILTO)) {
+            try {
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setData(Uri.parse(url));
+                this.cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error sending email " + 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");
+                this.cordova.getActivity().startActivity(intent);
+            } catch (android.content.ActivityNotFoundException e) {
+                LOG.e(TAG, "Error sending sms " + url + ":" + e.toString());
+            }
+        }
+
+        // All else
+        else {
+
+            // If our app or file:, then load into a new Cordova webview container by starting a new instance of our activity.
+            // Our app continues to run.  When BACK is pressed, our app is redisplayed.
+            if (url.startsWith("file://") || url.startsWith("data:") || url.indexOf(this.appView.baseUrl) == 0 || Config.isUrlWhiteListed(url)) {
+                //This will fix iFrames
+                if (appView.useBrowserHistory || url.startsWith("data:"))
+                    return false;
+                else
+                    this.appView.loadUrl(url);
+            }
+
+            // If not our application, let default viewer handle
+            else {
+                try {
+                    Intent intent = new Intent(Intent.ACTION_VIEW);
+                    intent.setData(Uri.parse(url));
+                    this.cordova.getActivity().startActivity(intent);
+                } catch (android.content.ActivityNotFoundException e) {
+                    LOG.e(TAG, "Error loading url " + url, e);
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * On received http auth request.
+     * The method reacts on all registered authentication tokens. There is one and only one authentication token for any host + realm combination
+     *
+     * @param view
+     * @param handler
+     * @param host
+     * @param realm
+     */
+    @Override
+    public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
+
+        // Get the authentication token
+        AuthenticationToken token = this.getAuthenticationToken(host, realm);
+        if (token != null) {
+            handler.proceed(token.getUserName(), token.getPassword());
+        }
+    }
+
+    /**
+     * Notify the host application that a page has started loading. 
+     * This method is called once for each main frame load so a page with iframes or framesets will call onPageStarted 
+     * one time for the main frame. This also means that onPageStarted will not be called when the contents of an 
+     * embedded frame changes, i.e. clicking a link whose target is an iframe. 
+     * 
+     * @param view          The webview initiating the callback.
+     * @param url           The url of the page.
+     */
+    @Override
+    public void onPageStarted(WebView view, String url, Bitmap favicon) {
+        // Clear history so history.back() doesn't do anything.
+        // So we can reinit() native side CallbackServer & PluginManager.
+        if (!this.appView.useBrowserHistory) {
+            view.clearHistory();
+            this.doClearHistory = true;
+        }
+
+        // Flush stale messages.
+        this.appView.jsMessageQueue.reset();
+
+        // Broadcast message that page has loaded
+        this.appView.postMessage("onPageStarted", url);
+
+        // Notify all plugins of the navigation, so they can clean up if necessary.
+        if (this.appView.pluginManager != null) {
+            this.appView.pluginManager.onReset();
+        }
+    }
+
+    /**
+     * Notify the host application that a page has finished loading.
+     * This method is called only for main frame. When onPageFinished() is called, the rendering picture may not be updated yet.
+     * 
+     *
+     * @param view          The webview initiating the callback.
+     * @param url           The url of the page.
+     */
+    @Override
+    public void onPageFinished(WebView view, String url) {
+        super.onPageFinished(view, url);
+        LOG.d(TAG, "onPageFinished(" + url + ")");
+
+        /**
+         * Because of a timing issue we need to clear this history in onPageFinished as well as
+         * onPageStarted. However we only want to do this if the doClearHistory boolean is set to
+         * true. You see when you load a url with a # in it which is common in jQuery applications
+         * onPageStared is not called. Clearing the history at that point would break jQuery apps.
+         */
+        if (this.doClearHistory) {
+            view.clearHistory();
+            this.doClearHistory = false;
+        }
+
+        // Clear timeout flag
+        this.appView.loadUrlTimeout++;
+
+        // Try firing the onNativeReady event in JS. If it fails because the JS is
+        // not loaded yet then just set a flag so that the onNativeReady can be fired
+        // from the JS side when the JS gets to that code.
+        if (!url.equals("about:blank")) {
+            LOG.d(TAG, "Trying to fire onNativeReady");
+            this.appView.loadUrl("javascript:try{ cordova.require('cordova/channel').onNativeReady.fire();}catch(e){_nativeReady = true;}");
+            this.appView.postMessage("onNativeReady", null);
+        }
+
+        // Broadcast message that page has loaded
+        this.appView.postMessage("onPageFinished", url);
+
+        // Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly
+        if (this.appView.getVisibility() == View.INVISIBLE) {
+            Thread t = new Thread(new Runnable() {
+                public void run() {
+                    try {
+                        Thread.sleep(2000);
+                        cordova.getActivity().runOnUiThread(new Runnable() {
+                            public void run() {
+                                appView.postMessage("spinner", "stop");
+                            }
+                        });
+                    } catch (InterruptedException e) {
+                    }
+                }
+            });
+            t.start();
+        }
+
+        // Shutdown if blank loaded
+        if (url.equals("about:blank")) {
+            appView.postMessage("exit", null);
+        }
+    }
+
+    /**
+     * Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable).
+     * The errorCode parameter corresponds to one of the ERROR_* constants.
+     *
+     * @param view          The WebView that is initiating the callback.
+     * @param errorCode     The error code corresponding to an ERROR_* value.
+     * @param description   A String describing the error.
+     * @param failingUrl    The url that failed to load.
+     */
+    @Override
+    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+        LOG.d(TAG, "CordovaWebViewClient.onReceivedError: Error code=%s Description=%s URL=%s", errorCode, description, failingUrl);
+
+        // Clear timeout flag
+        this.appView.loadUrlTimeout++;
+
+        // Handle error
+        JSONObject data = new JSONObject();
+        try {
+            data.put("errorCode", errorCode);
+            data.put("description", description);
+            data.put("url", failingUrl);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+        this.appView.postMessage("onReceivedError", data);
+    }
+
+    /**
+     * Notify the host application that an SSL error occurred while loading a resource. 
+     * The host application must call either handler.cancel() or handler.proceed(). 
+     * Note that the decision may be retained for use in response to future SSL errors. 
+     * The default behavior is to cancel the load.
+     * 
+     * @param view          The WebView that is initiating the callback.
+     * @param handler       An SslErrorHandler object that will handle the user's response.
+     * @param error         The SSL error object.
+     */
+    @TargetApi(8)
+    @Override
+    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
+
+        final String packageName = this.cordova.getActivity().getPackageName();
+        final PackageManager pm = this.cordova.getActivity().getPackageManager();
+
+        ApplicationInfo appInfo;
+        try {
+            appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
+            if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
+                // debug = true
+                handler.proceed();
+                return;
+            } else {
+                // debug = false
+                super.onReceivedSslError(view, handler, error);
+            }
+        } catch (NameNotFoundException e) {
+            // When it doubt, lock it out!
+            super.onReceivedSslError(view, handler, error);
+        }
+    }
+
+    /**
+     * Notify the host application to update its visited links database.
+     * 
+     * @param view          The WebView that is initiating the callback.
+     * @param url           The url being visited.
+     * @param isReload      True if this url is being reloaded.
+     */
+    @Override
+    public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
+        /*
+         * If you do a document.location.href the url does not get pushed on the stack
+         * so we do a check here to see if the url should be pushed.
+         */
+        if (!this.appView.peekAtUrlStack().equals(url)) {
+            this.appView.pushUrl(url);
+        }
+    }
+
+    /**
+     * Sets the authentication token.
+     * 
+     * @param authenticationToken
+     * @param host
+     * @param realm
+     */
+    public void setAuthenticationToken(AuthenticationToken authenticationToken, String host, String realm) {
+        if (host == null) {
+            host = "";
+        }
+        if (realm == null) {
+            realm = "";
+        }
+        this.authenticationTokens.put(host.concat(realm), authenticationToken);
+    }
+
+    /**
+     * Removes the authentication token.
+     * 
+     * @param host
+     * @param realm
+     * 
+     * @return the authentication token or null if did not exist
+     */
+    public AuthenticationToken removeAuthenticationToken(String host, String realm) {
+        return this.authenticationTokens.remove(host.concat(realm));
+    }
+
+    /**
+     * Gets the authentication token.
+     * 
+     * In order it tries:
+     * 1- host + realm
+     * 2- host
+     * 3- realm
+     * 4- no host, no realm
+     * 
+     * @param host
+     * @param realm
+     * 
+     * @return the authentication token
+     */
+    public AuthenticationToken getAuthenticationToken(String host, String realm) {
+        AuthenticationToken token = null;
+        token = this.authenticationTokens.get(host.concat(realm));
+
+        if (token == null) {
+            // try with just the host
+            token = this.authenticationTokens.get(host);
+
+            // Try the realm
+            if (token == null) {
+                token = this.authenticationTokens.get(realm);
+            }
+
+            // if no host found, just query for default
+            if (token == null) {
+                token = this.authenticationTokens.get("");
+            }
+        }
+
+        return token;
+    }
+
+    /**
+     * Clear all authentication tokens.
+     */
+    public void clearAuthenticationTokens() {
+        this.authenticationTokens.clear();
+    }
+
+}