From cc860804f6d55ba0758b2b89e8f3f5d60fee0923 Mon Sep 17 00:00:00 2001 From: Andrew Grieve Date: Thu, 10 Jul 2014 10:43:37 -0400 Subject: [PATCH] Backport CordovaBridge from 4.0.x -> master --- .../org/apache/cordova/CordovaActivity.java | 2 +- .../src/org/apache/cordova/CordovaBridge.java | 185 ++++++++++++++++++ .../apache/cordova/CordovaChromeClient.java | 68 +------ .../org/apache/cordova/CordovaWebView.java | 12 +- .../apache/cordova/CordovaWebViewClient.java | 3 +- .../src/org/apache/cordova/ExposedJsApi.java | 58 +----- .../cordova/NativeToJsMessageQueue.java | 6 +- 7 files changed, 206 insertions(+), 128 deletions(-) create mode 100644 framework/src/org/apache/cordova/CordovaBridge.java diff --git a/framework/src/org/apache/cordova/CordovaActivity.java b/framework/src/org/apache/cordova/CordovaActivity.java index 371172c9..d69dd591 100755 --- a/framework/src/org/apache/cordova/CordovaActivity.java +++ b/framework/src/org/apache/cordova/CordovaActivity.java @@ -669,7 +669,7 @@ public class CordovaActivity extends Activity implements CordovaInterface { @Deprecated // Call method on appView directly. public void sendJavascript(String statement) { if (this.appView != null) { - this.appView.jsMessageQueue.addJavaScript(statement); + this.appView.bridge.getMessageQueue().addJavaScript(statement); } } diff --git a/framework/src/org/apache/cordova/CordovaBridge.java b/framework/src/org/apache/cordova/CordovaBridge.java new file mode 100644 index 00000000..2bfad116 --- /dev/null +++ b/framework/src/org/apache/cordova/CordovaBridge.java @@ -0,0 +1,185 @@ +/* + 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.PluginManager; +import org.json.JSONArray; +import org.json.JSONException; + +import android.util.Log; + +/** + * Contains APIs that the JS can call. All functions in here should also have + * an equivalent entry in CordovaChromeClient.java, and be added to + * cordova-js/lib/android/plugin/android/promptbasednativeapi.js + */ +public class CordovaBridge { + private static final String LOG_TAG = "CordovaBridge"; + private PluginManager pluginManager; + private NativeToJsMessageQueue jsMessageQueue; + private volatile int bridgeSecret = -1; // written by UI thread, read by JS thread. + private String loadedUrl; + + public CordovaBridge(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) { + this.pluginManager = pluginManager; + this.jsMessageQueue = jsMessageQueue; + } + + private final boolean checkBridgeEnabled(String action) { + if (!jsMessageQueue.isBridgeEnabled()) { + if (bridgeSecret == -1) { + Log.d(LOG_TAG, action + " call made before bridge was enabled."); + } else { + Log.d(LOG_TAG, "Ignoring " + action + " from previous page load."); + } + return false; + } + return true; + } + + public String jsExec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException { + if (!checkBridgeEnabled("exec()")) { + return ""; + } + 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. + // 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) { + return "@Null arguments."; + } + + jsMessageQueue.setPaused(true); + try { + // Tell the resourceApi what thread the JS is running on. + CordovaResourceApi.jsThread = Thread.currentThread(); + + pluginManager.exec(service, action, callbackId, arguments); + String ret = ""; + if (!NativeToJsMessageQueue.DISABLE_EXEC_CHAINING) { + ret = jsMessageQueue.popAndEncode(false); + } + return ret; + } catch (Throwable e) { + e.printStackTrace(); + return ""; + } finally { + jsMessageQueue.setPaused(false); + } + } + + public void jsSetNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException { + verifySecret(bridgeSecret); + jsMessageQueue.setBridgeMode(value); + } + + public String jsRetrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException { + if (!checkBridgeEnabled("retrieveJsMessages()")) { + return ""; + } + verifySecret(bridgeSecret); + 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; + } + + public void reset(String loadedUrl) { + jsMessageQueue.reset(); + clearBridgeSecret(); + this.loadedUrl = loadedUrl; + } + + public String promptOnJsPrompt(String origin, String message, String defaultValue) { + if (defaultValue != null && defaultValue.length() > 3 && defaultValue.startsWith("gap:")) { + JSONArray array; + try { + array = new JSONArray(defaultValue.substring(4)); + int bridgeSecret = array.getInt(0); + String service = array.getString(1); + String action = array.getString(2); + String callbackId = array.getString(3); + String r = jsExec(bridgeSecret, service, action, callbackId, message); + return r == null ? "" : r; + } catch (JSONException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return ""; + } + // Sets the native->JS bridge mode. + else if (defaultValue != null && defaultValue.startsWith("gap_bridge_mode:")) { + try { + int bridgeSecret = Integer.parseInt(defaultValue.substring(16)); + jsSetNativeToJsBridgeMode(bridgeSecret, Integer.parseInt(message)); + } catch (NumberFormatException e){ + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return ""; + } + // Polling for JavaScript messages + else if (defaultValue != null && defaultValue.startsWith("gap_poll:")) { + int bridgeSecret = Integer.parseInt(defaultValue.substring(9)); + try { + String r = jsRetrieveJsMessages(bridgeSecret, "1".equals(message)); + return r == null ? "" : r; + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return ""; + } + else if (defaultValue != null && defaultValue.startsWith("gap_init:")) { + // 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. + if (origin.startsWith("file:") || (origin.startsWith("http") && loadedUrl.startsWith(origin))) { + // Enable the bridge + int bridgeMode = Integer.parseInt(defaultValue.substring(9)); + jsMessageQueue.setBridgeMode(bridgeMode); + // Tell JS the bridge secret. + int secret = generateBridgeSecret(); + return ""+secret; + } else { + Log.e(LOG_TAG, "gap_init called from restricted origin: " + origin); + } + return ""; + } + return null; + } + + public NativeToJsMessageQueue getMessageQueue() { + return jsMessageQueue; + } +} diff --git a/framework/src/org/apache/cordova/CordovaChromeClient.java b/framework/src/org/apache/cordova/CordovaChromeClient.java index cebabba2..737d0b84 100755 --- a/framework/src/org/apache/cordova/CordovaChromeClient.java +++ b/framework/src/org/apache/cordova/CordovaChromeClient.java @@ -20,15 +20,12 @@ package org.apache.cordova; import org.apache.cordova.CordovaInterface; import org.apache.cordova.LOG; -import org.json.JSONArray; -import org.json.JSONException; import android.annotation.TargetApi; 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; @@ -60,7 +57,6 @@ import android.widget.RelativeLayout; 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; protected CordovaInterface cordova; @@ -193,67 +189,9 @@ public class CordovaChromeClient extends WebChromeClient { @Override 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. - if (defaultValue != null && defaultValue.length() > 3 && defaultValue.startsWith("gap:")) { - JSONArray array; - try { - array = new JSONArray(defaultValue.substring(4)); - int bridgeSecret = array.getInt(0); - String service = array.getString(1); - String action = array.getString(2); - String callbackId = array.getString(3); - String r = appView.exposedJsApi.exec(bridgeSecret, service, action, callbackId, message); - result.confirm(r == null ? "" : r); - } catch (JSONException e) { - e.printStackTrace(); - result.cancel(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - result.cancel(); - } - } - - // Sets the native->JS bridge mode. - else if (defaultValue != null && defaultValue.startsWith("gap_bridge_mode:")) { - try { - int bridgeSecret = Integer.parseInt(defaultValue.substring(16)); - appView.exposedJsApi.setNativeToJsBridgeMode(bridgeSecret, Integer.parseInt(message)); - result.cancel(); - } catch (NumberFormatException e){ - e.printStackTrace(); - result.cancel(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - result.cancel(); - } - } - - // Polling for JavaScript messages - else if (defaultValue != null && defaultValue.startsWith("gap_poll:")) { - int bridgeSecret = Integer.parseInt(defaultValue.substring(9)); - try { - String r = appView.exposedJsApi.retrieveJsMessages(bridgeSecret, "1".equals(message)); - result.confirm(r == null ? "" : r); - } catch (IllegalAccessException e) { - e.printStackTrace(); - result.cancel(); - } - } - - else if (defaultValue != null && defaultValue.startsWith("gap_init:")) { - // 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. - if (origin.startsWith("file:") || (origin.startsWith("http") && appView.loadedUrl.startsWith(origin))) { - // 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(); - } + String handledRet = appView.bridge.promptOnJsPrompt(origin, message, defaultValue); + if (handledRet != null) { + result.confirm(handledRet); } else { // Returning false would also show a dialog, but the default one shows the origin (ugly). final JsPromptResult res = result; diff --git a/framework/src/org/apache/cordova/CordovaWebView.java b/framework/src/org/apache/cordova/CordovaWebView.java index 7ed7fff8..f74d5e19 100755 --- a/framework/src/org/apache/cordova/CordovaWebView.java +++ b/framework/src/org/apache/cordova/CordovaWebView.java @@ -81,8 +81,7 @@ public class CordovaWebView extends WebView { private long lastMenuEventTime = 0; - NativeToJsMessageQueue jsMessageQueue; - ExposedJsApi exposedJsApi; + CordovaBridge bridge; /** custom view created by the browser (a video player for example) */ private View mCustomView; @@ -149,8 +148,7 @@ public class CordovaWebView extends WebView { super.setWebViewClient(webViewClient); pluginManager = new PluginManager(this, this.cordova, pluginEntries); - jsMessageQueue = new NativeToJsMessageQueue(this, cordova); - exposedJsApi = new ExposedJsApi(pluginManager, jsMessageQueue); + bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova)); resourceApi = new CordovaResourceApi(this.getContext(), pluginManager); pluginManager.addService("App", "org.apache.cordova.App"); @@ -300,7 +298,7 @@ public class CordovaWebView extends WebView { // use the prompt bridge instead. return; } - this.addJavascriptInterface(exposedJsApi, "_cordovaNative"); + this.addJavascriptInterface(new ExposedJsApi(bridge), "_cordovaNative"); } @Override @@ -497,7 +495,7 @@ public class CordovaWebView extends WebView { */ @Deprecated public void sendJavascript(String statement) { - this.jsMessageQueue.addJavaScript(statement); + this.bridge.getMessageQueue().addJavaScript(statement); } /** @@ -508,7 +506,7 @@ public class CordovaWebView extends WebView { * @param callbackId */ public void sendPluginResult(PluginResult result, String callbackId) { - this.jsMessageQueue.addPluginResult(result, callbackId); + this.bridge.getMessageQueue().addPluginResult(result, callbackId); } /** diff --git a/framework/src/org/apache/cordova/CordovaWebViewClient.java b/framework/src/org/apache/cordova/CordovaWebViewClient.java index 35dbea17..a2cf0bed 100755 --- a/framework/src/org/apache/cordova/CordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/CordovaWebViewClient.java @@ -141,8 +141,7 @@ public class CordovaWebViewClient extends WebViewClient { isCurrentlyLoading = true; LOG.d(TAG, "onPageStarted(" + url + ")"); // Flush stale messages. - this.appView.jsMessageQueue.reset(); - this.appView.exposedJsApi.clearBridgeSecret(); + this.appView.bridge.reset(url); // Broadcast message that page has loaded this.appView.postMessage("onPageStarted", url); diff --git a/framework/src/org/apache/cordova/ExposedJsApi.java b/framework/src/org/apache/cordova/ExposedJsApi.java index 97f6038c..ffe4f2d8 100755 --- a/framework/src/org/apache/cordova/ExposedJsApi.java +++ b/framework/src/org/apache/cordova/ExposedJsApi.java @@ -20,7 +20,6 @@ package org.apache.cordova; import android.webkit.JavascriptInterface; -import org.apache.cordova.PluginManager; import org.json.JSONException; /** @@ -30,69 +29,24 @@ import org.json.JSONException; */ /* package */ class ExposedJsApi { - private PluginManager pluginManager; - private NativeToJsMessageQueue jsMessageQueue; - private volatile int bridgeSecret = -1; // written by UI thread, read by JS thread. + private CordovaBridge bridge; - public ExposedJsApi(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) { - this.pluginManager = pluginManager; - this.jsMessageQueue = jsMessageQueue; + public ExposedJsApi(CordovaBridge bridge) { + this.bridge = bridge; } @JavascriptInterface 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. - // 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) { - return "@Null arguments."; - } - - jsMessageQueue.setPaused(true); - try { - // Tell the resourceApi what thread the JS is running on. - CordovaResourceApi.jsThread = Thread.currentThread(); - - pluginManager.exec(service, action, callbackId, arguments); - String ret = ""; - if (!NativeToJsMessageQueue.DISABLE_EXEC_CHAINING) { - ret = jsMessageQueue.popAndEncode(false); - } - return ret; - } catch (Throwable e) { - e.printStackTrace(); - return ""; - } finally { - jsMessageQueue.setPaused(false); - } + return bridge.jsExec(bridgeSecret, service, action, callbackId, arguments); } @JavascriptInterface public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException { - verifySecret(bridgeSecret); - jsMessageQueue.setBridgeMode(value); + bridge.jsSetNativeToJsBridgeMode(bridgeSecret, value); } @JavascriptInterface public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException { - verifySecret(bridgeSecret); - 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; + return bridge.jsRetrieveJsMessages(bridgeSecret, fromOnlineEvent); } } diff --git a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java index b822800e..d05eed82 100755 --- a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java +++ b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java @@ -84,7 +84,11 @@ public class NativeToJsMessageQueue { registeredListeners[3] = new PrivateApiBridgeMode(); reset(); } - + + public boolean isBridgeEnabled() { + return activeBridgeMode != null; + } + /** * Changes the bridge mode. */