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 | |
5 | package org.chromium.content.browser; |
6 | |
7 | import android.content.Context; |
8 | import android.os.Handler; |
9 | import android.util.Log; |
10 | |
11 | import com.google.common.annotations.VisibleForTesting; |
12 | |
13 | import org.chromium.base.CalledByNative; |
14 | import org.chromium.base.JNINamespace; |
15 | import org.chromium.base.ThreadUtils; |
16 | import org.chromium.content.common.ProcessInitException; |
17 | |
18 | import java.util.ArrayList; |
19 | import java.util.List; |
20 | |
21 | /** |
22 | * This class controls how C++ browser main loop is started and ensures it happens only once. |
23 | * |
24 | * It supports kicking off the startup sequence in an asynchronous way. Startup can be called as |
25 | * many times as needed (for instance, multiple activities for the same application), but the |
26 | * browser process will still only be initialized once. All requests to start the browser will |
27 | * always get their callback executed; if the browser process has already been started, the callback |
28 | * is called immediately, else it is called when initialization is complete. |
29 | * |
30 | * All communication with this class must happen on the main thread. |
31 | * |
32 | * This is a singleton, and stores a reference to the application context. |
33 | */ |
34 | @JNINamespace("content") |
35 | public class BrowserStartupController { |
36 | |
37 | public interface StartupCallback { |
38 | void onSuccess(boolean alreadyStarted); |
39 | void onFailure(); |
40 | } |
41 | |
42 | private static final String TAG = "BrowserStartupController"; |
43 | |
44 | // Helper constants for {@link StartupCallback#onSuccess}. |
45 | private static final boolean ALREADY_STARTED = true; |
46 | private static final boolean NOT_ALREADY_STARTED = false; |
47 | |
48 | // Helper constants for {@link #executeEnqueuedCallbacks(int, boolean)}. |
49 | @VisibleForTesting |
50 | static final int STARTUP_SUCCESS = -1; |
51 | @VisibleForTesting |
52 | static final int STARTUP_FAILURE = 1; |
53 | |
54 | private static BrowserStartupController sInstance; |
55 | |
56 | private static boolean sBrowserMayStartAsynchronously = false; |
57 | |
58 | private static void setAsynchronousStartupConfig() { |
59 | sBrowserMayStartAsynchronously = true; |
60 | } |
61 | |
62 | @CalledByNative |
63 | private static boolean browserMayStartAsynchonously() { |
64 | return sBrowserMayStartAsynchronously; |
65 | } |
66 | |
67 | @VisibleForTesting |
68 | @CalledByNative |
69 | static void browserStartupComplete(int result) { |
70 | if (sInstance != null) { |
71 | sInstance.executeEnqueuedCallbacks(result, NOT_ALREADY_STARTED); |
72 | } |
73 | } |
74 | |
75 | // A list of callbacks that should be called when the async startup of the browser process is |
76 | // complete. |
77 | private final List<StartupCallback> mAsyncStartupCallbacks; |
78 | |
79 | // The context is set on creation, but the reference is cleared after the browser process |
80 | // initialization has been started, since it is not needed anymore. This is to ensure the |
81 | // context is not leaked. |
82 | private Context mContext; |
83 | |
84 | // Whether the async startup of the browser process has started. |
85 | private boolean mHasStartedInitializingBrowserProcess; |
86 | |
87 | // Whether the async startup of the browser process is complete. |
88 | private boolean mAsyncStartupDone; |
89 | |
90 | // This field is set after startup has been completed based on whether the startup was a success |
91 | // or not. It is used when later requests to startup come in that happen after the initial set |
92 | // of enqueued callbacks have been executed. |
93 | private boolean mStartupSuccess; |
94 | |
95 | BrowserStartupController(Context context) { |
96 | mContext = context; |
97 | mAsyncStartupCallbacks = new ArrayList<StartupCallback>(); |
98 | } |
99 | |
100 | public static BrowserStartupController get(Context context) { |
101 | assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread."; |
102 | ThreadUtils.assertOnUiThread(); |
103 | if (sInstance == null) { |
104 | sInstance = new BrowserStartupController(context.getApplicationContext()); |
105 | } |
106 | return sInstance; |
107 | } |
108 | |
109 | @VisibleForTesting |
110 | static BrowserStartupController overrideInstanceForTest(BrowserStartupController controller) { |
111 | if (sInstance == null) { |
112 | sInstance = controller; |
113 | } |
114 | return sInstance; |
115 | } |
116 | |
117 | /** |
118 | * Start the browser process asynchronously. This will set up a queue of UI thread tasks to |
119 | * initialize the browser process. |
120 | * <p/> |
121 | * Note that this can only be called on the UI thread. |
122 | * |
123 | * @param callback the callback to be called when browser startup is complete. |
124 | */ |
125 | public void startBrowserProcessesAsync(final StartupCallback callback) { |
126 | assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread."; |
127 | if (mAsyncStartupDone) { |
128 | // Browser process initialization has already been completed, so we can immediately post |
129 | // the callback. |
130 | postStartupCompleted(callback); |
131 | return; |
132 | } |
133 | |
134 | // Browser process has not been fully started yet, so we defer executing the callback. |
135 | mAsyncStartupCallbacks.add(callback); |
136 | |
137 | if (!mHasStartedInitializingBrowserProcess) { |
138 | // This is the first time we have been asked to start the browser process. We set the |
139 | // flag that indicates that we have kicked off starting the browser process. |
140 | mHasStartedInitializingBrowserProcess = true; |
141 | |
142 | enableAsynchronousStartup(); |
143 | |
144 | // Try to initialize the Android browser process. |
145 | tryToInitializeBrowserProcess(); |
146 | } |
147 | } |
148 | |
149 | private void tryToInitializeBrowserProcess() { |
150 | try { |
151 | assert mContext != null; |
152 | boolean wasAlreadyInitialized = initializeAndroidBrowserProcess(); |
153 | // The context is not needed anymore, so clear the member field to not leak. |
154 | mContext = null; |
155 | if (wasAlreadyInitialized) { |
156 | // Something has already initialized the browser process before we got to setup the |
157 | // async startup. This means that we will never get a callback, so manually call |
158 | // them now, and just assume that the startup was successful. |
159 | Log.w(TAG, "Browser process was initialized without BrowserStartupController"); |
160 | enqueueCallbackExecution(STARTUP_SUCCESS, ALREADY_STARTED); |
161 | } |
162 | } catch (ProcessInitException e) { |
163 | Log.e(TAG, "Unable to start browser process.", e); |
164 | // ProcessInitException could mean one of two things: |
165 | // 1) The LibraryLoader failed. |
166 | // 2) ContentMain failed to start. |
167 | // It is unclear whether the browser tasks have already been started, and in case they |
168 | // have not, post a message to execute all the callbacks. Whichever call to |
169 | // executeEnqueuedCallbacks comes first will trigger the callbacks, but since the list |
170 | // of callbacks is then cleared, they will only be called once. |
171 | enqueueCallbackExecution(STARTUP_FAILURE, NOT_ALREADY_STARTED); |
172 | } |
173 | } |
174 | |
175 | public void addStartupCompletedObserver(StartupCallback callback) { |
176 | ThreadUtils.assertOnUiThread(); |
177 | if (mAsyncStartupDone) |
178 | postStartupCompleted(callback); |
179 | else |
180 | mAsyncStartupCallbacks.add(callback); |
181 | } |
182 | |
183 | private void executeEnqueuedCallbacks(int startupResult, boolean alreadyStarted) { |
184 | assert ThreadUtils.runningOnUiThread() : "Callback from browser startup from wrong thread."; |
185 | mAsyncStartupDone = true; |
186 | for (StartupCallback asyncStartupCallback : mAsyncStartupCallbacks) { |
187 | if (startupResult > 0) { |
188 | asyncStartupCallback.onFailure(); |
189 | } else { |
190 | mStartupSuccess = true; |
191 | asyncStartupCallback.onSuccess(alreadyStarted); |
192 | } |
193 | } |
194 | // We don't want to hold on to any objects after we do not need them anymore. |
195 | mAsyncStartupCallbacks.clear(); |
196 | } |
197 | |
198 | private void enqueueCallbackExecution(final int startupFailure, final boolean alreadyStarted) { |
199 | new Handler().post(new Runnable() { |
200 | @Override |
201 | public void run() { |
202 | executeEnqueuedCallbacks(startupFailure, alreadyStarted); |
203 | } |
204 | }); |
205 | } |
206 | |
207 | private void postStartupCompleted(final StartupCallback callback) { |
208 | new Handler().post(new Runnable() { |
209 | @Override |
210 | public void run() { |
211 | if (mStartupSuccess) |
212 | callback.onSuccess(ALREADY_STARTED); |
213 | else |
214 | callback.onFailure(); |
215 | } |
216 | }); |
217 | } |
218 | |
219 | /** |
220 | * Ensure that the browser process will be asynchronously started up. This also ensures that we |
221 | * get a call to {@link #browserStartupComplete} when the browser startup is complete. |
222 | */ |
223 | @VisibleForTesting |
224 | void enableAsynchronousStartup() { |
225 | setAsynchronousStartupConfig(); |
226 | } |
227 | |
228 | /** |
229 | * @return whether the process was already initialized, so native was not instructed to start. |
230 | */ |
231 | @VisibleForTesting |
232 | boolean initializeAndroidBrowserProcess() throws ProcessInitException { |
233 | return !AndroidBrowserProcess.init(mContext, AndroidBrowserProcess.MAX_RENDERERS_LIMIT); |
234 | } |
235 | } |