Separate the registering of BridgeModes from NativeToJsMessageQueue

This makes the class usable no matter how a webview's bridge is
implemented under-the-hood.
This also deletes the PrivateApi bridge mode, which has never been a
good idea to use, and which we should replace with a Lollipop
"evaluateJavascript"-based bridge.
This commit is contained in:
Andrew Grieve 2015-02-06 14:03:10 -05:00
parent 5b2fa128a4
commit 4cb64580fd
2 changed files with 90 additions and 127 deletions

View File

@ -113,7 +113,7 @@ public class AndroidWebView extends WebView implements CordovaWebView {
// Use two-phase init so that the control will work with XML layouts.
@Override
public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries,
public void init(final CordovaInterface cordova, List<PluginEntry> pluginEntries,
Whitelist internalWhitelist, Whitelist externalWhitelist,
CordovaPreferences preferences) {
if (this.cordova != null) {
@ -127,9 +127,23 @@ public class AndroidWebView extends WebView implements CordovaWebView {
pluginManager = new PluginManager(this, this.cordova, pluginEntries);
cookieManager = new AndroidCookieManager(this);
resourceApi = new CordovaResourceApi(this.getContext(), pluginManager);
bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova), this.cordova.getActivity().getPackageName());
NativeToJsMessageQueue nativeToJsMessageQueue = new NativeToJsMessageQueue();
nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.NoOpBridgeMode());
nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.LoadUrlBridgeMode(this, cordova));
nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode.OnlineEventsBridgeModeDelegate() {
@Override
public void setNetworkAvailable(boolean value) {
AndroidWebView.this.setNetworkAvailable(value);
}
@Override
public void runOnUiThread(Runnable r) {
cordova.getActivity().runOnUiThread(r);
}
}));
bridge = new CordovaBridge(pluginManager, nativeToJsMessageQueue, this.cordova.getActivity().getPackageName());
initWebViewSettings();
pluginManager.addService(CoreAndroid.PLUGIN_NAME, "org.apache.cordova.CoreAndroid");
pluginManager.addService(CoreAndroid.PLUGIN_NAME, CoreAndroid.class.getCanonicalName());
pluginManager.init();
if (this.viewClient == null) {

View File

@ -18,16 +18,10 @@
*/
package org.apache.cordova;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedList;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.PluginResult;
import android.os.Message;
import android.util.Log;
import android.webkit.WebView;
/**
* Holds the list of messages to be sent to the WebView.
@ -63,7 +57,7 @@ public class NativeToJsMessageQueue {
/**
* The array of listeners that can be used to send messages to JS.
*/
private final BridgeMode[] registeredListeners;
private ArrayList<BridgeMode> bridgeModes = new ArrayList<BridgeMode>();
/**
* When null, the bridge is disabled. This occurs during page transitions.
@ -72,32 +66,26 @@ public class NativeToJsMessageQueue {
*/
private BridgeMode activeBridgeMode;
private final CordovaInterface cordova;
private final CordovaWebView webView;
public NativeToJsMessageQueue(CordovaWebView webView, CordovaInterface cordova) {
this.cordova = cordova;
this.webView = webView;
registeredListeners = new BridgeMode[4];
registeredListeners[0] = new PollingBridgeMode();
registeredListeners[1] = new LoadUrlBridgeMode();
registeredListeners[2] = new OnlineEventsBridgeMode();
registeredListeners[3] = new PrivateApiBridgeMode();
reset();
public void addBridgeMode(BridgeMode bridgeMode) {
bridgeModes.add(bridgeMode);
}
public boolean isBridgeEnabled() {
return activeBridgeMode != null;
}
public boolean isEmpty() {
return queue.isEmpty();
}
/**
* Changes the bridge mode.
*/
public void setBridgeMode(int value) {
if (value < -1 || value >= registeredListeners.length) {
if (value < -1 || value >= bridgeModes.size()) {
Log.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value);
} else {
BridgeMode newMode = value < 0 ? null : registeredListeners[value];
BridgeMode newMode = value < 0 ? null : bridgeModes.get(value);
if (newMode != activeBridgeMode) {
Log.d(LOG_TAG, "Set native->JS mode to " + (newMode == null ? "null" : newMode.getClass().getSimpleName()));
synchronized (this) {
@ -105,7 +93,7 @@ public class NativeToJsMessageQueue {
if (newMode != null) {
newMode.reset();
if (!paused && !queue.isEmpty()) {
newMode.onNativeToJsMessageAvailable();
newMode.onNativeToJsMessageAvailable(this);
}
}
}
@ -146,7 +134,7 @@ public class NativeToJsMessageQueue {
if (activeBridgeMode == null) {
return null;
}
activeBridgeMode.notifyOfFlush(fromOnlineEvent);
activeBridgeMode.notifyOfFlush(this, fromOnlineEvent);
if (queue.isEmpty()) {
return null;
}
@ -179,7 +167,7 @@ public class NativeToJsMessageQueue {
/**
* Same as popAndEncode(), except encodes in a form that can be executed as JS.
*/
private String popAndEncodeAsJs() {
public String popAndEncodeAsJs() {
synchronized (this) {
int length = queue.size();
if (length == 0) {
@ -260,7 +248,7 @@ public class NativeToJsMessageQueue {
}
queue.add(message);
if (!paused) {
activeBridgeMode.onNativeToJsMessageAvailable();
activeBridgeMode.onNativeToJsMessageAvailable(this);
}
}
}
@ -275,133 +263,94 @@ public class NativeToJsMessageQueue {
if (!value) {
synchronized (this) {
if (!queue.isEmpty() && activeBridgeMode != null) {
activeBridgeMode.onNativeToJsMessageAvailable();
activeBridgeMode.onNativeToJsMessageAvailable(this);
}
}
}
}
private abstract class BridgeMode {
abstract void onNativeToJsMessageAvailable();
void notifyOfFlush(boolean fromOnlineEvent) {}
public static abstract class BridgeMode {
abstract void onNativeToJsMessageAvailable(NativeToJsMessageQueue queue);
void notifyOfFlush(NativeToJsMessageQueue queue, boolean fromOnlineEvent) {}
void reset() {}
}
/** Uses JS polls for messages on a timer.. */
private class PollingBridgeMode extends BridgeMode {
@Override void onNativeToJsMessageAvailable() {
public static class NoOpBridgeMode extends BridgeMode {
@Override public void onNativeToJsMessageAvailable(NativeToJsMessageQueue queue) {
}
}
/** Uses webView.loadUrl("javascript:") to execute messages. */
private class LoadUrlBridgeMode extends BridgeMode {
final Runnable runnable = new Runnable() {
public void run() {
String js = popAndEncodeAsJs();
if (js != null) {
webView.loadUrlIntoView("javascript:" + js, false);
}
}
};
public static class LoadUrlBridgeMode extends BridgeMode {
private final CordovaWebView webView;
private final CordovaInterface cordova;
@Override void onNativeToJsMessageAvailable() {
cordova.getActivity().runOnUiThread(runnable);
public LoadUrlBridgeMode(CordovaWebView webView, CordovaInterface cordova) {
this.webView = webView;
this.cordova = cordova;
}
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
String js = queue.popAndEncodeAsJs();
if (js != null) {
webView.loadUrl("javascript:" + js);
}
}
});
}
}
/** Uses online/offline events to tell the JS when to poll for messages. */
private class OnlineEventsBridgeMode extends BridgeMode {
public static class OnlineEventsBridgeMode extends BridgeMode {
private final OnlineEventsBridgeModeDelegate delegate;
private boolean online;
private boolean ignoreNextFlush;
final Runnable toggleNetworkRunnable = new Runnable() {
public void run() {
if (!queue.isEmpty()) {
ignoreNextFlush = false;
webView.setNetworkAvailable(online);
public interface OnlineEventsBridgeModeDelegate {
void setNetworkAvailable(boolean value);
void runOnUiThread(Runnable r);
}
public OnlineEventsBridgeMode(OnlineEventsBridgeModeDelegate delegate) {
this.delegate = delegate;
}
};
final Runnable resetNetworkRunnable = new Runnable() {
@Override
public void reset() {
delegate.runOnUiThread(new Runnable() {
public void run() {
online = false;
// If the following call triggers a notifyOfFlush, then ignore it.
ignoreNextFlush = true;
webView.setNetworkAvailable(true);
delegate.setNetworkAvailable(true);
}
};
@Override void reset() {
cordova.getActivity().runOnUiThread(resetNetworkRunnable);
});
}
@Override void onNativeToJsMessageAvailable() {
cordova.getActivity().runOnUiThread(toggleNetworkRunnable);
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
delegate.runOnUiThread(new Runnable() {
public void run() {
if (!queue.isEmpty()) {
ignoreNextFlush = false;
delegate.setNetworkAvailable(online);
}
}
});
}
// Track when online/offline events are fired so that we don't fire excess events.
@Override void notifyOfFlush(boolean fromOnlineEvent) {
@Override
public void notifyOfFlush(final NativeToJsMessageQueue queue, boolean fromOnlineEvent) {
if (fromOnlineEvent && !ignoreNextFlush) {
online = !online;
}
}
}
/**
* Uses Java reflection to access an API that lets us eval JS.
* Requires Android 3.2.4 or above.
*/
private class PrivateApiBridgeMode extends BridgeMode {
// Message added in commit:
// http://omapzoom.org/?p=platform/frameworks/base.git;a=commitdiff;h=9497c5f8c4bc7c47789e5ccde01179abc31ffeb2
// Which first appeared in 3.2.4ish.
private static final int EXECUTE_JS = 194;
Method sendMessageMethod;
Object webViewCore;
boolean initFailed;
@SuppressWarnings("rawtypes")
private void initReflection() {
Object webViewObject = webView;
Class webViewClass = WebView.class;
try {
Field f = webViewClass.getDeclaredField("mProvider");
f.setAccessible(true);
webViewObject = f.get(webView);
webViewClass = webViewObject.getClass();
} catch (Throwable e) {
// mProvider is only required on newer Android releases.
}
try {
Field f = webViewClass.getDeclaredField("mWebViewCore");
f.setAccessible(true);
webViewCore = f.get(webViewObject);
if (webViewCore != null) {
sendMessageMethod = webViewCore.getClass().getDeclaredMethod("sendMessage", Message.class);
sendMessageMethod.setAccessible(true);
}
} catch (Throwable e) {
initFailed = true;
Log.e(LOG_TAG, "PrivateApiBridgeMode failed to find the expected APIs.", e);
}
}
@Override void onNativeToJsMessageAvailable() {
if (sendMessageMethod == null && !initFailed) {
initReflection();
}
// webViewCore is lazily initialized, and so may not be available right away.
if (sendMessageMethod != null) {
String js = popAndEncodeAsJs();
Message execJsMessage = Message.obtain(null, EXECUTE_JS, js);
try {
sendMessageMethod.invoke(webViewCore, execJsMessage);
} catch (Throwable e) {
Log.e(LOG_TAG, "Reflection message bridge failed.", e);
}
}
}
}
private static class JsMessage {
final String jsPayloadOrCallbackId;
final PluginResult pluginResult;