// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.content.browser; import android.content.Context; import android.os.Handler; import android.util.Log; import com.google.common.annotations.VisibleForTesting; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; import org.chromium.base.ThreadUtils; import org.chromium.content.app.ContentMain; import org.chromium.content.app.LibraryLoader; import org.chromium.content.common.ProcessInitException; import org.chromium.content.common.ResultCodes; import java.util.ArrayList; import java.util.List; /** * This class controls how C++ browser main loop is started and ensures it happens only once. * * It supports kicking off the startup sequence in an asynchronous way. Startup can be called as * many times as needed (for instance, multiple activities for the same application), but the * browser process will still only be initialized once. All requests to start the browser will * always get their callback executed; if the browser process has already been started, the callback * is called immediately, else it is called when initialization is complete. * * All communication with this class must happen on the main thread. * * This is a singleton, and stores a reference to the application context. */ @JNINamespace("content") public class BrowserStartupController { public interface StartupCallback { void onSuccess(boolean alreadyStarted); void onFailure(); } private static final String TAG = "BrowserStartupController"; // Helper constants for {@link StartupCallback#onSuccess}. private static final boolean ALREADY_STARTED = true; private static final boolean NOT_ALREADY_STARTED = false; // Helper constants for {@link #executeEnqueuedCallbacks(int, boolean)}. @VisibleForTesting static final int STARTUP_SUCCESS = -1; @VisibleForTesting static final int STARTUP_FAILURE = 1; private static BrowserStartupController sInstance; private static boolean sBrowserMayStartAsynchronously = false; private static void setAsynchronousStartup(boolean enable) { sBrowserMayStartAsynchronously = enable; } @VisibleForTesting @CalledByNative static boolean browserMayStartAsynchonously() { return sBrowserMayStartAsynchronously; } @VisibleForTesting @CalledByNative static void browserStartupComplete(int result) { if (sInstance != null) { sInstance.executeEnqueuedCallbacks(result, NOT_ALREADY_STARTED); } } // A list of callbacks that should be called when the async startup of the browser process is // complete. private final List mAsyncStartupCallbacks; // The context is set on creation, but the reference is cleared after the browser process // initialization has been started, since it is not needed anymore. This is to ensure the // context is not leaked. private final Context mContext; // Whether the async startup of the browser process has started. private boolean mHasStartedInitializingBrowserProcess; // Whether the async startup of the browser process is complete. private boolean mStartupDone; // Use single-process mode that runs the renderer on a separate thread in // the main application. public static final int MAX_RENDERERS_SINGLE_PROCESS = 0; // Cap on the maximum number of renderer processes that can be requested. // This is currently set to account for: // 13: The maximum number of sandboxed processes we have available // - 1: The regular New Tab Page // - 1: The incognito New Tab Page // - 1: A regular incognito tab // - 1: Safety buffer (http://crbug.com/251279) public static final int MAX_RENDERERS_LIMIT = ChildProcessLauncher.MAX_REGISTERED_SANDBOXED_SERVICES - 4; // This field is set after startup has been completed based on whether the startup was a success // or not. It is used when later requests to startup come in that happen after the initial set // of enqueued callbacks have been executed. private boolean mStartupSuccess; BrowserStartupController(Context context) { mContext = context; mAsyncStartupCallbacks = new ArrayList(); } public static BrowserStartupController get(Context context) { assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread."; ThreadUtils.assertOnUiThread(); if (sInstance == null) { sInstance = new BrowserStartupController(context.getApplicationContext()); } return sInstance; } @VisibleForTesting static BrowserStartupController overrideInstanceForTest(BrowserStartupController controller) { if (sInstance == null) { sInstance = controller; } return sInstance; } /** * Start the browser process asynchronously. This will set up a queue of UI thread tasks to * initialize the browser process. *

* Note that this can only be called on the UI thread. * * @param callback the callback to be called when browser startup is complete. */ public void startBrowserProcessesAsync(final StartupCallback callback) throws ProcessInitException { assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread."; if (mStartupDone) { // Browser process initialization has already been completed, so we can immediately post // the callback. postStartupCompleted(callback); return; } // Browser process has not been fully started yet, so we defer executing the callback. mAsyncStartupCallbacks.add(callback); if (!mHasStartedInitializingBrowserProcess) { // This is the first time we have been asked to start the browser process. We set the // flag that indicates that we have kicked off starting the browser process. mHasStartedInitializingBrowserProcess = true; prepareToStartBrowserProcess(MAX_RENDERERS_LIMIT); setAsynchronousStartup(true); if (contentStart() > 0) { // Failed. The callbacks may not have run, so run them. enqueueCallbackExecution(STARTUP_FAILURE, NOT_ALREADY_STARTED); } } } /** * Start the browser process synchronously. If the browser is already being started * asynchronously then complete startup synchronously * *

* Note that this can only be called on the UI thread. * * @param maxRenderers The maximum number of renderer processes the browser may * create. Zero for single process mode. * @throws ProcessInitException */ public void startBrowserProcessesSync(int maxRenderers) throws ProcessInitException { // If already started skip to checking the result if (!mStartupDone) { if (!mHasStartedInitializingBrowserProcess) { prepareToStartBrowserProcess(maxRenderers); } setAsynchronousStartup(false); if (contentStart() > 0) { // Failed. The callbacks may not have run, so run them. enqueueCallbackExecution(STARTUP_FAILURE, NOT_ALREADY_STARTED); } } // Startup should now be complete assert mStartupDone; if (!mStartupSuccess) { throw new ProcessInitException(ResultCodes.RESULT_CODE_NATIVE_STARTUP_FAILED); } } /** * Wrap ContentMain.start() for testing. */ @VisibleForTesting int contentStart() { return ContentMain.start(); } public void addStartupCompletedObserver(StartupCallback callback) { ThreadUtils.assertOnUiThread(); if (mStartupDone) { postStartupCompleted(callback); } else { mAsyncStartupCallbacks.add(callback); } } private void executeEnqueuedCallbacks(int startupResult, boolean alreadyStarted) { assert ThreadUtils.runningOnUiThread() : "Callback from browser startup from wrong thread."; mStartupDone = true; mStartupSuccess = (startupResult <= 0); for (StartupCallback asyncStartupCallback : mAsyncStartupCallbacks) { if (mStartupSuccess) { asyncStartupCallback.onSuccess(alreadyStarted); } else { asyncStartupCallback.onFailure(); } } // We don't want to hold on to any objects after we do not need them anymore. mAsyncStartupCallbacks.clear(); } // Queue the callbacks to run. Since running the callbacks clears the list it is safe to call // this more than once. private void enqueueCallbackExecution(final int startupFailure, final boolean alreadyStarted) { new Handler().post(new Runnable() { @Override public void run() { executeEnqueuedCallbacks(startupFailure, alreadyStarted); } }); } private void postStartupCompleted(final StartupCallback callback) { new Handler().post(new Runnable() { @Override public void run() { if (mStartupSuccess) { callback.onSuccess(ALREADY_STARTED); } else { callback.onFailure(); } } }); } @VisibleForTesting void prepareToStartBrowserProcess(int maxRendererProcesses) throws ProcessInitException { Log.i(TAG, "Initializing chromium process, renderers=" + maxRendererProcesses); // Normally Main.java will have kicked this off asynchronously for Chrome. But other // ContentView apps like tests also need them so we make sure we've extracted resources // here. We can still make it a little async (wait until the library is loaded). ResourceExtractor resourceExtractor = ResourceExtractor.get(mContext); resourceExtractor.startExtractingResources(); // Normally Main.java will have already loaded the library asynchronously, we only need // to load it here if we arrived via another flow, e.g. bookmark access & sync setup. LibraryLoader.ensureInitialized(); // TODO(yfriedman): Remove dependency on a command line flag for this. DeviceUtils.addDeviceSpecificUserAgentSwitch(mContext); Context appContext = mContext.getApplicationContext(); // Now we really need to have the resources ready. resourceExtractor.waitForCompletion(); nativeSetCommandLineFlags(maxRendererProcesses, nativeIsPluginEnabled() ? getPlugins() : null); ContentMain.initApplicationContext(appContext); } /** * Initialization needed for tests. Mainly used by content browsertests. */ public void initChromiumBrowserProcessForTests() { ResourceExtractor resourceExtractor = ResourceExtractor.get(mContext); resourceExtractor.startExtractingResources(); resourceExtractor.waitForCompletion(); // Having a single renderer should be sufficient for tests. We can't have more than // MAX_RENDERERS_LIMIT. nativeSetCommandLineFlags(1 /* maxRenderers */, null); } private String getPlugins() { return PepperPluginManager.getPlugins(mContext); } private static native void nativeSetCommandLineFlags(int maxRenderProcesses, String pluginDescriptor); // Is this an official build of Chrome? Only native code knows for sure. Official build // knowledge is needed very early in process startup. private static native boolean nativeIsOfficialBuild(); private static native boolean nativeIsPluginEnabled(); }