Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko;
8 import org.mozilla.gecko.util.GeckoEventListener;
10 import org.json.JSONException;
11 import org.json.JSONObject;
13 import android.content.Context;
14 import android.os.Bundle;
15 import android.os.Handler;
16 import android.os.Message;
17 import android.util.Log;
19 import dalvik.system.DexClassLoader;
21 import java.io.File;
22 import java.lang.reflect.Constructor;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.Map;
27 /**
28 * The manager for addon-provided Java code.
29 *
30 * Java code in addons can be loaded using the Dex:Load message, and unloaded
31 * via the Dex:Unload message. Addon classes loaded are checked for a constructor
32 * that takes a Map<String, Handler.Callback>. If such a constructor
33 * exists, it is called and the objects populated into the map by the constructor
34 * are registered as event listeners. If no such constructor exists, the default
35 * constructor is invoked instead.
36 *
37 * Note: The Map and Handler.Callback classes were used in this API definition
38 * rather than defining a custom class. This was done explicitly so that the
39 * addon code can be compiled against the android.jar provided in the Android
40 * SDK, rather than having to be compiled against Fennec source code.
41 *
42 * The Handler.Callback instances provided (as described above) are inovked with
43 * Message objects when the corresponding events are dispatched. The Bundle
44 * object attached to the Message will contain the "primitive" values from the
45 * JSON of the event. ("primitive" includes bool/int/long/double/String). If
46 * the addon callback wishes to synchronously return a value back to the event
47 * dispatcher, they can do so by inserting the response string into the bundle
48 * under the key "response".
49 */
50 class JavaAddonManager implements GeckoEventListener {
51 private static final String LOGTAG = "GeckoJavaAddonManager";
53 private static JavaAddonManager sInstance;
55 private final EventDispatcher mDispatcher;
56 private final Map<String, Map<String, GeckoEventListener>> mAddonCallbacks;
58 private Context mApplicationContext;
60 public static JavaAddonManager getInstance() {
61 if (sInstance == null) {
62 sInstance = new JavaAddonManager();
63 }
64 return sInstance;
65 }
67 private JavaAddonManager() {
68 mDispatcher = GeckoAppShell.getEventDispatcher();
69 mAddonCallbacks = new HashMap<String, Map<String, GeckoEventListener>>();
70 }
72 void init(Context applicationContext) {
73 if (mApplicationContext != null) {
74 // we've already done this registration. don't do it again
75 return;
76 }
77 mApplicationContext = applicationContext;
78 mDispatcher.registerEventListener("Dex:Load", this);
79 mDispatcher.registerEventListener("Dex:Unload", this);
80 }
82 @Override
83 public void handleMessage(String event, JSONObject message) {
84 try {
85 if (event.equals("Dex:Load")) {
86 String zipFile = message.getString("zipfile");
87 String implClass = message.getString("impl");
88 Log.d(LOGTAG, "Attempting to load classes.dex file from " + zipFile + " and instantiate " + implClass);
89 try {
90 File tmpDir = mApplicationContext.getDir("dex", 0);
91 DexClassLoader loader = new DexClassLoader(zipFile, tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader());
92 Class<?> c = loader.loadClass(implClass);
93 try {
94 Constructor<?> constructor = c.getDeclaredConstructor(Map.class);
95 Map<String, Handler.Callback> callbacks = new HashMap<String, Handler.Callback>();
96 constructor.newInstance(callbacks);
97 registerCallbacks(zipFile, callbacks);
98 } catch (NoSuchMethodException nsme) {
99 Log.d(LOGTAG, "Did not find constructor with parameters Map<String, Handler.Callback>. Falling back to default constructor...");
100 // fallback for instances with no constructor that takes a Map<String, Handler.Callback>
101 c.newInstance();
102 }
103 } catch (Exception e) {
104 Log.e(LOGTAG, "Unable to load dex successfully", e);
105 }
106 } else if (event.equals("Dex:Unload")) {
107 String zipFile = message.getString("zipfile");
108 unregisterCallbacks(zipFile);
109 }
110 } catch (JSONException e) {
111 Log.e(LOGTAG, "Exception handling message [" + event + "]:", e);
112 }
113 }
115 private void registerCallbacks(String zipFile, Map<String, Handler.Callback> callbacks) {
116 Map<String, GeckoEventListener> addonCallbacks = mAddonCallbacks.get(zipFile);
117 if (addonCallbacks != null) {
118 Log.w(LOGTAG, "Found pre-existing callbacks for zipfile [" + zipFile + "]; aborting re-registration!");
119 return;
120 }
121 addonCallbacks = new HashMap<String, GeckoEventListener>();
122 for (String event : callbacks.keySet()) {
123 CallbackWrapper wrapper = new CallbackWrapper(callbacks.get(event));
124 mDispatcher.registerEventListener(event, wrapper);
125 addonCallbacks.put(event, wrapper);
126 }
127 mAddonCallbacks.put(zipFile, addonCallbacks);
128 }
130 private void unregisterCallbacks(String zipFile) {
131 Map<String, GeckoEventListener> callbacks = mAddonCallbacks.remove(zipFile);
132 if (callbacks == null) {
133 Log.w(LOGTAG, "Attempting to unregister callbacks from zipfile [" + zipFile + "] which has no callbacks registered.");
134 return;
135 }
136 for (String event : callbacks.keySet()) {
137 mDispatcher.unregisterEventListener(event, callbacks.get(event));
138 }
139 }
141 private static class CallbackWrapper implements GeckoEventListener {
142 private final Handler.Callback mDelegate;
143 private Bundle mBundle;
145 CallbackWrapper(Handler.Callback delegate) {
146 mDelegate = delegate;
147 }
149 private Bundle jsonToBundle(JSONObject json) {
150 // XXX right now we only support primitive types;
151 // we don't recurse down into JSONArray or JSONObject instances
152 Bundle b = new Bundle();
153 for (Iterator<?> keys = json.keys(); keys.hasNext(); ) {
154 try {
155 String key = (String)keys.next();
156 Object value = json.get(key);
157 if (value instanceof Integer) {
158 b.putInt(key, (Integer)value);
159 } else if (value instanceof String) {
160 b.putString(key, (String)value);
161 } else if (value instanceof Boolean) {
162 b.putBoolean(key, (Boolean)value);
163 } else if (value instanceof Long) {
164 b.putLong(key, (Long)value);
165 } else if (value instanceof Double) {
166 b.putDouble(key, (Double)value);
167 }
168 } catch (JSONException e) {
169 Log.d(LOGTAG, "Error during JSON->bundle conversion", e);
170 }
171 }
172 return b;
173 }
175 @Override
176 public void handleMessage(String event, JSONObject json) {
177 try {
178 if (mBundle != null) {
179 Log.w(LOGTAG, "Event [" + event + "] handler is re-entrant; response messages may be lost");
180 }
181 mBundle = jsonToBundle(json);
182 Message msg = new Message();
183 msg.setData(mBundle);
184 mDelegate.handleMessage(msg);
186 JSONObject obj = new JSONObject();
187 obj.put("response", mBundle.getString("response"));
188 EventDispatcher.sendResponse(json, obj);
189 mBundle = null;
190 } catch (Exception e) {
191 Log.e(LOGTAG, "Caught exception thrown from wrapped addon message handler", e);
192 }
193 }
194 }
195 }