| 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 | } |