summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java677
1 files changed, 677 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
new file mode 100644
index 000000000..b57222a31
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
@@ -0,0 +1,677 @@
+/* -*- 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;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+public class GeckoThread extends Thread {
+ private static final String LOGTAG = "GeckoThread";
+
+ public enum State {
+ // After being loaded by class loader.
+ @WrapForJNI INITIAL(0),
+ // After launching Gecko thread
+ @WrapForJNI LAUNCHED(1),
+ // After loading the mozglue library.
+ @WrapForJNI MOZGLUE_READY(2),
+ // After loading the libxul library.
+ @WrapForJNI LIBS_READY(3),
+ // After initializing nsAppShell and JNI calls.
+ @WrapForJNI JNI_READY(4),
+ // After initializing profile and prefs.
+ @WrapForJNI PROFILE_READY(5),
+ // After initializing frontend JS
+ @WrapForJNI RUNNING(6),
+ // After leaving Gecko event loop
+ @WrapForJNI EXITING(3),
+ // After exiting GeckoThread (corresponding to "Gecko:Exited" event)
+ @WrapForJNI EXITED(0);
+
+ /* The rank is an arbitrary value reflecting the amount of components or features
+ * that are available for use. During startup and up to the RUNNING state, the
+ * rank value increases because more components are initialized and available for
+ * use. During shutdown and up to the EXITED state, the rank value decreases as
+ * components are shut down and become unavailable. EXITING has the same rank as
+ * LIBS_READY because both states have a similar amount of components available.
+ */
+ private final int rank;
+
+ private State(int rank) {
+ this.rank = rank;
+ }
+
+ public boolean is(final State other) {
+ return this == other;
+ }
+
+ public boolean isAtLeast(final State other) {
+ return this.rank >= other.rank;
+ }
+
+ public boolean isAtMost(final State other) {
+ return this.rank <= other.rank;
+ }
+
+ // Inclusive
+ public boolean isBetween(final State min, final State max) {
+ return this.rank >= min.rank && this.rank <= max.rank;
+ }
+ }
+
+ public static final State MIN_STATE = State.INITIAL;
+ public static final State MAX_STATE = State.EXITED;
+
+ private static volatile State sState = State.INITIAL;
+
+ private static class QueuedCall {
+ public Method method;
+ public Object target;
+ public Object[] args;
+ public State state;
+
+ public QueuedCall(final Method method, final Object target,
+ final Object[] args, final State state) {
+ this.method = method;
+ this.target = target;
+ this.args = args;
+ this.state = state;
+ }
+ }
+
+ private static final int QUEUED_CALLS_COUNT = 16;
+ private static final ArrayList<QueuedCall> QUEUED_CALLS = new ArrayList<>(QUEUED_CALLS_COUNT);
+
+ private static final Runnable UI_THREAD_CALLBACK = new Runnable() {
+ @Override
+ public void run() {
+ ThreadUtils.assertOnUiThread();
+ long nextDelay = runUiThreadCallback();
+ if (nextDelay >= 0) {
+ ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
+ }
+ }
+ };
+
+ private static GeckoThread sGeckoThread;
+
+ @WrapForJNI
+ private static final ClassLoader clsLoader = GeckoThread.class.getClassLoader();
+ @WrapForJNI
+ private static MessageQueue msgQueue;
+
+ private GeckoProfile mProfile;
+
+ private final String mArgs;
+ private final String mAction;
+ private final boolean mDebugging;
+
+ GeckoThread(GeckoProfile profile, String args, String action, boolean debugging) {
+ mProfile = profile;
+ mArgs = args;
+ mAction = action;
+ mDebugging = debugging;
+
+ setName("Gecko");
+ }
+
+ public static boolean init(GeckoProfile profile, String args, String action, boolean debugging) {
+ ThreadUtils.assertOnUiThread();
+ if (isState(State.INITIAL) && sGeckoThread == null) {
+ sGeckoThread = new GeckoThread(profile, args, action, debugging);
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean canUseProfile(final Context context, final GeckoProfile profile,
+ final String profileName, final File profileDir) {
+ if (profileDir != null && !profileDir.isDirectory()) {
+ return false;
+ }
+
+ if (profile == null) {
+ // We haven't initialized; any profile is okay as long as we follow the guest mode setting.
+ return GeckoProfile.shouldUseGuestMode(context) ==
+ GeckoProfile.isGuestProfile(context, profileName, profileDir);
+ }
+
+ // We already initialized and have a profile; see if it matches ours.
+ try {
+ return profileDir == null ? profileName.equals(profile.getName()) :
+ profile.getDir().getCanonicalPath().equals(profileDir.getCanonicalPath());
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Cannot compare profile " + profileName);
+ return false;
+ }
+ }
+
+ public static boolean canUseProfile(final String profileName, final File profileDir) {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Null profile name");
+ }
+ return canUseProfile(GeckoAppShell.getApplicationContext(), getActiveProfile(),
+ profileName, profileDir);
+ }
+
+ public static boolean initWithProfile(final String profileName, final File profileDir) {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Null profile name");
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+ final GeckoProfile profile = getActiveProfile();
+
+ if (!canUseProfile(context, profile, profileName, profileDir)) {
+ // Profile is incompatible with current profile.
+ return false;
+ }
+
+ if (profile != null) {
+ // We already have a compatible profile.
+ return true;
+ }
+
+ // We haven't initialized yet; okay to initialize now.
+ return init(GeckoProfile.get(context, profileName, profileDir),
+ /* args */ null, /* action */ null, /* debugging */ false);
+ }
+
+ public static boolean launch() {
+ ThreadUtils.assertOnUiThread();
+ if (checkAndSetState(State.INITIAL, State.LAUNCHED)) {
+ sGeckoThread.start();
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isLaunched() {
+ return !isState(State.INITIAL);
+ }
+
+ @RobocopTarget
+ public static boolean isRunning() {
+ return isState(State.RUNNING);
+ }
+
+ // Invoke the given Method and handle checked Exceptions.
+ private static void invokeMethod(final Method method, final Object obj, final Object[] args) {
+ try {
+ method.setAccessible(true);
+ method.invoke(obj, args);
+ } catch (final IllegalAccessException e) {
+ throw new IllegalStateException("Unexpected exception", e);
+ } catch (final InvocationTargetException e) {
+ throw new UnsupportedOperationException("Cannot make call", e.getCause());
+ }
+ }
+
+ // Queue a call to the given method.
+ private static void queueNativeCallLocked(final Class<?> cls, final String methodName,
+ final Object obj, final Object[] args,
+ final State state) {
+ final ArrayList<Class<?>> argTypes = new ArrayList<>(args.length);
+ final ArrayList<Object> argValues = new ArrayList<>(args.length);
+
+ for (int i = 0; i < args.length; i++) {
+ if (args[i] instanceof Class) {
+ argTypes.add((Class<?>) args[i]);
+ argValues.add(args[++i]);
+ continue;
+ }
+ Class<?> argType = args[i].getClass();
+ if (argType == Boolean.class) argType = Boolean.TYPE;
+ else if (argType == Byte.class) argType = Byte.TYPE;
+ else if (argType == Character.class) argType = Character.TYPE;
+ else if (argType == Double.class) argType = Double.TYPE;
+ else if (argType == Float.class) argType = Float.TYPE;
+ else if (argType == Integer.class) argType = Integer.TYPE;
+ else if (argType == Long.class) argType = Long.TYPE;
+ else if (argType == Short.class) argType = Short.TYPE;
+ argTypes.add(argType);
+ argValues.add(args[i]);
+ }
+ final Method method;
+ try {
+ method = cls.getDeclaredMethod(
+ methodName, argTypes.toArray(new Class<?>[argTypes.size()]));
+ } catch (final NoSuchMethodException e) {
+ throw new IllegalArgumentException("Cannot find method", e);
+ }
+
+ if (!Modifier.isNative(method.getModifiers())) {
+ // As a precaution, we disallow queuing non-native methods. Queuing non-native
+ // methods is dangerous because the method could end up being called on either
+ // the original thread or the Gecko thread depending on timing. Native methods
+ // usually handle this by posting an event to the Gecko thread automatically,
+ // but there is no automatic mechanism for non-native methods.
+ throw new UnsupportedOperationException("Not allowed to queue non-native methods");
+ }
+
+ if (isStateAtLeast(state)) {
+ invokeMethod(method, obj, argValues.toArray());
+ return;
+ }
+
+ QUEUED_CALLS.add(new QueuedCall(
+ method, obj, argValues.toArray(), state));
+ }
+
+ /**
+ * Queue a call to the given static method until Gecko is in the given state.
+ *
+ * @param state The Gecko state in which the native call could be executed.
+ * Default is State.RUNNING, which means this queued call will
+ * run when Gecko is at or after RUNNING state.
+ * @param cls Class that declares the static method.
+ * @param methodName Name of the static method.
+ * @param args Args to call the static method with; to specify a parameter type,
+ * pass in a Class instance first, followed by the value.
+ */
+ public static void queueNativeCallUntil(final State state, final Class<?> cls,
+ final String methodName, final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(cls, methodName, null, args, state);
+ }
+ }
+
+ /**
+ * Queue a call to the given static method until Gecko is in the RUNNING state.
+ */
+ public static void queueNativeCall(final Class<?> cls, final String methodName,
+ final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(cls, methodName, null, args, State.RUNNING);
+ }
+ }
+
+ /**
+ * Queue a call to the given instance method until Gecko is in the given state.
+ *
+ * @param state The Gecko state in which the native call could be executed.
+ * @param obj Object that declares the instance method.
+ * @param methodName Name of the instance method.
+ * @param args Args to call the instance method with; to specify a parameter type,
+ * pass in a Class instance first, followed by the value.
+ */
+ public static void queueNativeCallUntil(final State state, final Object obj,
+ final String methodName, final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, state);
+ }
+ }
+
+ /**
+ * Queue a call to the given instance method until Gecko is in the RUNNING state.
+ */
+ public static void queueNativeCall(final Object obj, final String methodName,
+ final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, State.RUNNING);
+ }
+ }
+
+ // Run all queued methods
+ private static void flushQueuedNativeCallsLocked(final State state) {
+ int lastSkipped = -1;
+ for (int i = 0; i < QUEUED_CALLS.size(); i++) {
+ final QueuedCall call = QUEUED_CALLS.get(i);
+ if (call == null) {
+ // We already handled the call.
+ continue;
+ }
+ if (!state.isAtLeast(call.state)) {
+ // The call is not ready yet; skip it.
+ lastSkipped = i;
+ continue;
+ }
+ // Mark as handled.
+ QUEUED_CALLS.set(i, null);
+
+ invokeMethod(call.method, call.target, call.args);
+ }
+ if (lastSkipped < 0) {
+ // We're done here; release the memory
+ QUEUED_CALLS.clear();
+ QUEUED_CALLS.trimToSize();
+ } else if (lastSkipped < QUEUED_CALLS.size() - 1) {
+ // We skipped some; free up null entries at the end,
+ // but keep all the previous entries for later.
+ QUEUED_CALLS.subList(lastSkipped + 1, QUEUED_CALLS.size()).clear();
+ }
+ }
+
+ private static String initGeckoEnvironment() {
+ final Context context = GeckoAppShell.getApplicationContext();
+ GeckoLoader.loadMozGlue(context);
+ setState(State.MOZGLUE_READY);
+
+ final Locale locale = Locale.getDefault();
+ final Resources res = context.getResources();
+ if (locale.toString().equalsIgnoreCase("zh_hk")) {
+ final Locale mappedLocale = Locale.TRADITIONAL_CHINESE;
+ Locale.setDefault(mappedLocale);
+ Configuration config = res.getConfiguration();
+ config.locale = mappedLocale;
+ res.updateConfiguration(config, null);
+ }
+
+ String[] pluginDirs = null;
+ try {
+ pluginDirs = GeckoAppShell.getPluginDirectories();
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Caught exception getting plugin dirs.", e);
+ }
+
+ final String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.setupGeckoEnvironment(context, pluginDirs, context.getFilesDir().getPath());
+
+ GeckoLoader.loadSQLiteLibs(context, resourcePath);
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+ GeckoLoader.loadGeckoLibs(context, resourcePath);
+ setState(State.LIBS_READY);
+
+ return resourcePath;
+ }
+
+ private String addCustomProfileArg(String args) {
+ String profileArg = "";
+
+ // Make sure a profile exists.
+ final GeckoProfile profile = getProfile();
+ profile.getDir(); // call the lazy initializer
+
+ // If args don't include the profile, make sure it's included.
+ if (args == null || !args.matches(".*\\B-(P|profile)\\s+\\S+.*")) {
+ if (profile.isCustomProfile()) {
+ profileArg = " -profile " + profile.getDir().getAbsolutePath();
+ } else {
+ profileArg = " -P " + profile.getName();
+ }
+ }
+
+ return (args != null ? args : "") + profileArg;
+ }
+
+ private String getGeckoArgs(final String apkPath) {
+ // argv[0] is the program name, which for us is the package name.
+ final Context context = GeckoAppShell.getApplicationContext();
+ final StringBuilder args = new StringBuilder(context.getPackageName());
+ args.append(" -greomni ").append(apkPath);
+
+ final String userArgs = addCustomProfileArg(mArgs);
+ if (userArgs != null) {
+ args.append(' ').append(userArgs);
+ }
+
+ // In un-official builds, we want to load Javascript resources fresh
+ // with each build. In official builds, the startup cache is purged by
+ // the buildid mechanism, but most un-official builds don't bump the
+ // buildid, so we purge here instead.
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ Log.w(LOGTAG, "STARTUP PERFORMANCE WARNING: un-official build: purging the " +
+ "startup (JavaScript) caches.");
+ args.append(" -purgecaches");
+ }
+
+ return args.toString();
+ }
+
+ public static GeckoProfile getActiveProfile() {
+ if (sGeckoThread == null) {
+ return null;
+ }
+ final GeckoProfile profile = sGeckoThread.mProfile;
+ if (profile != null) {
+ return profile;
+ }
+ return sGeckoThread.getProfile();
+ }
+
+ public synchronized GeckoProfile getProfile() {
+ if (mProfile == null) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ mProfile = GeckoProfile.initFromArgs(context, mArgs);
+ }
+ return mProfile;
+ }
+
+ @Override
+ public void run() {
+ Log.i(LOGTAG, "preparing to run Gecko");
+
+ Looper.prepare();
+ GeckoThread.msgQueue = Looper.myQueue();
+ ThreadUtils.sGeckoThread = this;
+ ThreadUtils.sGeckoHandler = new Handler();
+
+ // Preparation for pumpMessageLoop()
+ final MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
+ @Override public boolean queueIdle() {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+ Message idleMsg = Message.obtain(geckoHandler);
+ // Use |Message.obj == GeckoHandler| to identify our "queue is empty" message
+ idleMsg.obj = geckoHandler;
+ geckoHandler.sendMessageAtFrontOfQueue(idleMsg);
+ // Keep this IdleHandler
+ return true;
+ }
+ };
+ Looper.myQueue().addIdleHandler(idleHandler);
+
+ if (mDebugging) {
+ try {
+ Thread.sleep(5 * 1000 /* 5 seconds */);
+ } catch (final InterruptedException e) {
+ }
+ }
+
+ final String args = getGeckoArgs(initGeckoEnvironment());
+
+ // This can only happen after the call to initGeckoEnvironment
+ // above, because otherwise the JNI code hasn't been loaded yet.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override public void run() {
+ registerUiThread();
+ }
+ });
+
+ Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - runGecko");
+
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ Log.i(LOGTAG, "RunGecko - args = " + args);
+ }
+
+ // And go.
+ GeckoLoader.nativeRun(args);
+
+ // And... we're done.
+ setState(State.EXITED);
+
+ try {
+ final JSONObject msg = new JSONObject();
+ msg.put("type", "Gecko:Exited");
+ GeckoAppShell.getGeckoInterface().getAppEventDispatcher().dispatchEvent(msg, null);
+ EventDispatcher.getInstance().dispatchEvent(msg, null);
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "unable to dispatch event", e);
+ }
+
+ // Remove pumpMessageLoop() idle handler
+ Looper.myQueue().removeIdleHandler(idleHandler);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean pumpMessageLoop(final Message msg) {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+
+ if (msg.obj == geckoHandler && msg.getTarget() == geckoHandler) {
+ // Our "queue is empty" message; see runGecko()
+ return false;
+ }
+
+ if (msg.getTarget() == null) {
+ Looper.myLooper().quit();
+ } else {
+ msg.getTarget().dispatchMessage(msg);
+ }
+
+ return true;
+ }
+
+ /**
+ * Check that the current Gecko thread state matches the given state.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isState(final State state) {
+ return sState.is(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or further along,
+ * according to the order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtLeast(final State state) {
+ return sState.isAtLeast(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or prior,
+ * according to the order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtMost(final State state) {
+ return sState.isAtMost(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state falls into an inclusive range of states,
+ * according to the order defined in the State enum.
+ *
+ * @param minState Lower range of allowable states
+ * @param maxState Upper range of allowable states
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateBetween(final State minState, final State maxState) {
+ return sState.isBetween(minState, maxState);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setState(final State newState) {
+ ThreadUtils.assertOnGeckoThread();
+ synchronized (QUEUED_CALLS) {
+ flushQueuedNativeCallsLocked(newState);
+ sState = newState;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean checkAndSetState(final State currentState, final State newState) {
+ synchronized (QUEUED_CALLS) {
+ if (sState == currentState) {
+ flushQueuedNativeCallsLocked(newState);
+ sState = newState;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @WrapForJNI(stubName = "SpeculativeConnect")
+ private static native void speculativeConnectNative(String uri);
+
+ public static void speculativeConnect(final String uri) {
+ // This is almost always called before Gecko loads, so we don't
+ // bother checking here if Gecko is actually loaded or not.
+ // Speculative connection depends on proxy settings,
+ // so the earliest it can happen is after profile is ready.
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "speculativeConnectNative", uri);
+ }
+
+ @WrapForJNI @RobocopTarget
+ public static native void waitOnGecko();
+
+ @WrapForJNI(stubName = "OnPause", dispatchTo = "gecko")
+ private static native void nativeOnPause();
+
+ public static void onPause() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnPause();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "nativeOnPause");
+ }
+ }
+
+ @WrapForJNI(stubName = "OnResume", dispatchTo = "gecko")
+ private static native void nativeOnResume();
+
+ public static void onResume() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnResume();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "nativeOnResume");
+ }
+ }
+
+ @WrapForJNI(stubName = "CreateServices", dispatchTo = "gecko")
+ private static native void nativeCreateServices(String category, String data);
+
+ public static void createServices(final String category, final String data) {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeCreateServices(category, data);
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class, "nativeCreateServices",
+ String.class, category, String.class, data);
+ }
+ }
+
+ // Implemented in mozglue/android/APKOpen.cpp.
+ /* package */ static native void registerUiThread();
+
+ @WrapForJNI(calledFrom = "ui")
+ /* package */ static native long runUiThreadCallback();
+
+ @WrapForJNI
+ private static void requestUiThreadCallback(long delay) {
+ ThreadUtils.getUiHandler().postDelayed(UI_THREAD_CALLBACK, delay);
+ }
+}