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