Merge branch 'master' into 4.0.x (Bridge fixes)

Conflicts:
	framework/src/org/apache/cordova/CordovaChromeClient.java
	framework/src/org/apache/cordova/CordovaUriHelper.java
	framework/src/org/apache/cordova/CordovaWebView.java
	framework/src/org/apache/cordova/CordovaWebViewClient.java
	framework/src/org/apache/cordova/ExposedJsApi.java
	framework/src/org/apache/cordova/NativeToJsMessageQueue.java
	framework/src/org/apache/cordova/PluginManager.java
This commit is contained in:
Andrew Grieve 2014-07-03 23:02:02 -04:00
commit 4ca2305693
12 changed files with 193 additions and 380 deletions

View File

@ -44,6 +44,9 @@ import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.util.Log;
/** /**
* This class is the WebChromeClient that implements callbacks for our web view. * This class is the WebChromeClient that implements callbacks for our web view.
@ -60,7 +63,6 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom
public static final int FILECHOOSER_RESULTCODE = 5173; public static final int FILECHOOSER_RESULTCODE = 5173;
private static final String LOG_TAG = "CordovaChromeClient"; private static final String LOG_TAG = "CordovaChromeClient";
private String TAG = "CordovaLog";
private long MAX_QUOTA = 100 * 1024 * 1024; private long MAX_QUOTA = 100 * 1024 * 1024;
protected CordovaInterface cordova; protected CordovaInterface cordova;
protected CordovaWebView appView; protected CordovaWebView appView;
@ -201,68 +203,75 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom
* Since we are hacking prompts for our own purposes, we should not be using them for * 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! * this purpose, perhaps we should hack console.log to do this instead!
* *
* @param view
* @param url
* @param message
* @param defaultValue
* @param result
* @see Other implementation in the Dialogs plugin. * @see Other implementation in the Dialogs plugin.
*/ */
@Override @Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, JsPromptResult result) {
// Unlike the @JavascriptInterface bridge, this method is always called on the UI thread.
// Security check to make sure any requests are coming from the page initially if (defaultValue != null && defaultValue.length() > 3 && defaultValue.startsWith("gap:")) {
// loaded in webview and not another loaded in an iframe.
boolean reqOk = false;
if (url.startsWith("file://") || 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; JSONArray array;
try { try {
array = new JSONArray(defaultValue.substring(4)); array = new JSONArray(defaultValue.substring(4));
String service = array.getString(0); int bridgeSecret = array.getInt(0);
String action = array.getString(1); String service = array.getString(1);
String callbackId = array.getString(2); String action = array.getString(2);
String callbackId = array.getString(3);
//String r = this.appView.exposedJsApi.exec(service, action, callbackId, message); String r = appView.exposedJsApi.exec(bridgeSecret, service, action, callbackId, message);
String r = this.appView.exec(service, action, callbackId, message);
result.confirm(r == null ? "" : r); result.confirm(r == null ? "" : r);
} catch (JSONException e) { } catch (JSONException e) {
e.printStackTrace(); e.printStackTrace();
return false; result.cancel();
} catch (IllegalAccessException e) {
e.printStackTrace();
result.cancel();
} }
} }
// Sets the native->JS bridge mode. // Sets the native->JS bridge mode.
else if (reqOk && defaultValue != null && defaultValue.equals("gap_bridge_mode:")) { else if (defaultValue != null && defaultValue.startsWith("gap_bridge_mode:")) {
try { try {
//this.appView.exposedJsApi.setNativeToJsBridgeMode(Integer.parseInt(message)); int bridgeSecret = Integer.parseInt(defaultValue.substring(16));
this.appView.setNativeToJsBridgeMode(Integer.parseInt(message)); appView.exposedJsApi.setNativeToJsBridgeMode(bridgeSecret, Integer.parseInt(message));
result.confirm(""); result.cancel();
} catch (NumberFormatException e){ } catch (NumberFormatException e){
result.confirm("");
e.printStackTrace(); e.printStackTrace();
} result.cancel();
} catch (IllegalAccessException e) {
e.printStackTrace();
result.cancel();
}
} }
// Polling for JavaScript messages // Polling for JavaScript messages
else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) { else if (defaultValue != null && defaultValue.startsWith("gap_poll:")) {
//String r = this.appView.exposedJsApi.retrieveJsMessages("1".equals(message)); int bridgeSecret = Integer.parseInt(defaultValue.substring(9));
String r = this.appView.retrieveJsMessages("1".equals(message)); try {
result.confirm(r == null ? "" : r); String r = appView.exposedJsApi.retrieveJsMessages(bridgeSecret, "1".equals(message));
result.confirm(r == null ? "" : r);
} catch (IllegalAccessException e) {
e.printStackTrace();
result.cancel();
}
} }
// Do NO-OP so older code doesn't display dialog else if (defaultValue != null && defaultValue.startsWith("gap_init:")) {
else if (defaultValue != null && defaultValue.equals("gap_init:")) { String startUrl = Config.getStartUrl();
result.confirm("OK"); // Protect against random iframes being able to talk through the bridge.
} // Trust only file URLs and the start URL's domain.
// The extra origin.startsWith("http") is to protect against iframes with data: having "" as origin.
// Show dialog if (origin.startsWith("file:") || (origin.startsWith("http") && startUrl.startsWith(origin))) {
else { // Enable the bridge
int bridgeMode = Integer.parseInt(defaultValue.substring(9));
appView.jsMessageQueue.setBridgeMode(bridgeMode);
// Tell JS the bridge secret.
int secret = appView.exposedJsApi.generateBridgeSecret();
result.confirm(""+secret);
} else {
Log.e(LOG_TAG, "gap_init called from restricted origin: " + origin);
result.cancel();
}
} else {
// Returning false would also show a dialog, but the default one shows the origin (ugly).
final JsPromptResult res = result; final JsPromptResult res = result;
AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity()); AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity());
dlg.setMessage(message); dlg.setMessage(message);
@ -297,7 +306,7 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom
public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize,
long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
{ {
LOG.d(TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota); LOG.d(LOG_TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota);
quotaUpdater.updateQuota(MAX_QUOTA); quotaUpdater.updateQuota(MAX_QUOTA);
} }
@ -310,7 +319,7 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom
//This is only for Android 2.1 //This is only for Android 2.1
if(android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.ECLAIR_MR1) if(android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.ECLAIR_MR1)
{ {
LOG.d(TAG, "%s: Line %d : %s", sourceID, lineNumber, message); LOG.d(LOG_TAG, "%s: Line %d : %s", sourceID, lineNumber, message);
super.onConsoleMessage(message, lineNumber, sourceID); super.onConsoleMessage(message, lineNumber, sourceID);
} }
} }
@ -320,7 +329,7 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom
public boolean onConsoleMessage(ConsoleMessage consoleMessage) public boolean onConsoleMessage(ConsoleMessage consoleMessage)
{ {
if (consoleMessage.message() != null) if (consoleMessage.message() != null)
LOG.d(TAG, "%s: Line %d : %s" , consoleMessage.sourceId() , consoleMessage.lineNumber(), consoleMessage.message()); LOG.d(LOG_TAG, "%s: Line %d : %s" , consoleMessage.sourceId() , consoleMessage.lineNumber(), consoleMessage.message());
return super.onConsoleMessage(consoleMessage); return super.onConsoleMessage(consoleMessage);
} }
@ -342,10 +351,10 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom
this.appView.showCustomView(view, callback); this.appView.showCustomView(view, callback);
} }
@Override @Override
public void onHideCustomView() { public void onHideCustomView() {
this.appView.hideCustomView(); this.appView.hideCustomView();
} }
@Override @Override
/** /**
@ -355,24 +364,24 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom
*/ */
public View getVideoLoadingProgressView() { public View getVideoLoadingProgressView() {
if (mVideoProgressView == null) { if (mVideoProgressView == null) {
// Create a new Loading view programmatically. // Create a new Loading view programmatically.
// create the linear layout // create the linear layout
LinearLayout layout = new LinearLayout(this.appView.getContext()); LinearLayout layout = new LinearLayout(this.appView.getContext());
layout.setOrientation(LinearLayout.VERTICAL); layout.setOrientation(LinearLayout.VERTICAL);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
layout.setLayoutParams(layoutParams); layout.setLayoutParams(layoutParams);
// the proress bar // the proress bar
ProgressBar bar = new ProgressBar(this.appView.getContext()); ProgressBar bar = new ProgressBar(this.appView.getContext());
LinearLayout.LayoutParams barLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); LinearLayout.LayoutParams barLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
barLayoutParams.gravity = Gravity.CENTER; barLayoutParams.gravity = Gravity.CENTER;
bar.setLayoutParams(barLayoutParams); bar.setLayoutParams(barLayoutParams);
layout.addView(bar); layout.addView(bar);
mVideoProgressView = layout; mVideoProgressView = layout;
} }
return mVideoProgressView; return mVideoProgressView;
} }

View File

@ -27,10 +27,11 @@ import org.json.JSONException;
* an equivalent entry in CordovaChromeClient.java, and be added to * an equivalent entry in CordovaChromeClient.java, and be added to
* cordova-js/lib/android/plugin/android/promptbasednativeapi.js * cordova-js/lib/android/plugin/android/promptbasednativeapi.js
*/ */
public /* package */ class AndroidExposedJsApi implements ExposedJsApi { class AndroidExposedJsApi implements ExposedJsApi {
private PluginManager pluginManager; private PluginManager pluginManager;
private NativeToJsMessageQueue jsMessageQueue; private NativeToJsMessageQueue jsMessageQueue;
private volatile int bridgeSecret = -1; // written by UI thread, read by JS thread.
public AndroidExposedJsApi(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) { public AndroidExposedJsApi(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) {
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
@ -38,7 +39,8 @@ public /* package */ class AndroidExposedJsApi implements ExposedJsApi {
} }
@JavascriptInterface @JavascriptInterface
public String exec(String service, String action, String callbackId, String arguments) throws JSONException { public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
verifySecret(bridgeSecret);
// If the arguments weren't received, send a message back to JS. It will switch bridge modes and try again. See CB-2666. // If the arguments weren't received, send a message back to JS. It will switch bridge modes and try again. See CB-2666.
// We send a message meant specifically for this case. It starts with "@" so no other message can be encoded into the same string. // We send a message meant specifically for this case. It starts with "@" so no other message can be encoded into the same string.
if (arguments == null) { if (arguments == null) {
@ -65,12 +67,31 @@ public /* package */ class AndroidExposedJsApi implements ExposedJsApi {
} }
@JavascriptInterface @JavascriptInterface
public void setNativeToJsBridgeMode(int value) { public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException {
verifySecret(bridgeSecret);
jsMessageQueue.setBridgeMode(value); jsMessageQueue.setBridgeMode(value);
} }
@JavascriptInterface @JavascriptInterface
public String retrieveJsMessages(boolean fromOnlineEvent) { public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException {
verifySecret(bridgeSecret);
return jsMessageQueue.popAndEncode(fromOnlineEvent); return jsMessageQueue.popAndEncode(fromOnlineEvent);
} }
private void verifySecret(int value) throws IllegalAccessException {
if (bridgeSecret < 0 || value != bridgeSecret) {
throw new IllegalAccessException();
}
}
/** Called on page transitions */
void clearBridgeSecret() {
bridgeSecret = -1;
}
/** Called by cordova.js to initialize the bridge. */
int generateBridgeSecret() {
bridgeSecret = (int)(Math.random() * Integer.MAX_VALUE);
return bridgeSecret;
}
} }

View File

@ -412,17 +412,7 @@ public class AndroidWebView extends WebView implements CordovaWebView {
this.loadUrlNow(url); this.loadUrlNow(url);
} }
else { else {
this.loadUrlIntoView(url);
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.loadUrlIntoView(url);
}
// Otherwise use the URL specified in the activity's extras bundle
else {
this.loadUrlIntoView(initUrl);
}
} }
} }
@ -433,16 +423,15 @@ public class AndroidWebView extends WebView implements CordovaWebView {
* @param url * @param url
* @param time The number of ms to wait before loading webview * @param time The number of ms to wait before loading webview
*/ */
@Deprecated
public void loadUrl(final String url, int time) { public void loadUrl(final String url, int time) {
String initUrl = this.getProperty("url", null); if(url == null)
{
// If first page of app, then set URL to load to be the one passed in this.loadUrlIntoView(Config.getStartUrl());
if (initUrl == null) {
this.loadUrlIntoView(url, time);
} }
// Otherwise use the URL specified in the activity's extras bundle else
else { {
this.loadUrlIntoView(initUrl); this.loadUrlIntoView(url);
} }
} }
@ -1027,6 +1016,7 @@ public class AndroidWebView extends WebView implements CordovaWebView {
boundKeyCodes.clear(); boundKeyCodes.clear();
pluginManager.onReset(); pluginManager.onReset();
jsMessageQueue.reset(); jsMessageQueue.reset();
exposedJsApi.clearBridgeSecret();
} }
@Override @Override

View File

@ -21,8 +21,6 @@ package org.apache.cordova;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.util.Hashtable; import java.util.Hashtable;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.LOG;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -32,16 +30,15 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri;
import android.net.http.SslError; import android.net.http.SslError;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.webkit.HttpAuthHandler; import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler; import android.webkit.SslErrorHandler;
import android.webkit.WebResourceResponse;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
/** /**
* This class is the WebViewClient that implements callbacks for our web view. * This class is the WebViewClient that implements callbacks for our web view.
* The kind of callbacks that happen here are regarding the rendering of the * The kind of callbacks that happen here are regarding the rendering of the
@ -56,8 +53,8 @@ import android.webkit.WebViewClient;
*/ */
public class AndroidWebViewClient extends WebViewClient implements CordovaWebViewClient{ public class AndroidWebViewClient extends WebViewClient implements CordovaWebViewClient{
private static final String TAG = "AndroidWebViewClient"; private static final String TAG = "AndroidWebViewClient";
private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/"; private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/";
CordovaInterface cordova; CordovaInterface cordova;
AndroidWebView appView; AndroidWebView appView;
CordovaUriHelper helper; CordovaUriHelper helper;
@ -98,30 +95,6 @@ public class AndroidWebViewClient extends WebViewClient implements CordovaWebVie
helper = new CordovaUriHelper(cordova, view); helper = new CordovaUriHelper(cordova, 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);
try {
appView.exec(service, action, callbackId, jsonArgs);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/** /**
* Give the host application a chance to take over the control when a new url * Give the host application a chance to take over the control when a new url
* is about to be loaded in the current WebView. * is about to be loaded in the current WebView.
@ -132,115 +105,7 @@ public class AndroidWebViewClient extends WebViewClient implements CordovaWebVie
*/ */
@Override @Override
public boolean shouldOverrideUrlLoading(WebView view, String url) { public boolean shouldOverrideUrlLoading(WebView view, String url) {
// Check if it's an exec() bridge command message. return helper.shouldOverrideUrlLoading(view, url);
/*
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.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());
}
}
//Android Market
else if(url.startsWith("market:")) {
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 Google Play Store: " + url, e);
}
}
// 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:") || Config.isUrlWhiteListed(url)) {
return false;
}
// 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;
*/
return helper.shouldOverrideUrlLoading(view, url);
} }
/** /**

View File

@ -20,21 +20,16 @@
package org.apache.cordova; package org.apache.cordova;
import java.io.IOException; import java.io.IOException;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.cordova.LOG; import org.apache.cordova.LOG;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import android.app.Activity; import android.app.Activity;
import android.content.res.XmlResourceParser; import android.content.res.XmlResourceParser;
import android.graphics.Color; import android.graphics.Color;
import android.util.Log; import android.util.Log;
public class Config { public class Config {
@ -44,6 +39,8 @@ public class Config {
private Whitelist whitelist = new Whitelist(); private Whitelist whitelist = new Whitelist();
private String startUrl; private String startUrl;
private static String errorUrl;
private static Config self = null; private static Config self = null;
public static void init(Activity action) { public static void init(Activity action) {
@ -156,6 +153,10 @@ public class Config {
boolean value = xml.getAttributeValue(null, "value").equals("true"); boolean value = xml.getAttributeValue(null, "value").equals("true");
action.getIntent().putExtra(name, value); action.getIntent().putExtra(name, value);
} }
else if(name.equalsIgnoreCase("errorurl"))
{
errorUrl = xml.getAttributeValue(null, "value");
}
else else
{ {
String value = xml.getAttributeValue(null, "value"); String value = xml.getAttributeValue(null, "value");
@ -230,4 +231,8 @@ public class Config {
} }
return self.startUrl; return self.startUrl;
} }
public static String getErrorUrl() {
return errorUrl;
}
} }

View File

@ -608,7 +608,7 @@ public class CordovaActivity extends Activity implements CordovaInterface {
//Code to test CB-3064 //Code to test CB-3064
String errorUrl = this.getStringProperty("ErrorUrl", null); String errorUrl = Config.getErrorUrl();
LOG.d(TAG, "CB-3064: The errorUrl is " + errorUrl); LOG.d(TAG, "CB-3064: The errorUrl is " + errorUrl);
if (this.activityState == ACTIVITY_STARTING) { if (this.activityState == ACTIVITY_STARTING) {

View File

@ -22,11 +22,6 @@ import android.net.Uri;
import android.webkit.ValueCallback; import android.webkit.ValueCallback;
public interface CordovaChromeClient { public interface CordovaChromeClient {
int FILECHOOSER_RESULTCODE = 0;
void setWebView(CordovaWebView appView); void setWebView(CordovaWebView appView);
ValueCallback<Uri> getValueCallback(); ValueCallback<Uri> getValueCallback();
} }

View File

@ -19,17 +19,13 @@
package org.apache.cordova; package org.apache.cordova;
import org.json.JSONException;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.util.Log;
import android.webkit.WebView; import android.webkit.WebView;
public class CordovaUriHelper { public class CordovaUriHelper {
private static final String TAG = "CordovaUriHelper"; private static final String TAG = "CordovaUriHelper";
private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/";
private CordovaWebView appView; private CordovaWebView appView;
private CordovaInterface cordova; private CordovaInterface cordova;
@ -40,32 +36,6 @@ public class CordovaUriHelper {
cordova = cdv; cordova = cdv;
} }
// Parses commands sent by setting the webView's URL to:
// cdvbrg:service/action/callbackId#jsonArgs
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);
try {
appView.exec(service, action, callbackId, jsonArgs);
//There is no reason to not send this directly to the pluginManager
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/** /**
* Give the host application a chance to take over the control when a new url * Give the host application a chance to take over the control when a new url
* is about to be loaded in the current WebView. * is about to be loaded in the current WebView.
@ -76,14 +46,10 @@ public class CordovaUriHelper {
*/ */
public boolean shouldOverrideUrlLoading(WebView view, String url) { public boolean shouldOverrideUrlLoading(WebView view, String url) {
// The WebView should support http and https when going on the Internet // The WebView should support http and https when going on the Internet
if(url.startsWith("http")) if(url.startsWith("http:") || url.startsWith("https:"))
{ {
// Check if it's an exec() bridge command message.
if (NativeToJsMessageQueue.ENABLE_LOCATION_CHANGE_EXEC_MODE && url.startsWith(CORDOVA_EXEC_URL_PREFIX)) {
handleExecUrl(url);
}
// We only need to whitelist sites on the Internet! // We only need to whitelist sites on the Internet!
else if(Config.isUrlWhiteListed(url)) if(Config.isUrlWhiteListed(url))
{ {
return false; return false;
} }
@ -94,7 +60,9 @@ public class CordovaUriHelper {
} }
else if(url.startsWith("file://") | url.startsWith("data:")) else if(url.startsWith("file://") | url.startsWith("data:"))
{ {
return false; //This directory on WebKit/Blink based webviews contains SQLite databases!
//DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
return url.contains("app_webview");
} }
else else
{ {
@ -109,5 +77,4 @@ public class CordovaUriHelper {
//Default behaviour should be to load the default intent, let's see what happens! //Default behaviour should be to load the default intent, let's see what happens!
return true; return true;
} }
} }

View File

@ -2,16 +2,11 @@ package org.apache.cordova;
import org.json.JSONException; import org.json.JSONException;
import android.webkit.JavascriptInterface;
/* /*
* Any exposed Javascript API MUST implement these three things! * Any exposed Javascript API MUST implement these three things!
*/ */
public interface ExposedJsApi { public interface ExposedJsApi {
public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException;
@JavascriptInterface public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException;
public String exec(String service, String action, String callbackId, String arguments) throws JSONException; public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException;
public void setNativeToJsBridgeMode(int value);
public String retrieveJsMessages(boolean fromOnlineEvent);
} }

View File

@ -35,6 +35,7 @@ import android.webkit.WebView;
public class IceCreamCordovaWebViewClient extends AndroidWebViewClient implements CordovaWebViewClient{ public class IceCreamCordovaWebViewClient extends AndroidWebViewClient implements CordovaWebViewClient{
private static final String TAG = "IceCreamCordovaWebViewClient"; private static final String TAG = "IceCreamCordovaWebViewClient";
private CordovaUriHelper helper;
public IceCreamCordovaWebViewClient(CordovaInterface cordova) { public IceCreamCordovaWebViewClient(CordovaInterface cordova) {
super(cordova); super(cordova);
@ -47,8 +48,9 @@ public class IceCreamCordovaWebViewClient extends AndroidWebViewClient implement
@Override @Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) { public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
try { try {
// Check the against the white-list. // Check the against the whitelist and lock out access to the WebView directory
if ((url.startsWith("http:") || url.startsWith("https:")) && !Config.isUrlWhiteListed(url)) { // Changing this will cause problems for your application
if (isUrlHarmful(url)) {
LOG.w(TAG, "URL blocked by whitelist: " + url); LOG.w(TAG, "URL blocked by whitelist: " + url);
// Results in a 404. // Results in a 404.
return new WebResourceResponse("text/plain", "UTF-8", null); return new WebResourceResponse("text/plain", "UTF-8", null);
@ -74,6 +76,11 @@ public class IceCreamCordovaWebViewClient extends AndroidWebViewClient implement
} }
} }
private boolean isUrlHarmful(String url) {
return ((url.startsWith("http:") || url.startsWith("https:")) && !Config.isUrlWhiteListed(url))
|| url.contains("app_webview");
}
private static boolean needsKitKatContentUrlFix(Uri uri) { private static boolean needsKitKatContentUrlFix(Uri uri) {
return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && "content".equals(uri.getScheme()); return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && "content".equals(uri.getScheme());
} }

View File

@ -35,31 +35,19 @@ import android.webkit.WebView;
public class NativeToJsMessageQueue { public class NativeToJsMessageQueue {
private static final String LOG_TAG = "JsMessageQueue"; private static final String LOG_TAG = "JsMessageQueue";
// This must match the default value in cordova-js/lib/android/exec.js
private static final int DEFAULT_BRIDGE_MODE = 2;
// Set this to true to force plugin results to be encoding as // Set this to true to force plugin results to be encoding as
// JS instead of the custom format (useful for benchmarking). // JS instead of the custom format (useful for benchmarking).
private static final boolean FORCE_ENCODE_USING_EVAL = false; private static final boolean FORCE_ENCODE_USING_EVAL = false;
// Disable URL-based exec() bridge by default since it's a bit of a
// security concern.
public static final boolean ENABLE_LOCATION_CHANGE_EXEC_MODE = false;
// Disable sending back native->JS messages during an exec() when the active // Disable sending back native->JS messages during an exec() when the active
// exec() is asynchronous. Set this to true when running bridge benchmarks. // exec() is asynchronous. Set this to true when running bridge benchmarks.
public static final boolean DISABLE_EXEC_CHAINING = false; static final boolean DISABLE_EXEC_CHAINING = false;
// Arbitrarily chosen upper limit for how much data to send to JS in one shot. // Arbitrarily chosen upper limit for how much data to send to JS in one shot.
// This currently only chops up on message boundaries. It may be useful // This currently only chops up on message boundaries. It may be useful
// to allow it to break up messages. // to allow it to break up messages.
private static int MAX_PAYLOAD_SIZE = 50 * 1024 * 10240; private static int MAX_PAYLOAD_SIZE = 50 * 1024 * 10240;
/**
* The index into registeredListeners to treat as active.
*/
private int activeListenerIndex;
/** /**
* When true, the active listener is not fired upon enqueue. When set to false, * When true, the active listener is not fired upon enqueue. When set to false,
* the active listener will be fired if the queue is non-empty. * the active listener will be fired if the queue is non-empty.
@ -76,6 +64,13 @@ public class NativeToJsMessageQueue {
*/ */
private final BridgeMode[] registeredListeners; private final BridgeMode[] registeredListeners;
/**
* When null, the bridge is disabled. This occurs during page transitions.
* When disabled, all callbacks are dropped since they are assumed to be
* relevant to the previous page.
*/
private BridgeMode activeBridgeMode;
private final CordovaInterface cordova; private final CordovaInterface cordova;
private final CordovaWebView webView; private final CordovaWebView webView;
@ -94,17 +89,19 @@ public class NativeToJsMessageQueue {
* Changes the bridge mode. * Changes the bridge mode.
*/ */
public void setBridgeMode(int value) { public void setBridgeMode(int value) {
if (value < 0 || value >= registeredListeners.length) { if (value < -1 || value >= registeredListeners.length) {
Log.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value); Log.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value);
} else { } else {
if (value != activeListenerIndex) { BridgeMode newMode = value < 0 ? null : registeredListeners[value];
Log.d(LOG_TAG, "Set native->JS mode to " + value); if (newMode != activeBridgeMode) {
Log.d(LOG_TAG, "Set native->JS mode to " + (newMode == null ? "null" : newMode.getClass().getSimpleName()));
synchronized (this) { synchronized (this) {
activeListenerIndex = value; activeBridgeMode = newMode;
BridgeMode activeListener = registeredListeners[value]; if (newMode != null) {
activeListener.reset(); newMode.reset();
if (!paused && !queue.isEmpty()) { if (!paused && !queue.isEmpty()) {
activeListener.onNativeToJsMessageAvailable(); newMode.onNativeToJsMessageAvailable();
}
} }
} }
} }
@ -117,8 +114,7 @@ public class NativeToJsMessageQueue {
public void reset() { public void reset() {
synchronized (this) { synchronized (this) {
queue.clear(); queue.clear();
setBridgeMode(DEFAULT_BRIDGE_MODE); setBridgeMode(-1);
registeredListeners[activeListenerIndex].reset();
} }
} }
@ -142,7 +138,10 @@ public class NativeToJsMessageQueue {
*/ */
public String popAndEncode(boolean fromOnlineEvent) { public String popAndEncode(boolean fromOnlineEvent) {
synchronized (this) { synchronized (this) {
registeredListeners[activeListenerIndex].notifyOfFlush(fromOnlineEvent); if (activeBridgeMode == null) {
return null;
}
activeBridgeMode.notifyOfFlush(fromOnlineEvent);
if (queue.isEmpty()) { if (queue.isEmpty()) {
return null; return null;
} }
@ -250,9 +249,13 @@ public class NativeToJsMessageQueue {
private void enqueueMessage(JsMessage message) { private void enqueueMessage(JsMessage message) {
synchronized (this) { synchronized (this) {
if (activeBridgeMode == null) {
Log.d(LOG_TAG, "Dropping Native->JS message due to disabled bridge");
return;
}
queue.add(message); queue.add(message);
if (!paused) { if (!paused) {
registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable(); activeBridgeMode.onNativeToJsMessageAvailable();
} }
} }
} }
@ -266,17 +269,13 @@ public class NativeToJsMessageQueue {
paused = value; paused = value;
if (!value) { if (!value) {
synchronized (this) { synchronized (this) {
if (!queue.isEmpty()) { if (!queue.isEmpty() && activeBridgeMode != null) {
registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable(); activeBridgeMode.onNativeToJsMessageAvailable();
} }
} }
} }
} }
public boolean getPaused() {
return paused;
}
private abstract class BridgeMode { private abstract class BridgeMode {
abstract void onNativeToJsMessageAvailable(); abstract void onNativeToJsMessageAvailable();
void notifyOfFlush(boolean fromOnlineEvent) {} void notifyOfFlush(boolean fromOnlineEvent) {}
@ -308,23 +307,28 @@ public class NativeToJsMessageQueue {
/** Uses online/offline events to tell the JS when to poll for messages. */ /** Uses online/offline events to tell the JS when to poll for messages. */
private class OnlineEventsBridgeMode extends BridgeMode { private class OnlineEventsBridgeMode extends BridgeMode {
private boolean online; private boolean online;
final Runnable runnable = new Runnable() { private boolean ignoreNextFlush;
final Runnable toggleNetworkRunnable = new Runnable() {
public void run() { public void run() {
if (!queue.isEmpty()) { if (!queue.isEmpty()) {
ignoreNextFlush = false;
webView.setNetworkAvailable(online); webView.setNetworkAvailable(online);
} }
} }
}; };
@Override void reset() { @Override void reset() {
online = false; online = false;
// If the following call triggers a notifyOfFlush, then ignore it.
ignoreNextFlush = true;
webView.setNetworkAvailable(true); webView.setNetworkAvailable(true);
} }
@Override void onNativeToJsMessageAvailable() { @Override void onNativeToJsMessageAvailable() {
cordova.getActivity().runOnUiThread(runnable); cordova.getActivity().runOnUiThread(toggleNetworkRunnable);
} }
// Track when online/offline events are fired so that we don't fire excess events. // Track when online/offline events are fired so that we don't fire excess events.
@Override void notifyOfFlush(boolean fromOnlineEvent) { @Override void notifyOfFlush(boolean fromOnlineEvent) {
if (fromOnlineEvent) { if (fromOnlineEvent && !ignoreNextFlush) {
online = !online; online = !online;
} }
} }

View File

@ -23,10 +23,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaWebView; import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CallbackContext; import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaInterface;
@ -65,8 +62,6 @@ public class PluginManager {
// Using <url-filter> is deprecated. // Using <url-filter> is deprecated.
protected HashMap<String, List<String>> urlMap = new HashMap<String, List<String>>(); protected HashMap<String, List<String>> urlMap = new HashMap<String, List<String>>();
private AtomicInteger numPendingUiExecs;
private Set<String> pluginIdWhitelist; private Set<String> pluginIdWhitelist;
/** /**
@ -79,7 +74,6 @@ public class PluginManager {
this.ctx = ctx; this.ctx = ctx;
this.app = app; this.app = app;
this.firstRun = true; this.firstRun = true;
this.numPendingUiExecs = new AtomicInteger(0);
} }
public void setPluginIdWhitelist(Set<String> pluginIdWhitelist) { public void setPluginIdWhitelist(Set<String> pluginIdWhitelist) {
@ -105,9 +99,6 @@ public class PluginManager {
this.clearPluginObjects(); this.clearPluginObjects();
} }
// Insert PluginManager service
this.addService(new PluginEntry("PluginManager", new PluginManagerService()));
// Start up all plugins that have onload specified // Start up all plugins that have onload specified
this.startupPlugins(); this.startupPlugins();
} }
@ -224,20 +215,6 @@ public class PluginManager {
* plugin execute method. * plugin execute method.
*/ */
public void exec(final String service, final String action, final String callbackId, final String rawArgs) { public void exec(final String service, final String action, final String callbackId, final String rawArgs) {
if (numPendingUiExecs.get() > 0) {
numPendingUiExecs.getAndIncrement();
this.ctx.getActivity().runOnUiThread(new Runnable() {
public void run() {
execHelper(service, action, callbackId, rawArgs);
numPendingUiExecs.getAndDecrement();
}
});
} else {
execHelper(service, action, callbackId, rawArgs);
}
}
private void execHelper(final String service, final String action, final String callbackId, final String rawArgs) {
CordovaPlugin plugin = getPlugin(service); CordovaPlugin plugin = getPlugin(service);
if (plugin == null) { if (plugin == null) {
Log.d(TAG, "exec() call to unknown plugin: " + service); Log.d(TAG, "exec() call to unknown plugin: " + service);
@ -449,26 +426,4 @@ public class PluginManager {
} }
return null; return null;
} }
private class PluginManagerService extends CordovaPlugin {
@Override
public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
if ("startup".equals(action)) {
// The onPageStarted event of CordovaWebViewClient resets the queue of messages to be returned to javascript in response
// to exec calls. Since this event occurs on the UI thread and exec calls happen on the WebCore thread it is possible
// that onPageStarted occurs after exec calls have started happening on a new page, which can cause the message queue
// to be reset between the queuing of a new message and its retrieval by javascript. To avoid this from happening,
// javascript always sends a "startup" exec upon loading a new page which causes all future exec calls to happen on the UI
// thread (and hence after onPageStarted) until there are no more pending exec calls remaining.
numPendingUiExecs.getAndIncrement();
ctx.getActivity().runOnUiThread(new Runnable() {
public void run() {
numPendingUiExecs.getAndDecrement();
}
});
return true;
}
return false;
}
}
} }