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