summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java
blob: f361773ca5de32dd9e5c101b8739c36ee738ced9 (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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/* -*- 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.util.Log;
import android.util.Pair;
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);
                    }
                }
            });
        }
    }
}