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 org.chromium.base.CalledByNative; 12 import org.chromium.base.JNINamespace; 13 import org.chromium.base.ResourceExtractor; 14 import org.chromium.base.ThreadUtils; 15 import org.chromium.base.VisibleForTesting; 16 import org.chromium.base.library_loader.LibraryLoader; 17 import org.chromium.base.library_loader.LoaderErrors; 18 import org.chromium.base.library_loader.ProcessInitException; 19 import org.chromium.content.app.ContentMain; 20 21 import java.util.ArrayList; 22 import 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") 38 public class BrowserStartupController { 39 40 /** 41 * This provides the interface to the callbacks for successful or failed startup 42 */ 43 public interface StartupCallback { onSuccess(boolean alreadyStarted)44 void onSuccess(boolean alreadyStarted); onFailure()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 setAsynchronousStartup(boolean enable)64 private static void setAsynchronousStartup(boolean enable) { 65 sBrowserMayStartAsynchronously = enable; 66 } 67 68 @VisibleForTesting 69 @CalledByNative browserMayStartAsynchonously()70 static boolean browserMayStartAsynchonously() { 71 return sBrowserMayStartAsynchronously; 72 } 73 74 @VisibleForTesting 75 @CalledByNative browserStartupComplete(int result)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 BrowserStartupController(Context context)102 BrowserStartupController(Context context) { 103 mContext = context; 104 mAsyncStartupCallbacks = new ArrayList<StartupCallback>(); 105 } 106 get(Context context)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 overrideInstanceForTest(BrowserStartupController controller)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 */ startBrowserProcessesAsync(final StartupCallback callback)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 */ startBrowserProcessesSync(boolean singleProcess)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 contentStart()196 int contentStart() { 197 return ContentMain.start(); 198 } 199 addStartupCompletedObserver(StartupCallback callback)200 public void addStartupCompletedObserver(StartupCallback callback) { 201 ThreadUtils.assertOnUiThread(); 202 if (mStartupDone) { 203 postStartupCompleted(callback); 204 } else { 205 mAsyncStartupCallbacks.add(callback); 206 } 207 } 208 executeEnqueuedCallbacks(int startupResult, boolean alreadyStarted)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. enqueueCallbackExecution(final int startupFailure, final boolean alreadyStarted)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 postStartupCompleted(final StartupCallback callback)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 prepareToStartBrowserProcess(boolean singleProcess)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 */ initChromiumBrowserProcessForTests()276 public void initChromiumBrowserProcessForTests() { 277 ResourceExtractor resourceExtractor = ResourceExtractor.get(mContext); 278 resourceExtractor.startExtractingResources(); 279 resourceExtractor.waitForCompletion(); 280 nativeSetCommandLineFlags(false, null); 281 } 282 getPlugins()283 private String getPlugins() { 284 return PepperPluginManager.getPlugins(mContext); 285 } 286 nativeSetCommandLineFlags( boolean singleProcess, String pluginDescriptor)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. nativeIsOfficialBuild()292 private static native boolean nativeIsOfficialBuild(); 293 nativeIsPluginEnabled()294 private static native boolean nativeIsPluginEnabled(); 295 } 296