BrowserStartupController.java revision effb81e5f8246d0db0270817048dc992db66e9fb
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.content.browser;
6
7import android.content.Context;
8import android.os.Handler;
9import android.util.Log;
10
11import com.google.common.annotations.VisibleForTesting;
12
13import org.chromium.base.CalledByNative;
14import org.chromium.base.JNINamespace;
15import org.chromium.base.ThreadUtils;
16import org.chromium.base.library_loader.LibraryLoader;
17import org.chromium.base.library_loader.LoaderErrors;
18import org.chromium.base.library_loader.ProcessInitException;
19import org.chromium.content.app.ContentMain;
20
21import java.util.ArrayList;
22import java.util.List;
23
24/**
25 * This class controls how C++ browser main loop is started and ensures it happens only once.
26 *
27 * It supports kicking off the startup sequence in an asynchronous way. Startup can be called as
28 * many times as needed (for instance, multiple activities for the same application), but the
29 * browser process will still only be initialized once. All requests to start the browser will
30 * always get their callback executed; if the browser process has already been started, the callback
31 * is called immediately, else it is called when initialization is complete.
32 *
33 * All communication with this class must happen on the main thread.
34 *
35 * This is a singleton, and stores a reference to the application context.
36 */
37@JNINamespace("content")
38public class BrowserStartupController {
39
40    /**
41     * This provides the interface to the callbacks for successful or failed startup
42     */
43    public interface StartupCallback {
44        void onSuccess(boolean alreadyStarted);
45        void onFailure();
46    }
47
48    private static final String TAG = "BrowserStartupController";
49
50    // Helper constants for {@link StartupCallback#onSuccess}.
51    private static final boolean ALREADY_STARTED = true;
52    private static final boolean NOT_ALREADY_STARTED = false;
53
54    // Helper constants for {@link #executeEnqueuedCallbacks(int, boolean)}.
55    @VisibleForTesting
56    static final int STARTUP_SUCCESS = -1;
57    @VisibleForTesting
58    static final int STARTUP_FAILURE = 1;
59
60    private static BrowserStartupController sInstance;
61
62    private static boolean sBrowserMayStartAsynchronously = false;
63
64    private static void setAsynchronousStartup(boolean enable) {
65        sBrowserMayStartAsynchronously = enable;
66    }
67
68    @VisibleForTesting
69    @CalledByNative
70    static boolean browserMayStartAsynchonously() {
71        return sBrowserMayStartAsynchronously;
72    }
73
74    @VisibleForTesting
75    @CalledByNative
76    static void browserStartupComplete(int result) {
77        if (sInstance != null) {
78            sInstance.executeEnqueuedCallbacks(result, NOT_ALREADY_STARTED);
79        }
80    }
81
82    // A list of callbacks that should be called when the async startup of the browser process is
83    // complete.
84    private final List<StartupCallback> mAsyncStartupCallbacks;
85
86    // The context is set on creation, but the reference is cleared after the browser process
87    // initialization has been started, since it is not needed anymore. This is to ensure the
88    // context is not leaked.
89    private final Context mContext;
90
91    // Whether the async startup of the browser process has started.
92    private boolean mHasStartedInitializingBrowserProcess;
93
94    // Whether the async startup of the browser process is complete.
95    private boolean mStartupDone;
96
97    // Use single-process mode that runs the renderer on a separate thread in
98    // the main application.
99    public static final int MAX_RENDERERS_SINGLE_PROCESS = 0;
100
101    // Cap on the maximum number of renderer processes that can be requested.
102    // This is currently set to account for:
103    //  13: The maximum number of sandboxed processes we have available
104    // - 1: The regular New Tab Page
105    // - 1: The incognito New Tab Page
106    // - 1: A regular incognito tab
107    // - 1: Safety buffer (http://crbug.com/251279)
108    public static final int MAX_RENDERERS_LIMIT =
109            ChildProcessLauncher.MAX_REGISTERED_SANDBOXED_SERVICES - 4;
110
111    // This field is set after startup has been completed based on whether the startup was a success
112    // or not. It is used when later requests to startup come in that happen after the initial set
113    // of enqueued callbacks have been executed.
114    private boolean mStartupSuccess;
115
116    BrowserStartupController(Context context) {
117        mContext = context;
118        mAsyncStartupCallbacks = new ArrayList<StartupCallback>();
119    }
120
121    public static BrowserStartupController get(Context context) {
122        assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread.";
123        ThreadUtils.assertOnUiThread();
124        if (sInstance == null) {
125            sInstance = new BrowserStartupController(context.getApplicationContext());
126        }
127        return sInstance;
128    }
129
130    @VisibleForTesting
131    static BrowserStartupController overrideInstanceForTest(BrowserStartupController controller) {
132        if (sInstance == null) {
133            sInstance = controller;
134        }
135        return sInstance;
136    }
137
138    /**
139     * Start the browser process asynchronously. This will set up a queue of UI thread tasks to
140     * initialize the browser process.
141     * <p/>
142     * Note that this can only be called on the UI thread.
143     *
144     * @param callback the callback to be called when browser startup is complete.
145     */
146    public void startBrowserProcessesAsync(final StartupCallback callback)
147            throws ProcessInitException {
148        assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread.";
149        if (mStartupDone) {
150            // Browser process initialization has already been completed, so we can immediately post
151            // the callback.
152            postStartupCompleted(callback);
153            return;
154        }
155
156        // Browser process has not been fully started yet, so we defer executing the callback.
157        mAsyncStartupCallbacks.add(callback);
158
159        if (!mHasStartedInitializingBrowserProcess) {
160            // This is the first time we have been asked to start the browser process. We set the
161            // flag that indicates that we have kicked off starting the browser process.
162            mHasStartedInitializingBrowserProcess = true;
163
164            prepareToStartBrowserProcess(MAX_RENDERERS_LIMIT);
165
166            setAsynchronousStartup(true);
167            if (contentStart() > 0) {
168                // Failed. The callbacks may not have run, so run them.
169                enqueueCallbackExecution(STARTUP_FAILURE, NOT_ALREADY_STARTED);
170            }
171        }
172    }
173
174    /**
175     * Start the browser process synchronously. If the browser is already being started
176     * asynchronously then complete startup synchronously
177     *
178     * <p/>
179     * Note that this can only be called on the UI thread.
180     *
181     * @param maxRenderers The maximum number of renderer processes the browser may
182     *                      create. Zero for single process mode.
183     * @throws ProcessInitException
184     */
185    public void startBrowserProcessesSync(int maxRenderers) throws ProcessInitException {
186        // If already started skip to checking the result
187        if (!mStartupDone) {
188            if (!mHasStartedInitializingBrowserProcess) {
189                prepareToStartBrowserProcess(maxRenderers);
190            }
191
192            setAsynchronousStartup(false);
193            if (contentStart() > 0) {
194                // Failed. The callbacks may not have run, so run them.
195                enqueueCallbackExecution(STARTUP_FAILURE, NOT_ALREADY_STARTED);
196            }
197        }
198
199        // Startup should now be complete
200        assert mStartupDone;
201        if (!mStartupSuccess) {
202            throw new ProcessInitException(LoaderErrors.LOADER_ERROR_NATIVE_STARTUP_FAILED);
203        }
204    }
205
206    /**
207     * Wrap ContentMain.start() for testing.
208     */
209    @VisibleForTesting
210    int contentStart() {
211        return ContentMain.start();
212    }
213
214    public void addStartupCompletedObserver(StartupCallback callback) {
215        ThreadUtils.assertOnUiThread();
216        if (mStartupDone) {
217            postStartupCompleted(callback);
218        } else {
219            mAsyncStartupCallbacks.add(callback);
220        }
221    }
222
223    private void executeEnqueuedCallbacks(int startupResult, boolean alreadyStarted) {
224        assert ThreadUtils.runningOnUiThread() : "Callback from browser startup from wrong thread.";
225        mStartupDone = true;
226        mStartupSuccess = (startupResult <= 0);
227        for (StartupCallback asyncStartupCallback : mAsyncStartupCallbacks) {
228            if (mStartupSuccess) {
229                asyncStartupCallback.onSuccess(alreadyStarted);
230            } else {
231                asyncStartupCallback.onFailure();
232            }
233        }
234        // We don't want to hold on to any objects after we do not need them anymore.
235        mAsyncStartupCallbacks.clear();
236    }
237
238    // Queue the callbacks to run. Since running the callbacks clears the list it is safe to call
239    // this more than once.
240    private void enqueueCallbackExecution(final int startupFailure, final boolean alreadyStarted) {
241        new Handler().post(new Runnable() {
242            @Override
243            public void run() {
244                executeEnqueuedCallbacks(startupFailure, alreadyStarted);
245            }
246        });
247    }
248
249    private void postStartupCompleted(final StartupCallback callback) {
250        new Handler().post(new Runnable() {
251            @Override
252            public void run() {
253                if (mStartupSuccess) {
254                    callback.onSuccess(ALREADY_STARTED);
255                } else {
256                    callback.onFailure();
257                }
258            }
259        });
260    }
261
262    @VisibleForTesting
263    void prepareToStartBrowserProcess(int maxRendererProcesses) throws ProcessInitException {
264        Log.i(TAG, "Initializing chromium process, renderers=" + maxRendererProcesses);
265
266        // Normally Main.java will have kicked this off asynchronously for Chrome. But other
267        // ContentView apps like tests also need them so we make sure we've extracted resources
268        // here. We can still make it a little async (wait until the library is loaded).
269        ResourceExtractor resourceExtractor = ResourceExtractor.get(mContext);
270        resourceExtractor.startExtractingResources();
271
272        // Normally Main.java will have already loaded the library asynchronously, we only need
273        // to load it here if we arrived via another flow, e.g. bookmark access & sync setup.
274        LibraryLoader.ensureInitialized(mContext);
275
276        // TODO(yfriedman): Remove dependency on a command line flag for this.
277        DeviceUtils.addDeviceSpecificUserAgentSwitch(mContext);
278
279        Context appContext = mContext.getApplicationContext();
280        // Now we really need to have the resources ready.
281        resourceExtractor.waitForCompletion();
282
283        nativeSetCommandLineFlags(maxRendererProcesses,
284                nativeIsPluginEnabled() ? getPlugins() : null);
285        ContentMain.initApplicationContext(appContext);
286    }
287
288    /**
289     * Initialization needed for tests. Mainly used by content browsertests.
290     */
291    public void initChromiumBrowserProcessForTests() {
292        ResourceExtractor resourceExtractor = ResourceExtractor.get(mContext);
293        resourceExtractor.startExtractingResources();
294        resourceExtractor.waitForCompletion();
295
296        // Having a single renderer should be sufficient for tests. We can't have more than
297        // MAX_RENDERERS_LIMIT.
298        nativeSetCommandLineFlags(1 /* maxRenderers */, null);
299    }
300
301    private String getPlugins() {
302        return PepperPluginManager.getPlugins(mContext);
303    }
304
305    private static native void nativeSetCommandLineFlags(int maxRenderProcesses,
306            String pluginDescriptor);
307
308    // Is this an official build of Chrome? Only native code knows for sure. Official build
309    // knowledge is needed very early in process startup.
310    private static native boolean nativeIsOfficialBuild();
311
312    private static native boolean nativeIsPluginEnabled();
313}
314