/* -*- 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 android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.StringTokenizer; import org.mozilla.gecko.annotation.RobocopTarget; import org.mozilla.gecko.annotation.WrapForJNI; import org.mozilla.gecko.mozglue.GeckoLoader; import org.mozilla.gecko.process.GeckoProcessManager; import org.mozilla.gecko.process.GeckoProcessType; import org.mozilla.gecko.util.GeckoBundle; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.geckoview.BuildConfig; import org.mozilla.geckoview.GeckoResult; public class GeckoThread extends Thread { private static final String LOGTAG = "GeckoThread"; public enum State implements NativeQueue.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 granting request to shutdown @WrapForJNI EXITING(3), // After granting request to restart @WrapForJNI RESTARTING(3), // After failed lib extraction due to corrupted APK CORRUPT_APK(2), // 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 mRank; State(final int rank) { mRank = rank; } @Override public boolean is(final NativeQueue.State other) { return this == other; } @Override public boolean isAtLeast(final NativeQueue.State other) { if (other instanceof State) { return mRank >= ((State) other).mRank; } return false; } @Override public String toString() { return name(); } } // -1 denotes an invalid or missing File Descriptor private static final int INVALID_FD = -1; private static final NativeQueue sNativeQueue = new NativeQueue(State.INITIAL, State.RUNNING); /* package */ static NativeQueue getNativeQueue() { return sNativeQueue; } public static final State MIN_STATE = State.INITIAL; public static final State MAX_STATE = State.EXITED; private static final Runnable UI_THREAD_CALLBACK = new Runnable() { @Override public void run() { ThreadUtils.assertOnUiThread(); final long nextDelay = runUiThreadCallback(); if (nextDelay >= 0) { ThreadUtils.getUiHandler().postDelayed(this, nextDelay); } } }; private static final GeckoThread INSTANCE = new GeckoThread(); @WrapForJNI private static final ClassLoader clsLoader = GeckoThread.class.getClassLoader(); @WrapForJNI private static MessageQueue msgQueue; @WrapForJNI private static int uiThreadId; private static TelemetryUtils.Timer sInitTimer; private static LinkedList sStateListeners = new LinkedList<>(); // Main process parameters public static final int FLAG_DEBUGGING = 1 << 0; // Debugging mode. public static final int FLAG_PRELOAD_CHILD = 1 << 1; // Preload child during main thread start. public static final int FLAG_ENABLE_NATIVE_CRASHREPORTER = 1 << 2; // Enable native crash reporting. /* package */ static final String EXTRA_ARGS = "args"; private boolean mInitialized; private InitInfo mInitInfo; public static final class ParcelFileDescriptors { public final @Nullable ParcelFileDescriptor prefs; public final @Nullable ParcelFileDescriptor prefMap; public final @NonNull ParcelFileDescriptor ipc; public final @Nullable ParcelFileDescriptor crashReporter; private ParcelFileDescriptors(final Builder builder) { prefs = builder.prefs; prefMap = builder.prefMap; ipc = builder.ipc; crashReporter = builder.crashReporter; } public FileDescriptors detach() { return FileDescriptors.builder() .prefs(detach(prefs)) .prefMap(detach(prefMap)) .ipc(detach(ipc)) .crashReporter(detach(crashReporter)) .build(); } private static int detach(final ParcelFileDescriptor pfd) { if (pfd == null) { return INVALID_FD; } return pfd.detachFd(); } public void close() { close(prefs, prefMap, ipc, crashReporter); } private static void close(final ParcelFileDescriptor... pfds) { for (final ParcelFileDescriptor pfd : pfds) { if (pfd != null) { try { pfd.close(); } catch (final IOException ex) { // Nothing we can do about this really. Log.w(LOGTAG, "Failed to close File Descriptors.", ex); } } } } public static ParcelFileDescriptors from(final FileDescriptors fds) { return ParcelFileDescriptors.builder() .prefs(from(fds.prefs)) .prefMap(from(fds.prefMap)) .ipc(from(fds.ipc)) .crashReporter(from(fds.crashReporter)) .build(); } private static ParcelFileDescriptor from(final int fd) { if (fd == INVALID_FD) { return null; } try { return ParcelFileDescriptor.fromFd(fd); } catch (final IOException ex) { throw new RuntimeException(ex); } } public static Builder builder() { return new Builder(); } public static class Builder { ParcelFileDescriptor prefs; ParcelFileDescriptor prefMap; ParcelFileDescriptor ipc; ParcelFileDescriptor crashReporter; private Builder() {} public ParcelFileDescriptors build() { return new ParcelFileDescriptors(this); } public Builder prefs(final ParcelFileDescriptor prefs) { this.prefs = prefs; return this; } public Builder prefMap(final ParcelFileDescriptor prefMap) { this.prefMap = prefMap; return this; } public Builder ipc(final ParcelFileDescriptor ipc) { this.ipc = ipc; return this; } public Builder crashReporter(final ParcelFileDescriptor crashReporter) { this.crashReporter = crashReporter; return this; } } } public static final class FileDescriptors { final int prefs; final int prefMap; final int ipc; final int crashReporter; private FileDescriptors(final Builder builder) { prefs = builder.prefs; prefMap = builder.prefMap; ipc = builder.ipc; crashReporter = builder.crashReporter; } public static Builder builder() { return new Builder(); } public static class Builder { int prefs = INVALID_FD; int prefMap = INVALID_FD; int ipc = INVALID_FD; int crashReporter = INVALID_FD; private Builder() {} public FileDescriptors build() { return new FileDescriptors(this); } public Builder prefs(final int prefs) { this.prefs = prefs; return this; } public Builder prefMap(final int prefMap) { this.prefMap = prefMap; return this; } public Builder ipc(final int ipc) { this.ipc = ipc; return this; } public Builder crashReporter(final int crashReporter) { this.crashReporter = crashReporter; return this; } } } public static class InitInfo { public final String[] args; public final Bundle extras; public final int flags; public final Map prefs; public final String userSerialNumber; public final boolean xpcshell; public final String outFilePath; public final FileDescriptors fds; private InitInfo(final Builder builder) { final List result = new ArrayList<>(builder.mArgs.length); boolean xpcshell = false; for (final String argument : builder.mArgs) { if ("-xpcshell".equals(argument)) { xpcshell = true; } else { result.add(argument); } } this.xpcshell = xpcshell; args = result.toArray(new String[0]); extras = builder.mExtras != null ? new Bundle(builder.mExtras) : new Bundle(3); flags = builder.mFlags; prefs = builder.mPrefs; userSerialNumber = builder.mUserSerialNumber; outFilePath = xpcshell ? builder.mOutFilePath : null; if (builder.mFds != null) { fds = builder.mFds; } else { fds = FileDescriptors.builder().build(); } } public static Builder builder() { return new Builder(); } public static class Builder { private String[] mArgs; private Bundle mExtras; private int mFlags; private Map mPrefs; private String mUserSerialNumber; private String mOutFilePath; private FileDescriptors mFds; // Prevent direct instantiation private Builder() {} public InitInfo build() { return new InitInfo(this); } public Builder args(final String[] args) { mArgs = args; return this; } public Builder extras(final Bundle extras) { mExtras = extras; return this; } public Builder flags(final int flags) { mFlags = flags; return this; } public Builder prefs(final Map prefs) { mPrefs = prefs; return this; } public Builder userSerialNumber(final String userSerialNumber) { mUserSerialNumber = userSerialNumber; return this; } public Builder outFilePath(final String outFilePath) { mOutFilePath = outFilePath; return this; } public Builder fds(final FileDescriptors fds) { mFds = fds; return this; } } } private static class StateGeckoResult extends GeckoResult { final State state; public StateGeckoResult(final State state) { this.state = state; } } GeckoThread() { // Request more (virtual) stack space to avoid overflows in the CSS frame // constructor. 8 MB matches desktop. super(null, null, "Gecko", 8 * 1024 * 1024); } @WrapForJNI private static boolean isChildProcess() { final InitInfo info = INSTANCE.mInitInfo; return info != null && info.fds.ipc != INVALID_FD; } public static boolean init(final InitInfo info) { return INSTANCE.initInternal(info); } private synchronized boolean initInternal(final InitInfo info) { ThreadUtils.assertOnUiThread(); uiThreadId = Process.myTid(); if (mInitialized) { return false; } sInitTimer = new TelemetryUtils.UptimeTimer("GV_STARTUP_RUNTIME_MS"); mInitInfo = info; mInitialized = true; notifyAll(); return true; } public static boolean launch() { ThreadUtils.assertOnUiThread(); if (checkAndSetState(State.INITIAL, State.LAUNCHED)) { INSTANCE.start(); return true; } return false; } public static boolean isLaunched() { return !isState(State.INITIAL); } @RobocopTarget public static boolean isRunning() { return isState(State.RUNNING); } private static void loadGeckoLibs(final Context context) { GeckoLoader.loadSQLiteLibs(context); GeckoLoader.loadNSSLibs(context); GeckoLoader.loadGeckoLibs(context); setState(State.LIBS_READY); } private static void initGeckoEnvironment() { final Context context = GeckoAppShell.getApplicationContext(); 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); final Configuration config = res.getConfiguration(); config.locale = mappedLocale; res.updateConfiguration(config, null); } if (!isChildProcess()) { GeckoSystemStateListener.getInstance().initialize(context); } loadGeckoLibs(context); } private String[] getMainProcessArgs() { final Context context = GeckoAppShell.getApplicationContext(); final ArrayList args = new ArrayList<>(); // argv[0] is the program name, which for us is the package name. args.add(context.getPackageName()); if (!mInitInfo.xpcshell) { args.add("-greomni"); args.add(context.getPackageResourcePath()); } if (mInitInfo.args != null) { args.addAll(Arrays.asList(mInitInfo.args)); } // Legacy "args" parameter final String extraArgs = mInitInfo.extras.getString(EXTRA_ARGS, null); if (extraArgs != null) { final StringTokenizer st = new StringTokenizer(extraArgs); while (st.hasMoreTokens()) { args.add(st.nextToken()); } } // "argX" parameters for (int i = 0; mInitInfo.extras.containsKey("arg" + i); i++) { final String arg = mInitInfo.extras.getString("arg" + i); args.add(arg); } return args.toArray(new String[0]); } public static @Nullable Bundle getActiveExtras() { synchronized (INSTANCE) { if (!INSTANCE.mInitialized) { return null; } return new Bundle(INSTANCE.mInitInfo.extras); } } public static int getActiveFlags() { synchronized (INSTANCE) { if (!INSTANCE.mInitialized) { return 0; } return INSTANCE.mInitInfo.flags; } } private static ArrayList getEnvFromExtras(final Bundle extras) { if (extras == null) { return new ArrayList<>(); } final ArrayList result = new ArrayList<>(); if (extras != null) { String env = extras.getString("env0"); for (int c = 1; env != null; c++) { if (BuildConfig.DEBUG_BUILD) { Log.d(LOGTAG, "env var: " + env); } result.add(env); env = extras.getString("env" + c); } } return result; } @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; final 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); // Wait until initialization before preparing environment. synchronized (this) { while (!mInitialized) { try { wait(); } catch (final InterruptedException e) { } } } final Context context = GeckoAppShell.getApplicationContext(); final List env = getEnvFromExtras(mInitInfo.extras); // In Gecko, the native crash reporter is enabled by default in opt builds, and // disabled by default in debug builds. if ((mInitInfo.flags & FLAG_ENABLE_NATIVE_CRASHREPORTER) == 0 && !BuildConfig.DEBUG_BUILD) { env.add(0, "MOZ_CRASHREPORTER_DISABLE=1"); } else if ((mInitInfo.flags & FLAG_ENABLE_NATIVE_CRASHREPORTER) != 0 && BuildConfig.DEBUG_BUILD) { env.add(0, "MOZ_CRASHREPORTER=1"); } if (mInitInfo.userSerialNumber != null) { env.add(0, "MOZ_ANDROID_USER_SERIAL_NUMBER=" + mInitInfo.userSerialNumber); } // Start the profiler before even loading mozglue, so we can capture more // things that are happening on the JVM side. maybeStartGeckoProfiler(env); GeckoLoader.loadMozGlue(context); setState(State.MOZGLUE_READY); final boolean isChildProcess = isChildProcess(); GeckoLoader.setupGeckoEnvironment( context, isChildProcess, context.getFilesDir().getPath(), env, mInitInfo.prefs, mInitInfo.xpcshell); initGeckoEnvironment(); if ((mInitInfo.flags & FLAG_PRELOAD_CHILD) != 0) { // Preload the content ("tab") child process. GeckoProcessManager.getInstance().preload(GeckoProcessType.CONTENT); } if ((mInitInfo.flags & FLAG_DEBUGGING) != 0) { try { Thread.sleep(5 * 1000 /* 5 seconds */); } catch (final InterruptedException e) { } } Log.w(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() + " - runGecko"); final String[] args = isChildProcess ? mInitInfo.args : getMainProcessArgs(); if ((mInitInfo.flags & FLAG_DEBUGGING) != 0) { Log.i(LOGTAG, "RunGecko - args = " + TextUtils.join(" ", args)); } // And go. GeckoLoader.nativeRun( args, mInitInfo.fds.prefs, mInitInfo.fds.prefMap, mInitInfo.fds.ipc, mInitInfo.fds.crashReporter, !isChildProcess && mInitInfo.xpcshell, isChildProcess ? null : mInitInfo.outFilePath); // And... we're done. final boolean restarting = isState(State.RESTARTING); setState(State.EXITED); final GeckoBundle data = new GeckoBundle(1); data.putBoolean("restart", restarting); EventDispatcher.getInstance().dispatch("Gecko:Exited", data); // Remove pumpMessageLoop() idle handler Looper.myQueue().removeIdleHandler(idleHandler); if (isChildProcess) { // The child process is completely controlled by Gecko so we don't really need to keep // it alive after Gecko exits. System.exit(0); } } // This may start the gecko profiler early by looking at the environment variables. // Refer to the platform side for more information about the environment variables: // https://searchfox.org/mozilla-central/rev/2f9eacd9d3d995c937b4251a5557d95d494c9be1/tools/profiler/core/platform.cpp#2969-3072 private static void maybeStartGeckoProfiler(final @NonNull List env) { final String startupEnv = "MOZ_PROFILER_STARTUP="; final String intervalEnv = "MOZ_PROFILER_STARTUP_INTERVAL="; final String capacityEnv = "MOZ_PROFILER_STARTUP_ENTRIES="; final String filtersEnv = "MOZ_PROFILER_STARTUP_FILTERS="; boolean isStartupProfiling = false; // Putting default values for now, but they can be overwritten. // Keep these values in sync with profiler defaults. int interval = 1; // The default capacity value is the same with the min capacity, but users // can still enter a different capacity. We also keep this variable to make // sure that the entered value is not below the min capacity. // This value is kept in `scMinimumBufferEntries` variable in the cpp side: // https://searchfox.org/mozilla-central/rev/fa7f47027917a186fb2052dee104cd06c21dd76f/tools/profiler/core/platform.cpp#749 // This number represents 128MiB in entry size. // This is calculated as: // 128 * 1024 * 1024 / 8 = 16777216 final int minCapacity = 16777216; // ~16M entries which is 128MiB in entry size. // Keep this in sync with `PROFILER_DEFAULT_STARTUP_ENTRIES`. // It's computed as 16 * 1024 * 1024 there, which is the same number. int capacity = minCapacity; // Set the default value of no filters - an empty array - which is safer than using null. // If we find a user provided value, this will be overwritten. String[] filters = new String[0]; // Looping the environment variable list to check known variable names. for (final String envItem : env) { if (envItem == null) { continue; } if (envItem.startsWith(startupEnv)) { // Check the environment variable value to see if it's positive. final String value = envItem.substring(startupEnv.length()); if (value.isEmpty() || value.equals("0") || value.equals("n") || value.equals("N")) { // ''/'0'/'n'/'N' values mean do not start the startup profiler. // There's no need to inspect other environment variables, // so let's break out of the loop break; } isStartupProfiling = true; } else if (envItem.startsWith(intervalEnv)) { // Parse the interval environment variable if present final String value = envItem.substring(intervalEnv.length()); try { final int intValue = Integer.parseInt(value); interval = Math.max(intValue, interval); } catch (final NumberFormatException err) { // Failed to parse. Do nothing and just use the default value. } } else if (envItem.startsWith(capacityEnv)) { // Parse the capacity environment variable if present final String value = envItem.substring(capacityEnv.length()); try { final int intValue = Integer.parseInt(value); // See `scMinimumBufferEntries` variable for this value on the platform side. capacity = Math.max(intValue, minCapacity); } catch (final NumberFormatException err) { // Failed to parse. Do nothing and just use the default value. } } else if (envItem.startsWith(filtersEnv)) { filters = envItem.substring(filtersEnv.length()).split(","); } } if (isStartupProfiling) { GeckoJavaSampler.start(filters, interval, capacity); } } @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 sNativeQueue.getState().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 sNativeQueue.getState().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 state.isAtLeast(sNativeQueue.getState()); } /** * 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 isStateAtLeast(minState) && isStateAtMost(maxState); } @WrapForJNI(calledFrom = "gecko") private static void setState(final State newState) { checkAndSetState(null, newState); } @WrapForJNI(calledFrom = "gecko") private static boolean checkAndSetState(final State expectedState, final State newState) { final boolean result = sNativeQueue.checkAndSetState(expectedState, newState); if (result) { Log.d(LOGTAG, "State changed to " + newState); if (sInitTimer != null && isRunning()) { sInitTimer.stop(); sInitTimer = null; } notifyStateListeners(); } return result; } @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); } @UiThread public static GeckoResult waitForState(final State state) { final StateGeckoResult result = new StateGeckoResult(state); if (isStateAtLeast(state)) { result.complete(null); return result; } synchronized (sStateListeners) { sStateListeners.add(result); } return result; } private static void notifyStateListeners() { synchronized (sStateListeners) { final LinkedList newListeners = new LinkedList<>(); for (final StateGeckoResult result : sStateListeners) { if (!isStateAtLeast(result.state)) { newListeners.add(result); continue; } result.complete(null); } sStateListeners = newListeners; } } @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); } } @WrapForJNI(calledFrom = "ui") /* package */ static native long runUiThreadCallback(); @WrapForJNI(dispatchTo = "gecko") public static native void forceQuit(); @WrapForJNI(dispatchTo = "gecko") public static native void crash(); @WrapForJNI private static void requestUiThreadCallback(final long delay) { ThreadUtils.getUiHandler().postDelayed(UI_THREAD_CALLBACK, delay); } /** 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) { sNativeQueue.queueUntilReady(cls, methodName, args); } /** 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) { sNativeQueue.queueUntilReady(obj, methodName, args); } /** Queue a call to the given instance method until Gecko is in the RUNNING state. */ public static void queueNativeCallUntil( final State state, final Object obj, final String methodName, final Object... args) { sNativeQueue.queueUntil(state, obj, methodName, args); } /** Queue a call to the given static method until Gecko is in the RUNNING state. */ public static void queueNativeCallUntil( final State state, final Class cls, final String methodName, final Object... args) { sNativeQueue.queueUntil(state, cls, methodName, args); } }