Backport CordovaBridge from 4.0.x -> master

This commit is contained in:
Andrew Grieve 2014-07-10 10:43:37 -04:00
parent d8a19b5565
commit cc860804f6
7 changed files with 206 additions and 128 deletions

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
}
/**

View File

@ -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);

View File

@ -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);
}
}

View File

@ -84,7 +84,11 @@ public class NativeToJsMessageQueue {
registeredListeners[3] = new PrivateApiBridgeMode();
reset();
}
public boolean isBridgeEnabled() {
return activeBridgeMode != null;
}
/**
* Changes the bridge mode.
*/