// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.init;
import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.StrictMode;
import com.squareup.leakcanary.LeakCanary;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.BaseSwitches;
import org.chromium.base.CommandLine;
import org.chromium.base.ContentUriUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.ResourceExtractor;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.annotations.RemovableInRelease;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.ChromeStrictMode;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.FileProviderHelper;
import org.chromium.chrome.browser.crash.MinidumpDirectoryObserver;
import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.chrome.browser.services.GoogleServicesManager;
import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelImpl;
import org.chromium.chrome.browser.webapps.ActivityAssigner;
import org.chromium.chrome.browser.webapps.ChromeWebApkHost;
import org.chromium.content.app.ContentApplication;
import org.chromium.content.browser.BrowserStartupController;
import org.chromium.content.browser.ChildProcessCreationParams;
import org.chromium.content.browser.DeviceUtils;
import org.chromium.content.browser.SpeechRecognition;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.policy.CombinedPolicyProvider;
import org.chromium.ui.base.DeviceFormFactor;
import java.util.LinkedList;
import java.util.Locale;
/**
* Application level delegate that handles start up tasks.
* {@link AsyncInitializationActivity} classes should override the {@link BrowserParts}
* interface for any additional initialization tasks for the initialization to work as intended.
*/
public class ChromeBrowserInitializer {
private static final String TAG = "BrowserInitializer";
private static ChromeBrowserInitializer sChromeBrowserInitiliazer;
private final Handler mHandler;
private final ChromeApplication mApplication;
private final Locale mInitialLocale = Locale.getDefault();
private boolean mPreInflationStartupComplete;
private boolean mPostInflationStartupComplete;
private boolean mNativeInitializationComplete;
private MinidumpDirectoryObserver mMinidumpDirectoryObserver;
// Public to allow use in ChromeBackupAgent
public static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "chrome";
/**
* A callback to be executed when there is a new version available in Play Store.
*/
public interface OnNewVersionAvailableCallback extends Runnable {
/**
* Set the update url to get the new version available.
* @param updateUrl The url to be used.
*/
void setUpdateUrl(String updateUrl);
}
/**
* This class is an application specific object that orchestrates the app initialization.
* @param context The context to get the application context from.
* @return The singleton instance of {@link ChromeBrowserInitializer}.
*/
public static ChromeBrowserInitializer getInstance(Context context) {
if (sChromeBrowserInitiliazer == null) {
sChromeBrowserInitiliazer = new ChromeBrowserInitializer(context);
}
return sChromeBrowserInitiliazer;
}
private ChromeBrowserInitializer(Context context) {
mApplication = (ChromeApplication) context.getApplicationContext();
mHandler = new Handler(Looper.getMainLooper());
initLeakCanary();
}
@RemovableInRelease
private void initLeakCanary() {
// Watch that Activity objects are not retained after their onDestroy() has been called.
// This is a no-op in release builds.
LeakCanary.install(mApplication);
}
/**
* Initializes the Chrome browser process synchronously.
*
* @throws ProcessInitException if there is a problem with the native library.
*/
public void handleSynchronousStartup() throws ProcessInitException {
assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread";
BrowserParts parts = new EmptyBrowserParts();
handlePreNativeStartup(parts);
handlePostNativeStartup(false, parts);
}
/**
* Execute startup tasks that can be done without native libraries. See {@link BrowserParts} for
* a list of calls to be implemented.
* @param parts The delegate for the {@link ChromeBrowserInitializer} to communicate
* initialization tasks.
*/
public void handlePreNativeStartup(final BrowserParts parts) {
assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread";
ProcessInitializationHandler.getInstance().initializePreNative();
preInflationStartup();
parts.preInflationStartup();
if (parts.isActivityFinishing()) return;
preInflationStartupDone();
parts.setContentViewAndLoadLibrary();
postInflationStartup();
parts.postInflationStartup();
}
/**
* This is needed for device class manager which depends on commandline args that are
* initialized in preInflationStartup()
*/
private void preInflationStartupDone() {
// Domain reliability uses significant enough memory that we should disable it on low memory
// devices for now.
// TODO(zbowling): remove this after domain reliability is refactored. (crbug.com/495342)
if (DeviceClassManager.disableDomainReliability()) {
CommandLine.getInstance().appendSwitch(ChromeSwitches.DISABLE_DOMAIN_RELIABILITY);
}
}
/**
* Pre-load shared prefs to avoid being blocked on the disk access async task in the future.
* Running in an AsyncTask as pre-loading itself may cause I/O.
*/
private void warmUpSharedPrefs() {
if (Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
ContextUtils.getAppSharedPreferences();
DocumentTabModelImpl.warmUpSharedPrefs(mApplication);
ActivityAssigner.warmUpSharedPrefs(mApplication);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
ContextUtils.getAppSharedPreferences();
DocumentTabModelImpl.warmUpSharedPrefs(mApplication);
ActivityAssigner.warmUpSharedPrefs(mApplication);
}
}
private void preInflationStartup() {
ThreadUtils.assertOnUiThread();
if (mPreInflationStartupComplete) return;
PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX, mApplication);
// Ensure critical files are available, so they aren't blocked on the file-system
// behind long-running accesses in next phase.
// Don't do any large file access here!
ContentApplication.initCommandLine(mApplication);
waitForDebuggerIfNeeded();
ChromeStrictMode.configureStrictMode();
ChromeWebApkHost.init();
warmUpSharedPrefs();
DeviceUtils.addDeviceSpecificUserAgentSwitch(mApplication);
ApplicationStatus.registerStateListenerForAllActivities(
createActivityStateListener());
mPreInflationStartupComplete = true;
}
private void postInflationStartup() {
ThreadUtils.assertOnUiThread();
if (mPostInflationStartupComplete) return;
// Check to see if we need to extract any new resources from the APK. This could
// be on first run when we need to extract all the .pak files we need, or after
// the user has switched locale, in which case we want new locale resources.
ResourceExtractor.get(mApplication).startExtractingResources();
mPostInflationStartupComplete = true;
}
/**
* Execute startup tasks that require native libraries to be loaded. See {@link BrowserParts}
* for a list of calls to be implemented.
* @param isAsync Whether this call should synchronously wait for the browser process to be
* fully initialized before returning to the caller.
* @param delegate The delegate for the {@link ChromeBrowserInitializer} to communicate
* initialization tasks.
*/
public void handlePostNativeStartup(final boolean isAsync, final BrowserParts delegate)
throws ProcessInitException {
assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread";
final LinkedList<Runnable> initQueue = new LinkedList<>();
abstract class NativeInitTask implements Runnable {
@Override
public final void run() {
// Run the current task then put a request for the next one onto the
// back of the UI message queue. This lets Chrome handle input events
// between tasks.
initFunction();
if (!initQueue.isEmpty()) {
Runnable nextTask = initQueue.pop();
if (isAsync) {
mHandler.post(nextTask);
} else {
nextTask.run();
}
}
}
public abstract void initFunction();
}
initQueue.add(new NativeInitTask() {
@Override
public void initFunction() {
ProcessInitializationHandler.getInstance().initializePostNative();
}
});
initQueue.add(new NativeInitTask() {
@Override
public void initFunction() {
initNetworkChangeNotifier(mApplication.getApplicationContext());
}
});
initQueue.add(new NativeInitTask() {
@Override
public void initFunction() {
// This is not broken down as a separate task, since this:
// 1. Should happen as early as possible
// 2. Only submits asynchronous work
// 3. Is thus very cheap (profiled at 0.18ms on a Nexus 5 with Lollipop)
// It should also be in a separate task (and after) initNetworkChangeNotifier, as
// this posts a task to the UI thread that would interfere with preconneciton
// otherwise. By preconnecting afterwards, we make sure that this task has run.
delegate.maybePreconnect();
onStartNativeInitialization();
}
});
initQueue.add(new NativeInitTask() {
@Override
public void initFunction() {
if (delegate.isActivityDestroyed()) return;
delegate.initializeCompositor();
}
});
initQueue.add(new NativeInitTask() {
@Override
public void initFunction() {
if (delegate.isActivityDestroyed()) return;
delegate.initializeState();
}
});
initQueue.add(new NativeInitTask() {
@Override
public void initFunction() {
onFinishNativeInitialization();
}
});
initQueue.add(new NativeInitTask() {
@Override
public void initFunction() {
if (delegate.isActivityDestroyed()) return;
delegate.finishNativeInitialization();
}
});
// See crbug.com/593250. This can be removed after N SDK is released, crbug.com/592722.
ChildProcessCreationParams creationParams = mApplication.getChildProcessCreationParams();
// WebAPK uses this code path to initialize Chrome's native code, and the
// ChildProcessCreationParams has been set in {@link WebApkActivity}. We have to prevent
// resetting with a wrong parameter here. TODO(hanxi): Remove the entire if block after
// N SDK is released, since it breaks WebAPKs on N+.
if (creationParams != null && ChildProcessCreationParams.get() != null) {
ChildProcessCreationParams.set(creationParams);
}
if (isAsync) {
// We want to start this queue once the C++ startup tasks have run; allow the
// C++ startup to run asynchonously, and set it up to start the Java queue once
// it has finished.
startChromeBrowserProcessesAsync(
delegate.shouldStartGpuProcess(),
new BrowserStartupController.StartupCallback() {
@Override
public void onFailure() {
delegate.onStartupFailure();
}
@Override
public void onSuccess(boolean success) {
mHandler.post(initQueue.pop());
}
});
} else {
startChromeBrowserProcessesSync();
initQueue.pop().run();
assert initQueue.isEmpty();
}
}
private void startChromeBrowserProcessesAsync(
boolean startGpuProcess,
BrowserStartupController.StartupCallback callback) throws ProcessInitException {
try {
TraceEvent.begin("ChromeBrowserInitializer.startChromeBrowserProcessesAsync");
BrowserStartupController.get(mApplication, LibraryProcessType.PROCESS_BROWSER)
.startBrowserProcessesAsync(startGpuProcess, callback);
} finally {
TraceEvent.end("ChromeBrowserInitializer.startChromeBrowserProcessesAsync");
}
}
private void startChromeBrowserProcessesSync() throws ProcessInitException {
try {
TraceEvent.begin("ChromeBrowserInitializer.startChromeBrowserProcessesSync");
ThreadUtils.assertOnUiThread();
mApplication.initCommandLine();
LibraryLoader libraryLoader = LibraryLoader.get(LibraryProcessType.PROCESS_BROWSER);
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
libraryLoader.ensureInitialized();
StrictMode.setThreadPolicy(oldPolicy);
libraryLoader.asyncPrefetchLibrariesToMemory();
BrowserStartupController.get(mApplication, LibraryProcessType.PROCESS_BROWSER)
.startBrowserProcessesSync(false);
GoogleServicesManager.get(mApplication);
} finally {
TraceEvent.end("ChromeBrowserInitializer.startChromeBrowserProcessesSync");
}
}
public static void initNetworkChangeNotifier(Context context) {
ThreadUtils.assertOnUiThread();
TraceEvent.begin("NetworkChangeNotifier.init");
// Enable auto-detection of network connectivity state changes.
NetworkChangeNotifier.init(context);
NetworkChangeNotifier.setAutoDetectConnectivityState(true);
TraceEvent.end("NetworkChangeNotifier.init");
}
private void onStartNativeInitialization() {
ThreadUtils.assertOnUiThread();
if (mNativeInitializationComplete) return;
// The policies are used by browser startup, so we need to register the policy providers
// before starting the browser process.
mApplication.registerPolicyProviders(CombinedPolicyProvider.get());
SpeechRecognition.initialize(mApplication);
}
private void onFinishNativeInitialization() {
if (mNativeInitializationComplete) return;
mNativeInitializationComplete = true;
ContentUriUtils.setFileProviderUtil(new FileProviderHelper());
// Start the file observer to watch the minidump directory.
new AsyncTask<Void, Void, MinidumpDirectoryObserver>() {
@Override
protected MinidumpDirectoryObserver doInBackground(Void... params) {
return new MinidumpDirectoryObserver();
}
@Override
protected void onPostExecute(MinidumpDirectoryObserver minidumpDirectoryObserver) {
mMinidumpDirectoryObserver = minidumpDirectoryObserver;
mMinidumpDirectoryObserver.startWatching();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void waitForDebuggerIfNeeded() {
if (CommandLine.getInstance().hasSwitch(BaseSwitches.WAIT_FOR_JAVA_DEBUGGER)) {
Log.e(TAG, "Waiting for Java debugger to connect...");
android.os.Debug.waitForDebugger();
Log.e(TAG, "Java debugger connected. Resuming execution.");
}
}
private ActivityStateListener createActivityStateListener() {
return new ActivityStateListener() {
@Override
public void onActivityStateChange(Activity activity, int newState) {
if (newState == ActivityState.CREATED || newState == ActivityState.DESTROYED) {
// Android destroys Activities at some point after a locale change, but doesn't
// kill the process. This can lead to a bug where Chrome is halfway RTL, where
// stale natively-loaded resources are not reloaded (http://crbug.com/552618).
if (!mInitialLocale.equals(Locale.getDefault())) {
Log.e(TAG, "Killing process because of locale change.");
Process.killProcess(Process.myPid());
}
DeviceFormFactor.resetValuesIfNeeded(mApplication);
}
}
};
}
}