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. // Use two-phase init so that the control will work with XML layouts.
@Override @Override
public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, public void init(final CordovaInterface cordova, List<PluginEntry> pluginEntries,
Whitelist internalWhitelist, Whitelist externalWhitelist, Whitelist internalWhitelist, Whitelist externalWhitelist,
CordovaPreferences preferences) { CordovaPreferences preferences) {
if (this.cordova != null) { if (this.cordova != null) {
@ -127,9 +127,23 @@ public class AndroidWebView extends WebView implements CordovaWebView {
pluginManager = new PluginManager(this, this.cordova, pluginEntries); pluginManager = new PluginManager(this, this.cordova, pluginEntries);
cookieManager = new AndroidCookieManager(this); cookieManager = new AndroidCookieManager(this);
resourceApi = new CordovaResourceApi(this.getContext(), pluginManager); 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(); initWebViewSettings();
pluginManager.addService(CoreAndroid.PLUGIN_NAME, "org.apache.cordova.CoreAndroid"); pluginManager.addService(CoreAndroid.PLUGIN_NAME, CoreAndroid.class.getCanonicalName());
pluginManager.init(); pluginManager.init();
if (this.viewClient == null) { if (this.viewClient == null) {

View File

@ -18,16 +18,10 @@
*/ */
package org.apache.cordova; package org.apache.cordova;
import java.lang.reflect.Field; import java.util.ArrayList;
import java.lang.reflect.Method;
import java.util.LinkedList; import java.util.LinkedList;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.PluginResult;
import android.os.Message;
import android.util.Log; import android.util.Log;
import android.webkit.WebView;
/** /**
* Holds the list of messages to be sent to the 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. * 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. * When null, the bridge is disabled. This occurs during page transitions.
@ -72,32 +66,26 @@ public class NativeToJsMessageQueue {
*/ */
private BridgeMode activeBridgeMode; private BridgeMode activeBridgeMode;
private final CordovaInterface cordova; public void addBridgeMode(BridgeMode bridgeMode) {
private final CordovaWebView webView; bridgeModes.add(bridgeMode);
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 boolean isBridgeEnabled() { public boolean isBridgeEnabled() {
return activeBridgeMode != null; return activeBridgeMode != null;
} }
public boolean isEmpty() {
return queue.isEmpty();
}
/** /**
* Changes the bridge mode. * Changes the bridge mode.
*/ */
public void setBridgeMode(int value) { public void setBridgeMode(int value) {
if (value < -1 || value >= registeredListeners.length) { if (value < -1 || value >= bridgeModes.size()) {
Log.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value); Log.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value);
} else { } else {
BridgeMode newMode = value < 0 ? null : registeredListeners[value]; BridgeMode newMode = value < 0 ? null : bridgeModes.get(value);
if (newMode != activeBridgeMode) { if (newMode != activeBridgeMode) {
Log.d(LOG_TAG, "Set native->JS mode to " + (newMode == null ? "null" : newMode.getClass().getSimpleName())); Log.d(LOG_TAG, "Set native->JS mode to " + (newMode == null ? "null" : newMode.getClass().getSimpleName()));
synchronized (this) { synchronized (this) {
@ -105,7 +93,7 @@ public class NativeToJsMessageQueue {
if (newMode != null) { if (newMode != null) {
newMode.reset(); newMode.reset();
if (!paused && !queue.isEmpty()) { if (!paused && !queue.isEmpty()) {
newMode.onNativeToJsMessageAvailable(); newMode.onNativeToJsMessageAvailable(this);
} }
} }
} }
@ -146,7 +134,7 @@ public class NativeToJsMessageQueue {
if (activeBridgeMode == null) { if (activeBridgeMode == null) {
return null; return null;
} }
activeBridgeMode.notifyOfFlush(fromOnlineEvent); activeBridgeMode.notifyOfFlush(this, fromOnlineEvent);
if (queue.isEmpty()) { if (queue.isEmpty()) {
return null; return null;
} }
@ -179,7 +167,7 @@ public class NativeToJsMessageQueue {
/** /**
* Same as popAndEncode(), except encodes in a form that can be executed as JS. * Same as popAndEncode(), except encodes in a form that can be executed as JS.
*/ */
private String popAndEncodeAsJs() { public String popAndEncodeAsJs() {
synchronized (this) { synchronized (this) {
int length = queue.size(); int length = queue.size();
if (length == 0) { if (length == 0) {
@ -260,7 +248,7 @@ public class NativeToJsMessageQueue {
} }
queue.add(message); queue.add(message);
if (!paused) { if (!paused) {
activeBridgeMode.onNativeToJsMessageAvailable(); activeBridgeMode.onNativeToJsMessageAvailable(this);
} }
} }
} }
@ -275,133 +263,94 @@ public class NativeToJsMessageQueue {
if (!value) { if (!value) {
synchronized (this) { synchronized (this) {
if (!queue.isEmpty() && activeBridgeMode != null) { if (!queue.isEmpty() && activeBridgeMode != null) {
activeBridgeMode.onNativeToJsMessageAvailable(); activeBridgeMode.onNativeToJsMessageAvailable(this);
} }
} }
} }
} }
private abstract class BridgeMode { public static abstract class BridgeMode {
abstract void onNativeToJsMessageAvailable(); abstract void onNativeToJsMessageAvailable(NativeToJsMessageQueue queue);
void notifyOfFlush(boolean fromOnlineEvent) {} void notifyOfFlush(NativeToJsMessageQueue queue, boolean fromOnlineEvent) {}
void reset() {} void reset() {}
} }
/** Uses JS polls for messages on a timer.. */ /** Uses JS polls for messages on a timer.. */
private class PollingBridgeMode extends BridgeMode { public static class NoOpBridgeMode extends BridgeMode {
@Override void onNativeToJsMessageAvailable() { @Override public void onNativeToJsMessageAvailable(NativeToJsMessageQueue queue) {
} }
} }
/** Uses webView.loadUrl("javascript:") to execute messages. */ /** Uses webView.loadUrl("javascript:") to execute messages. */
private class LoadUrlBridgeMode extends BridgeMode { public static class LoadUrlBridgeMode extends BridgeMode {
final Runnable runnable = new Runnable() { private final CordovaWebView webView;
public void run() { private final CordovaInterface cordova;
String js = popAndEncodeAsJs();
if (js != null) {
webView.loadUrlIntoView("javascript:" + js, false);
}
}
};
@Override void onNativeToJsMessageAvailable() { public LoadUrlBridgeMode(CordovaWebView webView, CordovaInterface cordova) {
cordova.getActivity().runOnUiThread(runnable); 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. */ /** 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 online;
private boolean ignoreNextFlush; private boolean ignoreNextFlush;
final Runnable toggleNetworkRunnable = new Runnable() { public interface OnlineEventsBridgeModeDelegate {
public void run() { void setNetworkAvailable(boolean value);
if (!queue.isEmpty()) { void runOnUiThread(Runnable r);
ignoreNextFlush = false;
webView.setNetworkAvailable(online);
}
}
};
final Runnable resetNetworkRunnable = new Runnable() {
public void run() {
online = false;
// If the following call triggers a notifyOfFlush, then ignore it.
ignoreNextFlush = true;
webView.setNetworkAvailable(true);
}
};
@Override void reset() {
cordova.getActivity().runOnUiThread(resetNetworkRunnable);
} }
@Override void onNativeToJsMessageAvailable() {
cordova.getActivity().runOnUiThread(toggleNetworkRunnable); public OnlineEventsBridgeMode(OnlineEventsBridgeModeDelegate delegate) {
this.delegate = delegate;
}
@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;
delegate.setNetworkAvailable(true);
}
});
}
@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. // 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) { if (fromOnlineEvent && !ignoreNextFlush) {
online = !online; 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 { private static class JsMessage {
final String jsPayloadOrCallbackId; final String jsPayloadOrCallbackId;
final PluginResult pluginResult; final PluginResult pluginResult;