1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 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;
import org.mozilla.gecko.util.GeckoEventListener;
import org.json.JSONException;
import org.json.JSONObject;
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 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 inovked 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".
*/
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 = GeckoAppShell.getEventDispatcher();
mAddonCallbacks = new HashMap<String, Map<String, GeckoEventListener>>();
}
void init(Context applicationContext) {
if (mApplicationContext != null) {
// we've already done this registration. don't do it again
return;
}
mApplicationContext = applicationContext;
mDispatcher.registerEventListener("Dex:Load", this);
mDispatcher.registerEventListener("Dex:Unload", this);
}
@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.registerEventListener(event, wrapper);
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.unregisterEventListener(event, callbacks.get(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);
}
}
}
}
|