summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/javaaddons
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/javaaddons')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java195
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java260
2 files changed, 455 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java
new file mode 100644
index 000000000..33a97955f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java
@@ -0,0 +1,195 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.javaaddons;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import dalvik.system.DexClassLoader;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * The manager for addon-provided Java code.
+ *
+ * Java code in addons can be loaded using the Dex:Load message, and unloaded
+ * via the Dex:Unload message. Addon classes loaded are checked for a constructor
+ * that takes a Map<String, Handler.Callback>. If such a constructor
+ * exists, it is called and the objects populated into the map by the constructor
+ * are registered as event listeners. If no such constructor exists, the default
+ * constructor is invoked instead.
+ *
+ * Note: The Map and Handler.Callback classes were used in this API definition
+ * rather than defining a custom class. This was done explicitly so that the
+ * addon code can be compiled against the android.jar provided in the Android
+ * SDK, rather than having to be compiled against Fennec source code.
+ *
+ * The Handler.Callback instances provided (as described above) are invoked with
+ * Message objects when the corresponding events are dispatched. The Bundle
+ * object attached to the Message will contain the "primitive" values from the
+ * JSON of the event. ("primitive" includes bool/int/long/double/String). If
+ * the addon callback wishes to synchronously return a value back to the event
+ * dispatcher, they can do so by inserting the response string into the bundle
+ * under the key "response".
+ */
+public class JavaAddonManager implements GeckoEventListener {
+ private static final String LOGTAG = "GeckoJavaAddonManager";
+
+ private static JavaAddonManager sInstance;
+
+ private final EventDispatcher mDispatcher;
+ private final Map<String, Map<String, GeckoEventListener>> mAddonCallbacks;
+
+ private Context mApplicationContext;
+
+ public static JavaAddonManager getInstance() {
+ if (sInstance == null) {
+ sInstance = new JavaAddonManager();
+ }
+ return sInstance;
+ }
+
+ private JavaAddonManager() {
+ mDispatcher = EventDispatcher.getInstance();
+ mAddonCallbacks = new HashMap<String, Map<String, GeckoEventListener>>();
+ }
+
+ public void init(Context applicationContext) {
+ if (mApplicationContext != null) {
+ // we've already done this registration. don't do it again
+ return;
+ }
+ mApplicationContext = applicationContext;
+ mDispatcher.registerGeckoThreadListener(this,
+ "Dex:Load",
+ "Dex:Unload");
+ JavaAddonManagerV1.getInstance().init(applicationContext);
+ }
+
+ @Override
+ public void handleMessage(String event, JSONObject message) {
+ try {
+ if (event.equals("Dex:Load")) {
+ String zipFile = message.getString("zipfile");
+ String implClass = message.getString("impl");
+ Log.d(LOGTAG, "Attempting to load classes.dex file from " + zipFile + " and instantiate " + implClass);
+ try {
+ File tmpDir = mApplicationContext.getDir("dex", 0);
+ DexClassLoader loader = new DexClassLoader(zipFile, tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader());
+ Class<?> c = loader.loadClass(implClass);
+ try {
+ Constructor<?> constructor = c.getDeclaredConstructor(Map.class);
+ Map<String, Handler.Callback> callbacks = new HashMap<String, Handler.Callback>();
+ constructor.newInstance(callbacks);
+ registerCallbacks(zipFile, callbacks);
+ } catch (NoSuchMethodException nsme) {
+ Log.d(LOGTAG, "Did not find constructor with parameters Map<String, Handler.Callback>. Falling back to default constructor...");
+ // fallback for instances with no constructor that takes a Map<String, Handler.Callback>
+ c.newInstance();
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Unable to load dex successfully", e);
+ }
+ } else if (event.equals("Dex:Unload")) {
+ String zipFile = message.getString("zipfile");
+ unregisterCallbacks(zipFile);
+ }
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Exception handling message [" + event + "]:", e);
+ }
+ }
+
+ private void registerCallbacks(String zipFile, Map<String, Handler.Callback> callbacks) {
+ Map<String, GeckoEventListener> addonCallbacks = mAddonCallbacks.get(zipFile);
+ if (addonCallbacks != null) {
+ Log.w(LOGTAG, "Found pre-existing callbacks for zipfile [" + zipFile + "]; aborting re-registration!");
+ return;
+ }
+ addonCallbacks = new HashMap<String, GeckoEventListener>();
+ for (String event : callbacks.keySet()) {
+ CallbackWrapper wrapper = new CallbackWrapper(callbacks.get(event));
+ mDispatcher.registerGeckoThreadListener(wrapper, event);
+ addonCallbacks.put(event, wrapper);
+ }
+ mAddonCallbacks.put(zipFile, addonCallbacks);
+ }
+
+ private void unregisterCallbacks(String zipFile) {
+ Map<String, GeckoEventListener> callbacks = mAddonCallbacks.remove(zipFile);
+ if (callbacks == null) {
+ Log.w(LOGTAG, "Attempting to unregister callbacks from zipfile [" + zipFile + "] which has no callbacks registered.");
+ return;
+ }
+ for (String event : callbacks.keySet()) {
+ mDispatcher.unregisterGeckoThreadListener(callbacks.get(event), event);
+ }
+ }
+
+ private static class CallbackWrapper implements GeckoEventListener {
+ private final Handler.Callback mDelegate;
+ private Bundle mBundle;
+
+ CallbackWrapper(Handler.Callback delegate) {
+ mDelegate = delegate;
+ }
+
+ private Bundle jsonToBundle(JSONObject json) {
+ // XXX right now we only support primitive types;
+ // we don't recurse down into JSONArray or JSONObject instances
+ Bundle b = new Bundle();
+ for (Iterator<?> keys = json.keys(); keys.hasNext(); ) {
+ try {
+ String key = (String)keys.next();
+ Object value = json.get(key);
+ if (value instanceof Integer) {
+ b.putInt(key, (Integer)value);
+ } else if (value instanceof String) {
+ b.putString(key, (String)value);
+ } else if (value instanceof Boolean) {
+ b.putBoolean(key, (Boolean)value);
+ } else if (value instanceof Long) {
+ b.putLong(key, (Long)value);
+ } else if (value instanceof Double) {
+ b.putDouble(key, (Double)value);
+ }
+ } catch (JSONException e) {
+ Log.d(LOGTAG, "Error during JSON->bundle conversion", e);
+ }
+ }
+ return b;
+ }
+
+ @Override
+ public void handleMessage(String event, JSONObject json) {
+ try {
+ if (mBundle != null) {
+ Log.w(LOGTAG, "Event [" + event + "] handler is re-entrant; response messages may be lost");
+ }
+ mBundle = jsonToBundle(json);
+ Message msg = new Message();
+ msg.setData(mBundle);
+ mDelegate.handleMessage(msg);
+
+ JSONObject obj = new JSONObject();
+ obj.put("response", mBundle.getString("response"));
+ EventDispatcher.sendResponse(json, obj);
+ mBundle = null;
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Caught exception thrown from wrapped addon message handler", e);
+ }
+ }
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java
new file mode 100644
index 000000000..f361773ca
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java
@@ -0,0 +1,260 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.javaaddons;
+
+import android.content.Context;
+import android.util.Log;
+import android.util.Pair;
+import dalvik.system.DexClassLoader;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.util.GeckoJarReader;
+import org.mozilla.gecko.util.GeckoRequest;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.javaaddons.JavaAddonInterfaceV1;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+public class JavaAddonManagerV1 implements NativeEventListener {
+ private static final String LOGTAG = "GeckoJavaAddonMgrV1";
+ public static final String MESSAGE_LOAD = "JavaAddonManagerV1:Load";
+ public static final String MESSAGE_UNLOAD = "JavaAddonManagerV1:Unload";
+
+ private static JavaAddonManagerV1 sInstance;
+
+ // Protected by static synchronized.
+ private Context mApplicationContext;
+
+ private final org.mozilla.gecko.EventDispatcher mDispatcher;
+
+ // Protected by synchronized (this).
+ private final Map<String, EventDispatcherImpl> mGUIDToDispatcherMap = new HashMap<>();
+
+ public static synchronized JavaAddonManagerV1 getInstance() {
+ if (sInstance == null) {
+ sInstance = new JavaAddonManagerV1();
+ }
+ return sInstance;
+ }
+
+ private JavaAddonManagerV1() {
+ mDispatcher = org.mozilla.gecko.EventDispatcher.getInstance();
+ }
+
+ public synchronized void init(Context applicationContext) {
+ if (mApplicationContext != null) {
+ // We've already registered; don't register again.
+ return;
+ }
+ mApplicationContext = applicationContext;
+ mDispatcher.registerGeckoThreadListener(this,
+ MESSAGE_LOAD,
+ MESSAGE_UNLOAD);
+ }
+
+ protected String getExtension(String filename) {
+ if (filename == null) {
+ return "";
+ }
+ final int last = filename.lastIndexOf(".");
+ if (last < 0) {
+ return "";
+ }
+ return filename.substring(last);
+ }
+
+ protected synchronized EventDispatcherImpl registerNewInstance(String classname, String filename)
+ throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, IOException {
+ Log.d(LOGTAG, "Attempting to instantiate " + classname + "from filename " + filename);
+
+ // It's important to maintain the extension, either .dex, .apk, .jar.
+ final String extension = getExtension(filename);
+ final File dexFile = GeckoJarReader.extractStream(mApplicationContext, filename, mApplicationContext.getCacheDir(), "." + extension);
+ try {
+ if (dexFile == null) {
+ throw new IOException("Could not find file " + filename);
+ }
+ final File tmpDir = mApplicationContext.getDir("dex", 0); // We'd prefer getCodeCacheDir but it's API 21+.
+ final DexClassLoader loader = new DexClassLoader(dexFile.getAbsolutePath(), tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader());
+ final Class<?> c = loader.loadClass(classname);
+ final Constructor<?> constructor = c.getDeclaredConstructor(Context.class, JavaAddonInterfaceV1.EventDispatcher.class);
+ final String guid = Utils.generateGuid();
+ final EventDispatcherImpl dispatcher = new EventDispatcherImpl(guid, filename);
+ final Object instance = constructor.newInstance(mApplicationContext, dispatcher);
+ mGUIDToDispatcherMap.put(guid, dispatcher);
+ return dispatcher;
+ } finally {
+ // DexClassLoader writes an optimized version, so we can get rid of our temporary extracted version.
+ if (dexFile != null) {
+ dexFile.delete();
+ }
+ }
+ }
+
+ @Override
+ public synchronized void handleMessage(String event, NativeJSObject message, org.mozilla.gecko.util.EventCallback callback) {
+ try {
+ switch (event) {
+ case MESSAGE_LOAD: {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ final String classname = message.getString("classname");
+ final String filename = message.getString("filename");
+ final EventDispatcherImpl dispatcher = registerNewInstance(classname, filename);
+ callback.sendSuccess(dispatcher.guid);
+ }
+ break;
+ case MESSAGE_UNLOAD: {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ final String guid = message.getString("guid");
+ final EventDispatcherImpl dispatcher = mGUIDToDispatcherMap.remove(guid);
+ if (dispatcher == null) {
+ Log.w(LOGTAG, "Attempting to unload addon with unknown associated dispatcher; ignoring.");
+ callback.sendSuccess(false);
+ }
+ dispatcher.unregisterAllEventListeners();
+ callback.sendSuccess(true);
+ }
+ break;
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Exception handling message [" + event + "]", e);
+ if (callback != null) {
+ callback.sendError("Exception handling message [" + event + "]: " + e.toString());
+ }
+ }
+ }
+
+ /**
+ * An event dispatcher is tied to a single Java Addon instance. It serves to prefix all
+ * messages with its unique GUID.
+ * <p/>
+ * Curiously, the dispatcher does not hold a direct reference to its add-on instance. It will
+ * likely hold indirect instances through its wrapping map, since the instance will probably
+ * register event listeners that hold a reference to itself. When these listeners are
+ * unregistered, any link will be broken, allowing the instances to be garbage collected.
+ */
+ private class EventDispatcherImpl implements JavaAddonInterfaceV1.EventDispatcher {
+ private final String guid;
+ private final String dexFileName;
+
+ // Protected by synchronized (this).
+ private final Map<JavaAddonInterfaceV1.EventListener, Pair<NativeEventListener, String[]>> mListenerToWrapperMap = new IdentityHashMap<>();
+
+ public EventDispatcherImpl(String guid, String dexFileName) {
+ this.guid = guid;
+ this.dexFileName = dexFileName;
+ }
+
+ protected class ListenerWrapper implements NativeEventListener {
+ private final JavaAddonInterfaceV1.EventListener listener;
+
+ public ListenerWrapper(JavaAddonInterfaceV1.EventListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void handleMessage(String prefixedEvent, NativeJSObject message, final org.mozilla.gecko.util.EventCallback callback) {
+ if (!prefixedEvent.startsWith(guid + ":")) {
+ return;
+ }
+ final String event = prefixedEvent.substring(guid.length() + 1); // Skip "guid:".
+ try {
+ JavaAddonInterfaceV1.EventCallback callbackAdapter = null;
+ if (callback != null) {
+ callbackAdapter = new JavaAddonInterfaceV1.EventCallback() {
+ @Override
+ public void sendSuccess(Object response) {
+ callback.sendSuccess(response);
+ }
+
+ @Override
+ public void sendError(Object response) {
+ callback.sendError(response);
+ }
+ };
+ }
+ final JSONObject json = new JSONObject(message.toString());
+ listener.handleMessage(mApplicationContext, event, json, callbackAdapter);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Exception handling message [" + prefixedEvent + "]", e);
+ if (callback != null) {
+ callback.sendError("Got exception handling message [" + prefixedEvent + "]: " + e.toString());
+ }
+ }
+ }
+ }
+
+ @Override
+ public synchronized void registerEventListener(final JavaAddonInterfaceV1.EventListener listener, String... events) {
+ if (mListenerToWrapperMap.containsKey(listener)) {
+ Log.e(LOGTAG, "Attempting to register listener which is already registered; ignoring.");
+ return;
+ }
+
+ final NativeEventListener listenerWrapper = new ListenerWrapper(listener);
+
+ final String[] prefixedEvents = new String[events.length];
+ for (int i = 0; i < events.length; i++) {
+ prefixedEvents[i] = this.guid + ":" + events[i];
+ }
+ mDispatcher.registerGeckoThreadListener(listenerWrapper, prefixedEvents);
+ mListenerToWrapperMap.put(listener, new Pair<>(listenerWrapper, prefixedEvents));
+ }
+
+ @Override
+ public synchronized void unregisterEventListener(final JavaAddonInterfaceV1.EventListener listener) {
+ final Pair<NativeEventListener, String[]> pair = mListenerToWrapperMap.remove(listener);
+ if (pair == null) {
+ Log.e(LOGTAG, "Attempting to unregister listener which is not registered; ignoring.");
+ return;
+ }
+ mDispatcher.unregisterGeckoThreadListener(pair.first, pair.second);
+ }
+
+
+ protected synchronized void unregisterAllEventListeners() {
+ // Unregister everything, then forget everything.
+ for (Pair<NativeEventListener, String[]> pair : mListenerToWrapperMap.values()) {
+ mDispatcher.unregisterGeckoThreadListener(pair.first, pair.second);
+ }
+ mListenerToWrapperMap.clear();
+ }
+
+ @Override
+ public void sendRequestToGecko(final String event, final JSONObject message, final JavaAddonInterfaceV1.RequestCallback callback) {
+ final String prefixedEvent = guid + ":" + event;
+ GeckoAppShell.sendRequestToGecko(new GeckoRequest(prefixedEvent, message) {
+ @Override
+ public void onResponse(NativeJSObject nativeJSObject) {
+ if (callback == null) {
+ // Nothing to do.
+ return;
+ }
+ try {
+ final JSONObject json = new JSONObject(nativeJSObject.toString());
+ callback.onResponse(GeckoAppShell.getContext(), json);
+ } catch (JSONException e) {
+ // No way to report failure.
+ Log.e(LOGTAG, "Exception handling response to request [" + event + "]:", e);
+ }
+ }
+ });
+ }
+ }
+}