mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-06 00:55:37 +00:00
192182c441
One thing we do in the Fennec CLH is to make a speculative connection based on the URI that's passed in. However, by the time the CLH runs, we're far along into startup, and the advantage of a speculative connection is reduced. This patch implements making speculative connection as a method in GeckoThread, so that Fennec can make a speculative connection without relying on the Fennec CLH.
562 lines
20 KiB
Java
562 lines
20 KiB
Java
/* -*- 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.GeckoEventListener;
|
|
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.DisplayMetrics;
|
|
import android.util.Log;
|
|
|
|
import java.io.IOException;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.util.ArrayList;
|
|
import java.util.Locale;
|
|
import java.util.Queue;
|
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
|
|
public class GeckoThread extends Thread implements GeckoEventListener {
|
|
private static final String LOGTAG = "GeckoThread";
|
|
|
|
@WrapForJNI
|
|
public enum State {
|
|
// After being loaded by class loader.
|
|
INITIAL,
|
|
// After launching Gecko thread
|
|
LAUNCHED,
|
|
// After loading the mozglue library.
|
|
MOZGLUE_READY,
|
|
// After loading the libxul library.
|
|
LIBS_READY,
|
|
// After initializing nsAppShell and JNI calls.
|
|
JNI_READY,
|
|
// After initializing profile and prefs.
|
|
PROFILE_READY,
|
|
// After initializing frontend JS (corresponding to "Gecko:Ready" event)
|
|
RUNNING,
|
|
// After leaving Gecko event loop
|
|
EXITING,
|
|
// After exiting GeckoThread (corresponding to "Gecko:Exited" event)
|
|
EXITED;
|
|
|
|
public boolean is(final State other) {
|
|
return this == other;
|
|
}
|
|
|
|
public boolean isAtLeast(final State other) {
|
|
return ordinal() >= other.ordinal();
|
|
}
|
|
|
|
public boolean isAtMost(final State other) {
|
|
return ordinal() <= other.ordinal();
|
|
}
|
|
|
|
// Inclusive
|
|
public boolean isBetween(final State min, final State max) {
|
|
final int ord = ordinal();
|
|
return ord >= min.ordinal() && ord <= max.ordinal();
|
|
}
|
|
}
|
|
|
|
public static final State MIN_STATE = State.INITIAL;
|
|
public static final State MAX_STATE = State.EXITED;
|
|
|
|
private static final AtomicReference<State> sState = new AtomicReference<>(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 GeckoThread sGeckoThread;
|
|
|
|
@WrapForJNI
|
|
private static final ClassLoader clsLoader = GeckoThread.class.getClassLoader();
|
|
@WrapForJNI
|
|
private static MessageQueue msgQueue;
|
|
|
|
private final String mArgs;
|
|
private final String mAction;
|
|
private final String mUri;
|
|
private final boolean mDebugging;
|
|
|
|
GeckoThread(String args, String action, String uri, boolean debugging) {
|
|
mArgs = args;
|
|
mAction = action;
|
|
mUri = uri;
|
|
mDebugging = debugging;
|
|
|
|
setName("Gecko");
|
|
EventDispatcher.getInstance().registerGeckoThreadListener(this, "Gecko:Ready");
|
|
}
|
|
|
|
public static boolean ensureInit(String args, String action, String uri) {
|
|
return ensureInit(args, action, uri, /* debugging */ false);
|
|
}
|
|
|
|
public static boolean ensureInit(String args, String action, String uri, boolean debugging) {
|
|
ThreadUtils.assertOnUiThread();
|
|
if (isState(State.INITIAL) && sGeckoThread == null) {
|
|
sGeckoThread = new GeckoThread(args, action, uri, debugging);
|
|
return true;
|
|
}
|
|
return 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.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 Class<?>[] argTypes = new Class<?>[args.length];
|
|
for (int i = 0; i < args.length; i++) {
|
|
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[i] = argType;
|
|
}
|
|
final Method method;
|
|
try {
|
|
method = cls.getDeclaredMethod(methodName, argTypes);
|
|
} catch (final NoSuchMethodException e) {
|
|
throw new UnsupportedOperationException("Cannot find method", e);
|
|
}
|
|
|
|
if (QUEUED_CALLS.size() == 0 && isStateAtLeast(state)) {
|
|
invokeMethod(method, obj, args);
|
|
return;
|
|
}
|
|
QUEUED_CALLS.add(new QueuedCall(method, obj, args, 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.
|
|
*/
|
|
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.
|
|
*/
|
|
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 flushQueuedNativeCalls(final State state) {
|
|
synchronized (QUEUED_CALLS) {
|
|
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);
|
|
|
|
if (call.method == null) {
|
|
final GeckoEvent e = (GeckoEvent) call.target;
|
|
GeckoAppShell.notifyGeckoOfEvent(e);
|
|
e.recycle();
|
|
continue;
|
|
}
|
|
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.getContext();
|
|
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 static String getTypeFromAction(String action) {
|
|
if (GeckoApp.ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
|
|
return "-bookmark";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static String addCustomProfileArg(String args) {
|
|
String profileArg = "";
|
|
String guestArg = "";
|
|
if (GeckoAppShell.getGeckoInterface() != null) {
|
|
final GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
|
|
|
|
if (profile.inGuestMode()) {
|
|
try {
|
|
profileArg = " -profile " + profile.getDir().getCanonicalPath();
|
|
} catch (final IOException ioe) {
|
|
Log.e(LOGTAG, "error getting guest profile path", ioe);
|
|
}
|
|
|
|
if (args == null || !args.contains(BrowserApp.GUEST_BROWSING_ARG)) {
|
|
guestArg = " " + BrowserApp.GUEST_BROWSING_ARG;
|
|
}
|
|
} else if (!GeckoProfile.sIsUsingCustomProfile) {
|
|
// If nothing was passed in the intent, make sure the default profile exists and
|
|
// force Gecko to use the default profile for this activity
|
|
profileArg = " -P " + profile.forceCreate().getName();
|
|
}
|
|
}
|
|
|
|
return (args != null ? args : "") + profileArg + guestArg;
|
|
}
|
|
|
|
private String getGeckoArgs(final String apkPath) {
|
|
// First argument is the .apk path
|
|
final StringBuilder args = new StringBuilder(apkPath);
|
|
args.append(" -greomni ").append(apkPath);
|
|
|
|
final String userArgs = addCustomProfileArg(mArgs);
|
|
if (userArgs != null) {
|
|
args.append(' ').append(userArgs);
|
|
}
|
|
|
|
if (mUri != null) {
|
|
args.append(" -url ").append(mUri);
|
|
}
|
|
|
|
final String type = getTypeFromAction(mAction);
|
|
if (type != null) {
|
|
args.append(" ").append(type);
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
|
|
final DisplayMetrics metrics
|
|
= GeckoAppShell.getContext().getResources().getDisplayMetrics();
|
|
args.append(" -width ").append(metrics.widthPixels)
|
|
.append(" -height ").append(metrics.heightPixels);
|
|
|
|
return args.toString();
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
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() {
|
|
GeckoAppShell.registerJavaUiThread();
|
|
}
|
|
});
|
|
|
|
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");
|
|
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);
|
|
}
|
|
|
|
public static void addPendingEvent(final GeckoEvent e) {
|
|
synchronized (QUEUED_CALLS) {
|
|
if (QUEUED_CALLS.size() == 0 && isRunning()) {
|
|
// We may just have switched to running state.
|
|
GeckoAppShell.notifyGeckoOfEvent(e);
|
|
e.recycle();
|
|
} else {
|
|
QUEUED_CALLS.add(new QueuedCall(null, e, null, State.RUNNING));
|
|
}
|
|
}
|
|
}
|
|
|
|
@WrapForJNI
|
|
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;
|
|
}
|
|
|
|
@Override
|
|
public void handleMessage(String event, JSONObject message) {
|
|
if ("Gecko:Ready".equals(event)) {
|
|
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, event);
|
|
setState(State.RUNNING);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.get().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.get().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.get().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.get().isBetween(minState, maxState);
|
|
}
|
|
|
|
@WrapForJNI
|
|
private static void setState(final State newState) {
|
|
ThreadUtils.assertOnGeckoThread();
|
|
sState.set(newState);
|
|
flushQueuedNativeCalls(newState);
|
|
}
|
|
|
|
private static boolean checkAndSetState(final State currentState, final State newState) {
|
|
if (!sState.compareAndSet(currentState, newState)) {
|
|
return false;
|
|
}
|
|
flushQueuedNativeCalls(newState);
|
|
return true;
|
|
}
|
|
|
|
@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);
|
|
}
|
|
}
|