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 org.chromium.base.CalledByNative;
12import org.chromium.base.JNINamespace;
13import org.chromium.base.ResourceExtractor;
14import org.chromium.base.ThreadUtils;
15import org.chromium.base.VisibleForTesting;
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    // This field is set after startup has been completed based on whether the startup was a success
98    // or not. It is used when later requests to startup come in that happen after the initial set
99    // of enqueued callbacks have been executed.
100    private boolean mStartupSuccess;
101
102    BrowserStartupController(Context context) {
103        mContext = context;
104        mAsyncStartupCallbacks = new ArrayList<StartupCallback>();
105    }
106
107    public static BrowserStartupController get(Context context) {
108        assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread.";
109        ThreadUtils.assertOnUiThread();
110        if (sInstance == null) {
111            sInstance = new BrowserStartupController(context.getApplicationContext());
112        }
113        return sInstance;
114    }
115
116    @VisibleForTesting
117    static BrowserStartupController overrideInstanceForTest(BrowserStartupController controller) {
118        if (sInstance == null) {
119            sInstance = controller;
120        }
121        return sInstance;
122    }
123
124    /**
125     * Start the browser process asynchronously. This will set up a queue of UI thread tasks to
126     * initialize the browser process.
127     * <p/>
128     * Note that this can only be called on the UI thread.
129     *
130     * @param callback the callback to be called when browser startup is complete.
131     */
132    public void startBrowserProcessesAsync(final StartupCallback callback)
133            throws ProcessInitException {
134        assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread.";
135        if (mStartupDone) {
136            // Browser process initialization has already been completed, so we can immediately post
137            // the callback.
138            postStartupCompleted(callback);
139            return;
140        }
141
142        // Browser process has not been fully started yet, so we defer executing the callback.
143        mAsyncStartupCallbacks.add(callback);
144
145        if (!mHasStartedInitializingBrowserProcess) {
146            // This is the first time we have been asked to start the browser process. We set the
147            // flag that indicates that we have kicked off starting the browser process.
148            mHasStartedInitializingBrowserProcess = true;
149
150            prepareToStartBrowserProcess(false);
151
152            setAsynchronousStartup(true);
153            if (contentStart() > 0) {
154                // Failed. The callbacks may not have run, so run them.
155                enqueueCallbackExecution(STARTUP_FAILURE, NOT_ALREADY_STARTED);
156            }
157        }
158    }
159
160    /**
161     * Start the browser process synchronously. If the browser is already being started
162     * asynchronously then complete startup synchronously
163     *
164     * <p/>
165     * Note that this can only be called on the UI thread.
166     *
167     * @param singleProcess true iff the browser should run single-process, ie. keep renderers in
168     *                      the browser process
169     * @throws ProcessInitException
170     */
171    public void startBrowserProcessesSync(boolean singleProcess) throws ProcessInitException {
172        // If already started skip to checking the result
173        if (!mStartupDone) {
174            if (!mHasStartedInitializingBrowserProcess) {
175                prepareToStartBrowserProcess(singleProcess);
176            }
177
178            setAsynchronousStartup(false);
179            if (contentStart() > 0) {
180                // Failed. The callbacks may not have run, so run them.
181                enqueueCallbackExecution(STARTUP_FAILURE, NOT_ALREADY_STARTED);
182            }
183        }
184
185        // Startup should now be complete
186        assert mStartupDone;
187        if (!mStartupSuccess) {
188            throw new ProcessInitException(LoaderErrors.LOADER_ERROR_NATIVE_STARTUP_FAILED);
189        }
190    }
191
192    /**
193     * Wrap ContentMain.start() for testing.
194     */
195    @VisibleForTesting
196    int contentStart() {
197        return ContentMain.start();
198    }
199
200    public void addStartupCompletedObserver(StartupCallback callback) {
201        ThreadUtils.assertOnUiThread();
202        if (mStartupDone) {
203            postStartupCompleted(callback);
204        } else {
205            mAsyncStartupCallbacks.add(callback);
206        }
207    }
208
209    private void executeEnqueuedCallbacks(int startupResult, boolean alreadyStarted) {
210        assert ThreadUtils.runningOnUiThread() : "Callback from browser startup from wrong thread.";
211        mStartupDone = true;
212        mStartupSuccess = (startupResult <= 0);
213        for (StartupCallback asyncStartupCallback : mAsyncStartupCallbacks) {
214            if (mStartupSuccess) {
215                asyncStartupCallback.onSuccess(alreadyStarted);
216            } else {
217                asyncStartupCallback.onFailure();
218            }
219        }
220        // We don't want to hold on to any objects after we do not need them anymore.
221        mAsyncStartupCallbacks.clear();
222    }
223
224    // Queue the callbacks to run. Since running the callbacks clears the list it is safe to call
225    // this more than once.
226    private void enqueueCallbackExecution(final int startupFailure, final boolean alreadyStarted) {
227        new Handler().post(new Runnable() {
228            @Override
229            public void run() {
230                executeEnqueuedCallbacks(startupFailure, alreadyStarted);
231            }
232        });
233    }
234
235    private void postStartupCompleted(final StartupCallback callback) {
236        new Handler().post(new Runnable() {
237            @Override
238            public void run() {
239                if (mStartupSuccess) {
240                    callback.onSuccess(ALREADY_STARTED);
241                } else {
242                    callback.onFailure();
243                }
244            }
245        });
246    }
247
248    @VisibleForTesting
249    void prepareToStartBrowserProcess(boolean singleProcess) throws ProcessInitException {
250        Log.i(TAG, "Initializing chromium process, singleProcess=" + singleProcess);
251
252        // Normally Main.java will have kicked this off asynchronously for Chrome. But other
253        // ContentView apps like tests also need them so we make sure we've extracted resources
254        // here. We can still make it a little async (wait until the library is loaded).
255        ResourceExtractor resourceExtractor = ResourceExtractor.get(mContext);
256        resourceExtractor.startExtractingResources();
257
258        // Normally Main.java will have already loaded the library asynchronously, we only need
259        // to load it here if we arrived via another flow, e.g. bookmark access & sync setup.
260        LibraryLoader.ensureInitialized(mContext, true);
261
262        // TODO(yfriedman): Remove dependency on a command line flag for this.
263        DeviceUtils.addDeviceSpecificUserAgentSwitch(mContext);
264
265        Context appContext = mContext.getApplicationContext();
266        // Now we really need to have the resources ready.
267        resourceExtractor.waitForCompletion();
268
269        nativeSetCommandLineFlags(singleProcess, nativeIsPluginEnabled() ? getPlugins() : null);
270        ContentMain.initApplicationContext(appContext);
271    }
272
273    /**
274     * Initialization needed for tests. Mainly used by content browsertests.
275     */
276    public void initChromiumBrowserProcessForTests() {
277        ResourceExtractor resourceExtractor = ResourceExtractor.get(mContext);
278        resourceExtractor.startExtractingResources();
279        resourceExtractor.waitForCompletion();
280        nativeSetCommandLineFlags(false, null);
281    }
282
283    private String getPlugins() {
284        return PepperPluginManager.getPlugins(mContext);
285    }
286
287    private static native void nativeSetCommandLineFlags(
288            boolean singleProcess, String pluginDescriptor);
289
290    // Is this an official build of Chrome? Only native code knows for sure. Official build
291    // knowledge is needed very early in process startup.
292    private static native boolean nativeIsOfficialBuild();
293
294    private static native boolean nativeIsPluginEnabled();
295}
296