summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java
blob: 33a97955f4ffbe182592de1086d5966b14765695 (plain)
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.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);
            }
        }
    }
}