michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: michael@0: import android.content.Context; michael@0: import android.os.Bundle; michael@0: import android.os.Handler; michael@0: import android.os.Message; michael@0: import android.util.Log; michael@0: michael@0: import dalvik.system.DexClassLoader; michael@0: michael@0: import java.io.File; michael@0: import java.lang.reflect.Constructor; michael@0: import java.util.HashMap; michael@0: import java.util.Iterator; michael@0: import java.util.Map; michael@0: michael@0: /** michael@0: * The manager for addon-provided Java code. michael@0: * michael@0: * Java code in addons can be loaded using the Dex:Load message, and unloaded michael@0: * via the Dex:Unload message. Addon classes loaded are checked for a constructor michael@0: * that takes a Map<String, Handler.Callback>. If such a constructor michael@0: * exists, it is called and the objects populated into the map by the constructor michael@0: * are registered as event listeners. If no such constructor exists, the default michael@0: * constructor is invoked instead. michael@0: * michael@0: * Note: The Map and Handler.Callback classes were used in this API definition michael@0: * rather than defining a custom class. This was done explicitly so that the michael@0: * addon code can be compiled against the android.jar provided in the Android michael@0: * SDK, rather than having to be compiled against Fennec source code. michael@0: * michael@0: * The Handler.Callback instances provided (as described above) are inovked with michael@0: * Message objects when the corresponding events are dispatched. The Bundle michael@0: * object attached to the Message will contain the "primitive" values from the michael@0: * JSON of the event. ("primitive" includes bool/int/long/double/String). If michael@0: * the addon callback wishes to synchronously return a value back to the event michael@0: * dispatcher, they can do so by inserting the response string into the bundle michael@0: * under the key "response". michael@0: */ michael@0: class JavaAddonManager implements GeckoEventListener { michael@0: private static final String LOGTAG = "GeckoJavaAddonManager"; michael@0: michael@0: private static JavaAddonManager sInstance; michael@0: michael@0: private final EventDispatcher mDispatcher; michael@0: private final Map> mAddonCallbacks; michael@0: michael@0: private Context mApplicationContext; michael@0: michael@0: public static JavaAddonManager getInstance() { michael@0: if (sInstance == null) { michael@0: sInstance = new JavaAddonManager(); michael@0: } michael@0: return sInstance; michael@0: } michael@0: michael@0: private JavaAddonManager() { michael@0: mDispatcher = GeckoAppShell.getEventDispatcher(); michael@0: mAddonCallbacks = new HashMap>(); michael@0: } michael@0: michael@0: void init(Context applicationContext) { michael@0: if (mApplicationContext != null) { michael@0: // we've already done this registration. don't do it again michael@0: return; michael@0: } michael@0: mApplicationContext = applicationContext; michael@0: mDispatcher.registerEventListener("Dex:Load", this); michael@0: mDispatcher.registerEventListener("Dex:Unload", this); michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: try { michael@0: if (event.equals("Dex:Load")) { michael@0: String zipFile = message.getString("zipfile"); michael@0: String implClass = message.getString("impl"); michael@0: Log.d(LOGTAG, "Attempting to load classes.dex file from " + zipFile + " and instantiate " + implClass); michael@0: try { michael@0: File tmpDir = mApplicationContext.getDir("dex", 0); michael@0: DexClassLoader loader = new DexClassLoader(zipFile, tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader()); michael@0: Class c = loader.loadClass(implClass); michael@0: try { michael@0: Constructor constructor = c.getDeclaredConstructor(Map.class); michael@0: Map callbacks = new HashMap(); michael@0: constructor.newInstance(callbacks); michael@0: registerCallbacks(zipFile, callbacks); michael@0: } catch (NoSuchMethodException nsme) { michael@0: Log.d(LOGTAG, "Did not find constructor with parameters Map. Falling back to default constructor..."); michael@0: // fallback for instances with no constructor that takes a Map michael@0: c.newInstance(); michael@0: } michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Unable to load dex successfully", e); michael@0: } michael@0: } else if (event.equals("Dex:Unload")) { michael@0: String zipFile = message.getString("zipfile"); michael@0: unregisterCallbacks(zipFile); michael@0: } michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "Exception handling message [" + event + "]:", e); michael@0: } michael@0: } michael@0: michael@0: private void registerCallbacks(String zipFile, Map callbacks) { michael@0: Map addonCallbacks = mAddonCallbacks.get(zipFile); michael@0: if (addonCallbacks != null) { michael@0: Log.w(LOGTAG, "Found pre-existing callbacks for zipfile [" + zipFile + "]; aborting re-registration!"); michael@0: return; michael@0: } michael@0: addonCallbacks = new HashMap(); michael@0: for (String event : callbacks.keySet()) { michael@0: CallbackWrapper wrapper = new CallbackWrapper(callbacks.get(event)); michael@0: mDispatcher.registerEventListener(event, wrapper); michael@0: addonCallbacks.put(event, wrapper); michael@0: } michael@0: mAddonCallbacks.put(zipFile, addonCallbacks); michael@0: } michael@0: michael@0: private void unregisterCallbacks(String zipFile) { michael@0: Map callbacks = mAddonCallbacks.remove(zipFile); michael@0: if (callbacks == null) { michael@0: Log.w(LOGTAG, "Attempting to unregister callbacks from zipfile [" + zipFile + "] which has no callbacks registered."); michael@0: return; michael@0: } michael@0: for (String event : callbacks.keySet()) { michael@0: mDispatcher.unregisterEventListener(event, callbacks.get(event)); michael@0: } michael@0: } michael@0: michael@0: private static class CallbackWrapper implements GeckoEventListener { michael@0: private final Handler.Callback mDelegate; michael@0: private Bundle mBundle; michael@0: michael@0: CallbackWrapper(Handler.Callback delegate) { michael@0: mDelegate = delegate; michael@0: } michael@0: michael@0: private Bundle jsonToBundle(JSONObject json) { michael@0: // XXX right now we only support primitive types; michael@0: // we don't recurse down into JSONArray or JSONObject instances michael@0: Bundle b = new Bundle(); michael@0: for (Iterator keys = json.keys(); keys.hasNext(); ) { michael@0: try { michael@0: String key = (String)keys.next(); michael@0: Object value = json.get(key); michael@0: if (value instanceof Integer) { michael@0: b.putInt(key, (Integer)value); michael@0: } else if (value instanceof String) { michael@0: b.putString(key, (String)value); michael@0: } else if (value instanceof Boolean) { michael@0: b.putBoolean(key, (Boolean)value); michael@0: } else if (value instanceof Long) { michael@0: b.putLong(key, (Long)value); michael@0: } else if (value instanceof Double) { michael@0: b.putDouble(key, (Double)value); michael@0: } michael@0: } catch (JSONException e) { michael@0: Log.d(LOGTAG, "Error during JSON->bundle conversion", e); michael@0: } michael@0: } michael@0: return b; michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject json) { michael@0: try { michael@0: if (mBundle != null) { michael@0: Log.w(LOGTAG, "Event [" + event + "] handler is re-entrant; response messages may be lost"); michael@0: } michael@0: mBundle = jsonToBundle(json); michael@0: Message msg = new Message(); michael@0: msg.setData(mBundle); michael@0: mDelegate.handleMessage(msg); michael@0: michael@0: JSONObject obj = new JSONObject(); michael@0: obj.put("response", mBundle.getString("response")); michael@0: EventDispatcher.sendResponse(json, obj); michael@0: mBundle = null; michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Caught exception thrown from wrapped addon message handler", e); michael@0: } michael@0: } michael@0: } michael@0: }