There are several parts to this ticket: 1) Produce javaaddons-1.0.jar, a standalone JAR defining a (versioned) Java interface suitable for consumption by third-party Java addon implementations. 2) Support the new V1 interface in the JavaAddonManager. 3) Add Robocop JavascriptTests testing the JavaScript message passing interface to and from Java. This patch can be read as "not in tests/" and "everything in tests/".
261 lines
12 KiB
Java
261 lines
12 KiB
Java
/* -*- 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.support.v4.util.Pair;
|
|
import android.util.Log;
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|