mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1168407 - Implement a bidirectional Java addon interface. f=jchen,r=rnewman,r=mfinkle
There are several parts to this ticket: 1) Produce javaaddons-1.0.jar, a standalone JAR defining a (versioned) Java interface suitable for consumption by third-party Java addon implementations. 2) Support the new V1 interface in the JavaAddonManager. 3) Add Robocop JavascriptTests testing the JavaScript message passing interface to and from Java. This patch can be read as "not in tests/" and "everything in tests/".
This commit is contained in:
parent
e05804a07e
commit
7e516a64dd
@ -147,6 +147,10 @@ accessible/xpcom/export: xpcom/xpidl/export
|
||||
# The widget binding generator code is part of the annotationProcessors.
|
||||
widget/android/bindings/export: build/annotationProcessors/export
|
||||
|
||||
# The roboextender addon includes a classes.dex containing a test Java addon.
|
||||
# The test addon must be built first.
|
||||
mobile/android/tests/browser/robocop/roboextender/tools: mobile/android/tests/javaaddons/tools
|
||||
|
||||
ifdef ENABLE_CLANG_PLUGIN
|
||||
$(filter-out build/clang-plugin/%,$(compile_targets)): build/clang-plugin/target build/clang-plugin/tests/target
|
||||
build/clang-plugin/tests/target: build/clang-plugin/target
|
||||
|
@ -46,6 +46,7 @@ import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.home.HomePanelsManager;
|
||||
import org.mozilla.gecko.home.SearchEngine;
|
||||
import org.mozilla.gecko.javaaddons.JavaAddonManager;
|
||||
import org.mozilla.gecko.menu.GeckoMenu;
|
||||
import org.mozilla.gecko.menu.GeckoMenuItem;
|
||||
import org.mozilla.gecko.mozglue.ContextUtils;
|
||||
|
@ -158,8 +158,14 @@ public final class EventDispatcher {
|
||||
}
|
||||
|
||||
if (listeners != null) {
|
||||
if (listeners.size() == 0) {
|
||||
if (listeners.isEmpty()) {
|
||||
Log.w(LOGTAG, "No listeners for " + type);
|
||||
|
||||
// There were native listeners, and they're gone. Dispatch an error rather than
|
||||
// looking for JSON listeners.
|
||||
if (callback != null) {
|
||||
callback.sendError("No listeners for request");
|
||||
}
|
||||
}
|
||||
try {
|
||||
for (final NativeEventListener listener : listeners) {
|
||||
@ -195,7 +201,7 @@ public final class EventDispatcher {
|
||||
synchronized (mGeckoThreadJSONListeners) {
|
||||
listeners = mGeckoThreadJSONListeners.get(type);
|
||||
}
|
||||
if (listeners == null || listeners.size() == 0) {
|
||||
if (listeners == null || listeners.isEmpty()) {
|
||||
Log.w(LOGTAG, "No listeners for " + type);
|
||||
|
||||
// If there are no listeners, dispatch an error.
|
||||
|
@ -105,6 +105,7 @@ ALL_JARS = \
|
||||
gecko-thirdparty.jar \
|
||||
gecko-util.jar \
|
||||
sync-thirdparty.jar \
|
||||
../javaaddons/javaaddons-1.0.jar \
|
||||
$(NULL)
|
||||
|
||||
ifdef MOZ_WEBRTC
|
||||
|
@ -3,20 +3,18 @@
|
||||
* 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;
|
||||
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;
|
||||
@ -47,7 +45,7 @@ import java.util.Map;
|
||||
* dispatcher, they can do so by inserting the response string into the bundle
|
||||
* under the key "response".
|
||||
*/
|
||||
class JavaAddonManager implements GeckoEventListener {
|
||||
public class JavaAddonManager implements GeckoEventListener {
|
||||
private static final String LOGTAG = "GeckoJavaAddonManager";
|
||||
|
||||
private static JavaAddonManager sInstance;
|
||||
@ -69,7 +67,7 @@ class JavaAddonManager implements GeckoEventListener {
|
||||
mAddonCallbacks = new HashMap<String, Map<String, GeckoEventListener>>();
|
||||
}
|
||||
|
||||
void init(Context applicationContext) {
|
||||
public void init(Context applicationContext) {
|
||||
if (mApplicationContext != null) {
|
||||
// we've already done this registration. don't do it again
|
||||
return;
|
||||
@ -78,6 +76,7 @@ class JavaAddonManager implements GeckoEventListener {
|
||||
mDispatcher.registerGeckoThreadListener(this,
|
||||
"Dex:Load",
|
||||
"Dex:Unload");
|
||||
JavaAddonManagerV1.getInstance().init(applicationContext);
|
||||
}
|
||||
|
||||
@Override
|
260
mobile/android/base/javaaddons/JavaAddonManagerV1.java
Normal file
260
mobile/android/base/javaaddons/JavaAddonManagerV1.java
Normal file
@ -0,0 +1,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.support.v4.util.Pair;
|
||||
import android.util.Log;
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -357,7 +357,8 @@ gbjar.sources += [
|
||||
'home/TwoLinePageRow.java',
|
||||
'InputMethods.java',
|
||||
'IntentHelper.java',
|
||||
'JavaAddonManager.java',
|
||||
'javaaddons/JavaAddonManager.java',
|
||||
'javaaddons/JavaAddonManagerV1.java',
|
||||
'LayoutInterceptor.java',
|
||||
'LocaleManager.java',
|
||||
'Locales.java',
|
||||
@ -597,6 +598,7 @@ if CONFIG['MOZ_ANDROID_READING_LIST_SERVICE']:
|
||||
|
||||
gbjar.sources += sync_java_files
|
||||
gbjar.extra_jars += [
|
||||
OBJDIR + '/../javaaddons/javaaddons-1.0.jar',
|
||||
'gecko-R.jar',
|
||||
'gecko-mozglue.jar',
|
||||
'gecko-thirdparty.jar',
|
||||
|
@ -210,6 +210,15 @@
|
||||
*;
|
||||
}
|
||||
|
||||
# Keep all interfaces that might be dynamically required by Java Addons.
|
||||
-keep class org.mozilla.javaaddons.* {
|
||||
*;
|
||||
}
|
||||
|
||||
-keep class org.mozilla.javaaddons.*$* {
|
||||
*;
|
||||
}
|
||||
|
||||
# Disable obfuscation because it makes exception stack traces more difficult to read.
|
||||
-dontobfuscate
|
||||
|
||||
|
@ -37,6 +37,7 @@ android {
|
||||
srcDir "${topobjdir}/mobile/android/gradle/app/src/robocop"
|
||||
srcDir "${topobjdir}/mobile/android/gradle/app/src/background"
|
||||
srcDir "${topobjdir}/mobile/android/gradle/app/src/browser"
|
||||
srcDir "${topobjdir}/mobile/android/gradle/app/src/javaaddons"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
10
mobile/android/javaaddons/Makefile.in
Normal file
10
mobile/android/javaaddons/Makefile.in
Normal file
@ -0,0 +1,10 @@
|
||||
# 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/.
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
JAVA_CLASSPATH := $(ANDROID_SDK)/android.jar
|
||||
include $(topsrcdir)/config/android-common.mk
|
||||
|
||||
libs:: javaaddons-1.0.jar
|
@ -0,0 +1,51 @@
|
||||
/* -*- 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.javaaddons;
|
||||
|
||||
import android.content.Context;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public interface JavaAddonInterfaceV1 {
|
||||
/**
|
||||
* Callback interface for Gecko requests.
|
||||
* <p/>
|
||||
* For each instance of EventCallback, exactly one of sendResponse, sendError, must be called to prevent observer leaks.
|
||||
* If more than one send* method is called, or if a single send method is called multiple times, an
|
||||
* {@link IllegalStateException} will be thrown.
|
||||
*/
|
||||
interface EventCallback {
|
||||
/**
|
||||
* Sends a success response with the given data.
|
||||
*
|
||||
* @param response The response data to send to Gecko. Can be any of the types accepted by
|
||||
* JSONObject#put(String, Object).
|
||||
*/
|
||||
public void sendSuccess(Object response);
|
||||
|
||||
/**
|
||||
* Sends an error response with the given data.
|
||||
*
|
||||
* @param response The response data to send to Gecko. Can be any of the types accepted by
|
||||
* JSONObject#put(String, Object).
|
||||
*/
|
||||
public void sendError(Object response);
|
||||
}
|
||||
|
||||
interface EventDispatcher {
|
||||
void registerEventListener(EventListener listener, String... events);
|
||||
void unregisterEventListener(EventListener listener);
|
||||
|
||||
void sendRequestToGecko(String event, JSONObject message, RequestCallback callback);
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
public void handleMessage(final Context context, final String event, final JSONObject message, final EventCallback callback);
|
||||
}
|
||||
|
||||
interface RequestCallback {
|
||||
void onResponse(final Context context, JSONObject jsonObject);
|
||||
}
|
||||
}
|
11
mobile/android/javaaddons/moz.build
Normal file
11
mobile/android/javaaddons/moz.build
Normal file
@ -0,0 +1,11 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
jar = add_java_jar('javaaddons-1.0')
|
||||
jar.sources = [
|
||||
'java/org/mozilla/javaaddons/JavaAddonInterfaceV1.java',
|
||||
]
|
||||
jar.javac_flags += ['-Xlint:all']
|
@ -126,6 +126,7 @@ class MachCommands(MachCommandBase):
|
||||
srcdir('app/src/robocop/org/mozilla/gecko/tests', 'mobile/android/tests/browser/robocop')
|
||||
srcdir('app/src/background/org/mozilla/gecko', 'mobile/android/tests/background/junit3/src')
|
||||
srcdir('app/src/browser', 'mobile/android/tests/browser/junit3/src')
|
||||
srcdir('app/src/javaaddons', 'mobile/android/tests/javaaddons/src')
|
||||
# Test libraries.
|
||||
srcdir('app/libs', 'build/mobile/robocop')
|
||||
|
||||
@ -135,6 +136,7 @@ class MachCommands(MachCommandBase):
|
||||
srcdir('base/src/main/java/org/mozilla/gecko', 'mobile/android/base')
|
||||
srcdir('base/src/main/java/org/mozilla/mozstumbler', 'mobile/android/stumbler/java/org/mozilla/mozstumbler')
|
||||
srcdir('base/src/main/java/org/mozilla/search', 'mobile/android/search/java/org/mozilla/search')
|
||||
srcdir('base/src/main/java/org/mozilla/javaaddons', 'mobile/android/javaaddons/java/org/mozilla/javaaddons')
|
||||
srcdir('base/src/main/res', 'mobile/android/base/resources')
|
||||
srcdir('base/src/crashreporter/res', 'mobile/android/base/crashreporter/res')
|
||||
|
||||
|
115
mobile/android/modules/JavaAddonManager.jsm
Normal file
115
mobile/android/modules/JavaAddonManager.jsm
Normal file
@ -0,0 +1,115 @@
|
||||
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["JavaAddonManager"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components; /*global Components */
|
||||
|
||||
Cu.import("resource://gre/modules/Messaging.jsm"); /*global Messaging */
|
||||
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
|
||||
|
||||
function resolveGeckoURI(uri) {
|
||||
if (!uri) {
|
||||
throw new Error("Can't resolve an empty uri");
|
||||
}
|
||||
if (uri.startsWith("chrome://")) {
|
||||
let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
|
||||
return registry.convertChromeURL(Services.io.newURI(uri, null, null)).spec;
|
||||
} else if (uri.startsWith("resource://")) {
|
||||
let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
|
||||
return handler.resolveURI(Services.io.newURI(uri, null, null));
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise-based API
|
||||
*/
|
||||
let JavaAddonManager = Object.freeze({
|
||||
classInstanceFromFile: function(classname, filename) {
|
||||
if (!classname) {
|
||||
throw new Error("classname cannot be null");
|
||||
}
|
||||
if (!filename) {
|
||||
throw new Error("filename cannot be null");
|
||||
}
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "JavaAddonManagerV1:Load",
|
||||
classname: classname,
|
||||
filename: resolveGeckoURI(filename)
|
||||
})
|
||||
.then((guid) => {
|
||||
if (!guid) {
|
||||
throw new Error("Internal error: guid should not be null");
|
||||
}
|
||||
return new JavaAddonV1({classname: classname, guid: guid});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function JavaAddonV1(options = {}) {
|
||||
if (!(this instanceof JavaAddonV1)) {
|
||||
return new JavaAddonV1(options);
|
||||
}
|
||||
if (!options.classname) {
|
||||
throw new Error("options.classname cannot be null");
|
||||
}
|
||||
if (!options.guid) {
|
||||
throw new Error("options.guid cannot be null");
|
||||
}
|
||||
this._classname = options.classname;
|
||||
this._guid = options.guid;
|
||||
this._loaded = true;
|
||||
this._listeners = {};
|
||||
}
|
||||
|
||||
JavaAddonV1.prototype = Object.freeze({
|
||||
unload: function() {
|
||||
if (!this._loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
Messaging.sendRequestForResult({
|
||||
type: "JavaAddonManagerV1:Unload",
|
||||
guid: this._guid
|
||||
})
|
||||
.then(() => {
|
||||
this._loaded = false;
|
||||
for (let listener of this._listeners) {
|
||||
// If we use this.removeListener, we prefix twice.
|
||||
Messaging.removeListener(listener);
|
||||
}
|
||||
this._listeners = {};
|
||||
});
|
||||
},
|
||||
|
||||
_prefix: function(message) {
|
||||
let newMessage = Cu.cloneInto(message, {}, { cloneFunctions: false });
|
||||
newMessage.type = this._guid + ":" + message.type;
|
||||
return newMessage;
|
||||
},
|
||||
|
||||
sendRequest: function(message) {
|
||||
return Messaging.sendRequest(this._prefix(message));
|
||||
},
|
||||
|
||||
sendRequestForResult: function(message) {
|
||||
return Messaging.sendRequestForResult(this._prefix(message));
|
||||
},
|
||||
|
||||
addListener: function(listener, message) {
|
||||
let prefixedMessage = this._guid + ":" + message;
|
||||
this._listeners[prefixedMessage] = listener;
|
||||
return Messaging.addListener(listener, prefixedMessage);
|
||||
},
|
||||
|
||||
removeListener: function(message) {
|
||||
let prefixedMessage = this._guid + ":" + message;
|
||||
delete this._listeners[prefixedMessage];
|
||||
return Messaging.removeListener(prefixedMessage);
|
||||
}
|
||||
});
|
@ -14,6 +14,7 @@ EXTRA_JS_MODULES += [
|
||||
'HelperApps.jsm',
|
||||
'Home.jsm',
|
||||
'HomeProvider.jsm',
|
||||
'JavaAddonManager.jsm',
|
||||
'JNI.jsm',
|
||||
'LightweightThemeConsumer.jsm',
|
||||
'MatchstickApp.jsm',
|
||||
|
@ -15,6 +15,7 @@ if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
|
||||
DIRS += ['stumbler']
|
||||
|
||||
DIRS += [
|
||||
'javaaddons', # Must be built before base.
|
||||
'base',
|
||||
'chrome',
|
||||
'components',
|
||||
|
@ -123,6 +123,7 @@ skip-if = android_version == "18"
|
||||
[testHistoryService.java]
|
||||
# disabled on 4.3, bug 1116036
|
||||
skip-if = android_version == "18"
|
||||
[testJavaAddons.java]
|
||||
[testJNI.java]
|
||||
# [testMozPay.java] # see bug 945675
|
||||
[testMigrateUI.java]
|
||||
|
@ -19,6 +19,7 @@ INSTALL_TARGETS += TEST
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
libs:: $(_TEST_FILES)
|
||||
tools:: $(_TEST_FILES)
|
||||
$(MKDIR) -p $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base
|
||||
-cp $(TESTPATH)/base/* $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base
|
||||
-cp $(DEPTH)/mobile/android/tests/javaaddons/javaaddons-test.apk $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base
|
||||
|
13
mobile/android/tests/browser/robocop/testJavaAddons.java
Normal file
13
mobile/android/tests/browser/robocop/testJavaAddons.java
Normal file
@ -0,0 +1,13 @@
|
||||
/* 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.tests;
|
||||
|
||||
|
||||
|
||||
public class testJavaAddons extends JavascriptTest {
|
||||
public testJavaAddons() {
|
||||
super("testJavaAddons.js");
|
||||
}
|
||||
}
|
97
mobile/android/tests/browser/robocop/testJavaAddons.js
Normal file
97
mobile/android/tests/browser/robocop/testJavaAddons.js
Normal file
@ -0,0 +1,97 @@
|
||||
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
/* 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/. */
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
let Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("TestJavaAddons");
|
||||
Cu.import("resource://gre/modules/JavaAddonManager.jsm"); /*global JavaAddonManager */
|
||||
Cu.import("resource://gre/modules/Promise.jsm"); /*global Promise */
|
||||
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
|
||||
Cu.import("resource://gre/modules/Messaging.jsm"); /*global Messaging */
|
||||
|
||||
const DEX_FILE = "chrome://roboextender/content/javaaddons-test.apk";
|
||||
const CLASS = "org.mozilla.javaaddons.test.JavaAddonV1";
|
||||
|
||||
const MESSAGE = "JavaAddon:V1";
|
||||
|
||||
add_task(function testFailureCases() {
|
||||
do_print("Loading Java Addon from non-existent class.");
|
||||
let gotError1 = yield JavaAddonManager.classInstanceFromFile(CLASS + "GARBAGE", DEX_FILE)
|
||||
.then((result) => false)
|
||||
.catch((error) => true);
|
||||
do_check_eq(gotError1, true);
|
||||
|
||||
do_print("Loading Java Addon from non-existent DEX file.");
|
||||
let gotError2 = yield JavaAddonManager.classInstanceFromFile(CLASS, DEX_FILE + "GARBAGE")
|
||||
.then((result) => false)
|
||||
.catch((error) => true);
|
||||
do_check_eq(gotError2, true);
|
||||
});
|
||||
|
||||
// Make a request to a dynamically loaded Java Addon; wait for a response.
|
||||
// Then expect the add-on to make a request; respond.
|
||||
// Then expect the add-on to make a second request; use it to verify the response to the first request.
|
||||
add_task(function testJavaAddonV1() {
|
||||
do_print("Loading Java Addon from: " + DEX_FILE);
|
||||
|
||||
let javaAddon = yield JavaAddonManager.classInstanceFromFile(CLASS, DEX_FILE);
|
||||
do_check_neq(javaAddon, null);
|
||||
do_check_neq(javaAddon._guid, null);
|
||||
do_check_eq(javaAddon._classname, CLASS);
|
||||
do_check_eq(javaAddon._loaded, true);
|
||||
|
||||
let messagePromise = Promise.defer();
|
||||
var count = 0;
|
||||
function listener(data) {
|
||||
do_print("Got request initiated from Java Addon: " + data + ", " + typeof(data) + ", " + JSON.stringify(data));
|
||||
count += 1;
|
||||
messagePromise.resolve(); // It's okay to resolve before returning: we'll wait on the verification promise no matter what.
|
||||
return {
|
||||
outputStringKey: "inputStringKey=" + data.inputStringKey,
|
||||
outputIntKey: data.inputIntKey - 1
|
||||
};
|
||||
}
|
||||
javaAddon.addListener(listener, "JavaAddon:V1:Request");
|
||||
|
||||
let verifierPromise = Promise.defer();
|
||||
function verifier(data) {
|
||||
do_print("Got verification request initiated from Java Addon: " + data + ", " + typeof(data) + ", " + JSON.stringify(data));
|
||||
// These values are from the test Java Addon, after being processed by the :Request listener above.
|
||||
do_check_eq(data.outputStringKey, "inputStringKey=raw");
|
||||
do_check_eq(data.outputIntKey, 2);
|
||||
verifierPromise.resolve();
|
||||
return {};
|
||||
}
|
||||
javaAddon.addListener(verifier, "JavaAddon:V1:VerificationRequest");
|
||||
|
||||
let message = {type: MESSAGE, inputStringKey: "test", inputIntKey: 5};
|
||||
do_print("Sending request to Java Addon: " + JSON.stringify(message));
|
||||
let output = yield javaAddon.sendRequestForResult(message);
|
||||
|
||||
do_print("Got response from Java Addon: " + output + ", " + typeof(output) + ", " + JSON.stringify(output));
|
||||
do_check_eq(output.outputStringKey, "inputStringKey=test");
|
||||
do_check_eq(output.outputIntKey, 6);
|
||||
|
||||
// We don't worry about timing out: the harness will (very much later)
|
||||
// kill us if we don't see the expected messages.
|
||||
|
||||
do_print("Waiting for request initiated from Java Addon.");
|
||||
yield messagePromise.promise;
|
||||
do_check_eq(count, 1);
|
||||
|
||||
do_print("Send request for result 2 for request initiated from Java Addon.");
|
||||
|
||||
// The JavaAddon should have removed its listener, so we shouldn't get a response and count should stay the same.
|
||||
let gotError = yield javaAddon.sendRequestForResult(message)
|
||||
.then((result) => false)
|
||||
.catch((error) => true);
|
||||
do_check_eq(gotError, true);
|
||||
do_check_eq(count, 1);
|
||||
|
||||
do_print("Waiting for verification request initiated from Java Addon.");
|
||||
yield verifierPromise.promise;
|
||||
});
|
||||
|
||||
run_next_test();
|
14
mobile/android/tests/javaaddons/AndroidManifest.xml.in
Normal file
14
mobile/android/tests/javaaddons/AndroidManifest.xml.in
Normal file
@ -0,0 +1,14 @@
|
||||
#filter substitution
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.mozilla.javaaddons.test"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0" >
|
||||
|
||||
<uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
|
||||
#ifdef MOZ_ANDROID_MAX_SDK_VERSION
|
||||
android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
|
||||
#endif
|
||||
android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
|
||||
|
||||
</manifest>
|
16
mobile/android/tests/javaaddons/Makefile.in
Normal file
16
mobile/android/tests/javaaddons/Makefile.in
Normal file
@ -0,0 +1,16 @@
|
||||
# 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/.
|
||||
|
||||
ANDROID_APK_NAME := javaaddons-test
|
||||
|
||||
PP_TARGETS += manifest
|
||||
manifest := $(srcdir)/AndroidManifest.xml.in
|
||||
manifest_TARGET := export
|
||||
ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
|
||||
|
||||
ANDROID_EXTRA_JARS := javaaddons-test.jar
|
||||
|
||||
tools libs:: $(ANDROID_APK_NAME).apk
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
16
mobile/android/tests/javaaddons/moz.build
Normal file
16
mobile/android/tests/javaaddons/moz.build
Normal file
@ -0,0 +1,16 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
jar = add_java_jar('javaaddons-test')
|
||||
jar.extra_jars += [
|
||||
TOPOBJDIR + '/mobile/android/javaaddons/javaaddons-1.0.jar',
|
||||
]
|
||||
jar.javac_flags += ['-Xlint:all']
|
||||
jar.sources += [
|
||||
'src/org/mozilla/javaaddons/test/ClassWithNoRecognizedConstructors.java',
|
||||
'src/org/mozilla/javaaddons/test/JavaAddonV0.java',
|
||||
'src/org/mozilla/javaaddons/test/JavaAddonV1.java',
|
||||
]
|
3
mobile/android/tests/javaaddons/res/values/strings.xml
Normal file
3
mobile/android/tests/javaaddons/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">org.mozilla.javaaddons.test</string>
|
||||
</resources>
|
@ -0,0 +1,11 @@
|
||||
/* -*- 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.javaaddons.test;
|
||||
|
||||
public class ClassWithNoRecognizedConstructors {
|
||||
public ClassWithNoRecognizedConstructors(int a, String b, boolean c) {
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/* -*- 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.javaaddons.test;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class JavaAddonV0 implements Handler.Callback {
|
||||
public JavaAddonV0(Map<String, Handler.Callback> callbacks) {
|
||||
callbacks.put("JavaAddon:V0", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(Message message) {
|
||||
Log.i("JavaAddon", "handleMessage " + message.toString());
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/* -*- 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.javaaddons.test;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.javaaddons.JavaAddonInterfaceV1.EventCallback;
|
||||
import org.mozilla.javaaddons.JavaAddonInterfaceV1.EventDispatcher;
|
||||
import org.mozilla.javaaddons.JavaAddonInterfaceV1.EventListener;
|
||||
import org.mozilla.javaaddons.JavaAddonInterfaceV1.RequestCallback;
|
||||
|
||||
public class JavaAddonV1 implements EventListener, RequestCallback {
|
||||
protected final EventDispatcher mDispatcher;
|
||||
|
||||
public JavaAddonV1(Context context, EventDispatcher dispatcher) {
|
||||
mDispatcher = dispatcher;
|
||||
mDispatcher.registerEventListener(this, "JavaAddon:V1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Context context, String event, JSONObject message, EventCallback callback) {
|
||||
Log.i("JavaAddon", "handleMessage: " + event + ", " + message.toString());
|
||||
final JSONObject output = new JSONObject();
|
||||
try {
|
||||
output.put("outputStringKey", "inputStringKey=" + message.getString("inputStringKey"));
|
||||
output.put("outputIntKey", 1 + message.getInt("inputIntKey"));
|
||||
} catch (JSONException e) {
|
||||
// Should never happen; ignore.
|
||||
}
|
||||
// Respond.
|
||||
if (callback != null) {
|
||||
callback.sendSuccess(output);
|
||||
}
|
||||
|
||||
// And send an independent Gecko event.
|
||||
final JSONObject input = new JSONObject();
|
||||
try {
|
||||
input.put("inputStringKey", "raw");
|
||||
input.put("inputIntKey", 3);
|
||||
} catch (JSONException e) {
|
||||
// Should never happen; ignore.
|
||||
}
|
||||
mDispatcher.sendRequestToGecko("JavaAddon:V1:Request", input, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Context context, JSONObject jsonObject) {
|
||||
Log.i("JavaAddon", "onResponse: " + jsonObject.toString());
|
||||
// Unregister event listener, so that the JavaScript side can send a test message and
|
||||
// check it is not handled.
|
||||
mDispatcher.unregisterEventListener(this);
|
||||
mDispatcher.sendRequestToGecko("JavaAddon:V1:VerificationRequest", jsonObject, null);
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@
|
||||
TEST_DIRS += [
|
||||
'background',
|
||||
'browser',
|
||||
'javaaddons', # Must be built before browser/robocop/roboextender.
|
||||
# This is enforced in config/recurse.mk.
|
||||
]
|
||||
|
||||
ANDROID_INSTRUMENTATION_MANIFESTS += ['browser/robocop/robocop.ini']
|
||||
|
Loading…
Reference in New Issue
Block a user