diff options
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/javaaddons')
-rw-r--r-- | mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java | 195 | ||||
-rw-r--r-- | mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java | 260 |
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); + } + } + }); + } + } +} |