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