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