blob: abe9e0ec0849c6212e725a8a2d68fc1c6156f5ec [file] [log] [blame]
// Copyright 2018 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 com.android.webview.chromium;
import android.Manifest;
import android.app.compat.CompatChanges;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions;
import android.webkit.WebSettings;
import android.webkit.WebStorage;
import android.webkit.WebViewDatabase;
import org.chromium.android_webview.AwBrowserContext;
import org.chromium.android_webview.AwBrowserProcess;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwContentsStatics;
import org.chromium.android_webview.AwCookieManager;
import org.chromium.android_webview.AwDarkMode;
import org.chromium.android_webview.AwLocaleConfig;
import org.chromium.android_webview.AwNetworkChangeNotifierRegistrationPolicy;
import org.chromium.android_webview.AwProxyController;
import org.chromium.android_webview.AwServiceWorkerController;
import org.chromium.android_webview.AwThreadUtils;
import org.chromium.android_webview.AwTracingController;
import org.chromium.android_webview.HttpAuthDatabase;
import org.chromium.android_webview.ProductConfig;
import org.chromium.android_webview.R;
import org.chromium.android_webview.WebViewChromiumRunQueue;
import org.chromium.android_webview.common.AwResource;
import org.chromium.android_webview.common.AwSwitches;
import org.chromium.android_webview.gfx.AwDrawFnImpl;
import org.chromium.android_webview.variations.VariationsSeedLoader;
import org.chromium.base.BuildInfo;
import org.chromium.base.BundleUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.FieldTrialList;
import org.chromium.base.JNIUtils;
import org.chromium.base.PathService;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.base.task.PostTask;
import org.chromium.build.BuildConfig;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.ResourceBundle;
/**
* Class controlling the Chromium initialization for WebView.
* We hold on to most static objects used by WebView here.
* This class is shared between the webkit glue layer and the support library glue layer.
*/
public class WebViewChromiumAwInit {
private static final String TAG = "WebViewChromiumAwInit";
private static final String HTTP_AUTH_DATABASE_FILE = "http_auth.db";
// TODO(gsennton): store aw-objects instead of adapters here
// Initialization guarded by mLock.
private AwBrowserContext mBrowserContext;
private AwTracingController mTracingController;
private SharedStatics mSharedStatics;
private GeolocationPermissionsAdapter mGeolocationPermissions;
private CookieManagerAdapter mCookieManager;
private WebIconDatabaseAdapter mWebIconDatabase;
private WebStorageAdapter mWebStorage;
private WebViewDatabaseAdapter mWebViewDatabase;
private AwServiceWorkerController mServiceWorkerController;
private AwTracingController mAwTracingController;
private VariationsSeedLoader mSeedLoader;
private Thread mSetUpResourcesThread;
private AwProxyController mAwProxyController;
// Guards accees to the other members, and is notifyAll() signalled on the UI thread
// when the chromium process has been started.
// This member is not private only because the downstream subclass needs to access it,
// it shouldn't be accessed from anywhere else.
/* package */ final Object mLock = new Object();
// mInitState should only transition INIT_NOT_STARTED -> INIT_STARTED -> INIT_FINISHED
private static final int INIT_NOT_STARTED = 0;
private static final int INIT_STARTED = 1;
private static final int INIT_FINISHED = 2;
// Read/write protected by mLock
private int mInitState;
private final WebViewChromiumFactoryProvider mFactory;
private boolean mIsPostedFromBackgroundThread;
WebViewChromiumAwInit(WebViewChromiumFactoryProvider factory) {
mFactory = factory;
// Do not make calls into 'factory' in this ctor - this ctor is called from the
// WebViewChromiumFactoryProvider ctor, so 'factory' is not properly initialized yet.
TraceEvent.maybeEnableEarlyTracing(
TraceEvent.ATRACE_TAG_WEBVIEW, /*readCommandLine=*/false);
}
public AwTracingController getAwTracingController() {
synchronized (mLock) {
if (mAwTracingController == null) {
ensureChromiumStartedLocked(true);
}
}
return mAwTracingController;
}
public AwProxyController getAwProxyController() {
synchronized (mLock) {
if (mAwProxyController == null) {
ensureChromiumStartedLocked(true);
}
}
return mAwProxyController;
}
// TODO: DIR_RESOURCE_PAKS_ANDROID needs to live somewhere sensible,
// inlined here for simplicity setting up the HTMLViewer demo. Unfortunately
// it can't go into base.PathService, as the native constant it refers to
// lives in the ui/ layer. See ui/base/ui_base_paths.h
private static final int DIR_RESOURCE_PAKS_ANDROID = 3003;
protected void startChromiumLocked() {
long startTime = SystemClock.uptimeMillis();
try (ScopedSysTraceEvent event =
ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.startChromiumLocked")) {
assert Thread.holdsLock(mLock) && ThreadUtils.runningOnUiThread();
// The post-condition of this method is everything is ready, so notify now to cover all
// return paths. (Other threads will not wake-up until we release |mLock|, whatever).
mLock.notifyAll();
if (mInitState == INIT_FINISHED) {
return;
}
final Context context = ContextUtils.getApplicationContext();
JNIUtils.setClassLoader(WebViewChromiumAwInit.class.getClassLoader());
ResourceBundle.setAvailablePakLocales(AwLocaleConfig.getWebViewSupportedPakLocales());
BundleUtils.setIsBundle(ProductConfig.IS_BUNDLE);
// We are rewriting Java resources in the background.
// NOTE: Any reference to Java resources will cause a crash.
try (ScopedSysTraceEvent e =
ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.LibraryLoader")) {
LibraryLoader.getInstance().ensureInitialized();
}
PathService.override(PathService.DIR_MODULE, "/system/lib/");
PathService.override(DIR_RESOURCE_PAKS_ANDROID, "/system/framework/webview/paks");
initPlatSupportLibrary();
doNetworkInitializations(context);
waitUntilSetUpResources();
// NOTE: Finished writing Java resources. From this point on, it's safe to use them.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && mIsPostedFromBackgroundThread) {
// Try to work around the problem we're seeing with resources on Android 12. When
// WebView is being initialized from a background thread, it's possible that the
// asset path updated by WebViewFactory is no longer present by the time we get
// here due to something on the UI thread having caused a resource update in the
// app in the meantime, because WebViewFactory does not add the path persistently.
// So, we can try to add them again using the "better" method in WebViewDelegate.
// However, we only want to try this if the resources are actually missing, because
// in the past we've seen this cause apps that were working to *start* crashing.
// The first resource that gets accessed in startup happens during the
// AwBrowserProcess.start() call when trying to determine if the device is a tablet,
// and that's the most common place for us to crash. So, try calling that same
// method and see if it throws - if so then we're unlikely to make the situation
// any worse by trying to fix the path.
try {
DeviceFormFactor.isTablet();
} catch (Resources.NotFoundException e) {
mFactory.addWebViewAssetPath(context);
}
}
AwBrowserProcess.configureChildProcessLauncher();
// finishVariationsInitLocked() must precede native initialization so the seed is
// available when AwFeatureListCreator::SetUpFieldTrials() runs.
finishVariationsInitLocked();
AwBrowserProcess.start();
AwBrowserProcess.handleMinidumpsAndSetMetricsConsent(true /* updateMetricsConsent */);
// This has to be done after variations are initialized, so components could be
// registered or not depending on the variations flags.
AwBrowserProcess.loadComponents();
AwBrowserProcess.initializeMetricsLogUploader();
mSharedStatics = new SharedStatics();
if (BuildInfo.isDebugAndroid()) {
mSharedStatics.setWebContentsDebuggingEnabledUnconditionally(true);
}
mInitState = INIT_FINISHED;
RecordHistogram.recordSparseHistogram("Android.WebView.TargetSdkVersion",
context.getApplicationInfo().targetSdkVersion);
try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped(
"WebViewChromiumAwInit.initThreadUnsafeSingletons")) {
// Initialize thread-unsafe singletons.
AwBrowserContext awBrowserContext = getBrowserContextOnUiThread();
mGeolocationPermissions = new GeolocationPermissionsAdapter(
mFactory, awBrowserContext.getGeolocationPermissions());
mWebStorage =
new WebStorageAdapter(mFactory, mBrowserContext.getQuotaManagerBridge());
mAwTracingController = getTracingController();
mServiceWorkerController = awBrowserContext.getServiceWorkerController();
mAwProxyController = new AwProxyController();
}
mFactory.getRunQueue().drainQueue();
if (BuildInfo.isAtLeastT()
? CompatChanges.isChangeEnabled(WebSettings.ENABLE_SIMPLIFIED_DARK_MODE)
: BuildInfo.targetsAtLeastT()) {
AwDarkMode.enableSimplifiedDarkMode();
}
if (CommandLine.getInstance().hasSwitch(AwSwitches.WEBVIEW_VERBOSE_LOGGING)) {
logCommandLineAndActiveTrials();
}
}
RecordHistogram.recordTimesHistogram(
"Android.WebView.Startup.CreationTime.StartChromiumLocked",
SystemClock.uptimeMillis() - startTime);
}
/**
* Set up resources on a background thread.
* @param context The context.
*/
public void setUpResourcesOnBackgroundThread(int packageId, Context context) {
try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped(
"WebViewChromiumAwInit.setUpResourcesOnBackgroundThread")) {
assert mSetUpResourcesThread == null : "This method shouldn't be called twice.";
// Make sure that ResourceProvider is initialized before starting the browser process.
mSetUpResourcesThread = new Thread(new Runnable() {
@Override
public void run() {
// Run this in parallel as it takes some time.
setUpResources(packageId, context);
}
});
mSetUpResourcesThread.start();
}
}
private void waitUntilSetUpResources() {
try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped(
"WebViewChromiumAwInit.waitUntilSetUpResources")) {
mSetUpResourcesThread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void setUpResources(int packageId, Context context) {
try (ScopedSysTraceEvent e =
ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.setUpResources")) {
R.onResourcesLoaded(packageId);
AwResource.setResources(context.getResources());
AwResource.setConfigKeySystemUuidMapping(android.R.array.config_keySystemUuidMapping);
}
}
boolean hasStarted() {
return mInitState == INIT_FINISHED;
}
void startYourEngines(boolean fromThreadSafeFunction) {
synchronized (mLock) {
ensureChromiumStartedLocked(fromThreadSafeFunction);
}
}
// This method is not private only because the downstream subclass needs to access it,
// it shouldn't be accessed from anywhere else.
/* package */ void ensureChromiumStartedLocked(boolean fromThreadSafeFunction) {
assert Thread.holdsLock(mLock);
if (mInitState == INIT_FINISHED) { // Early-out for the common case.
return;
}
if (mInitState == INIT_NOT_STARTED) {
// If we're the first thread to enter ensureChromiumStartedLocked, we need to determine
// which thread will be the UI thread; declare init has started so that no other thread
// will try to do this.
mInitState = INIT_STARTED;
setChromiumUiThreadLocked(fromThreadSafeFunction);
}
if (ThreadUtils.runningOnUiThread()) {
// If we are currently running on the UI thread then we must do init now. If there was
// already a task posted to the UI thread from another thread to do it, it will just
// no-op when it runs.
startChromiumLocked();
return;
}
mIsPostedFromBackgroundThread = true;
// If we're not running on the UI thread (because init was triggered by a thread-safe
// function), post init to the UI thread, since init is *not* thread-safe.
AwThreadUtils.postToUiThreadLooper(new Runnable() {
@Override
public void run() {
synchronized (mLock) {
startChromiumLocked();
}
}
});
// Wait for the UI thread to finish init.
while (mInitState != INIT_FINISHED) {
try {
mLock.wait();
} catch (InterruptedException e) {
// Keep trying; we can't abort init as WebView APIs do not declare that they throw
// InterruptedException.
}
}
}
private void setChromiumUiThreadLocked(boolean fromThreadSafeFunction) {
// If we're being started from a function that's allowed to be called on any thread,
// then we can't just assume the current thread is the UI thread; instead we assume the
// process's main looper will be the UI thread, because that's the case for almost all
// Android apps.
//
// If we're being started from a function that must be called from the UI
// thread, then by definition the current thread is the UI thread whether it's the main
// looper or not.
Looper looper = fromThreadSafeFunction ? Looper.getMainLooper() : Looper.myLooper();
Log.v(TAG,
"Binding Chromium to "
+ (Looper.getMainLooper().equals(looper) ? "main" : "background")
+ " looper " + looper);
ThreadUtils.setUiThread(looper);
}
private void initPlatSupportLibrary() {
try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped(
"WebViewChromiumAwInit.initPlatSupportLibrary")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
AwDrawFnImpl.setDrawFnFunctionTable(DrawFunctor.getDrawFnFunctionTable());
}
DrawGLFunctor.setChromiumAwDrawGLFunction(AwContents.getAwDrawGLFunction());
AwContents.setAwDrawSWFunctionTable(GraphicsUtils.getDrawSWFunctionTable());
AwContents.setAwDrawGLFunctionTable(GraphicsUtils.getDrawGLFunctionTable());
}
}
private void doNetworkInitializations(Context applicationContext) {
try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped(
"WebViewChromiumAwInit.doNetworkInitializations")) {
if (applicationContext.checkPermission(
Manifest.permission.ACCESS_NETWORK_STATE, Process.myPid(), Process.myUid())
== PackageManager.PERMISSION_GRANTED) {
NetworkChangeNotifier.init();
NetworkChangeNotifier.setAutoDetectConnectivityState(
new AwNetworkChangeNotifierRegistrationPolicy());
}
AwContentsStatics.setCheckClearTextPermitted(
applicationContext.getApplicationInfo().targetSdkVersion
>= Build.VERSION_CODES.O);
}
}
public AwTracingController getTracingController() {
if (mTracingController == null) {
mTracingController = new AwTracingController();
}
return mTracingController;
}
// Only on UI thread.
AwBrowserContext getBrowserContextOnUiThread() {
assert mInitState == INIT_FINISHED;
if (BuildConfig.ENABLE_ASSERTS && !ThreadUtils.runningOnUiThread()) {
throw new RuntimeException(
"getBrowserContextOnUiThread called on " + Thread.currentThread());
}
if (mBrowserContext == null) {
mBrowserContext = AwBrowserContext.getDefault();
}
return mBrowserContext;
}
/**
* Returns the lock used for guarding chromium initialization.
* We make this public to let higher-level classes use this lock to guard variables
* dependent on this class, to avoid introducing new locks (which can cause deadlocks).
*/
public Object getLock() {
return mLock;
}
public SharedStatics getStatics() {
synchronized (mLock) {
if (mSharedStatics == null) {
// TODO: Optimization potential: most these methods only need the native library
// loaded and initialized, not the entire browser process started.
// See also http://b/7009882
ensureChromiumStartedLocked(true);
}
}
return mSharedStatics;
}
public GeolocationPermissions getGeolocationPermissions() {
synchronized (mLock) {
if (mGeolocationPermissions == null) {
ensureChromiumStartedLocked(true);
}
}
return mGeolocationPermissions;
}
public CookieManager getCookieManager() {
synchronized (mLock) {
if (mCookieManager == null) {
mCookieManager = new CookieManagerAdapter(new AwCookieManager());
}
}
return mCookieManager;
}
public AwServiceWorkerController getServiceWorkerController() {
synchronized (mLock) {
if (mServiceWorkerController == null) {
ensureChromiumStartedLocked(true);
}
}
return mServiceWorkerController;
}
public android.webkit.WebIconDatabase getWebIconDatabase() {
synchronized (mLock) {
ensureChromiumStartedLocked(true);
if (mWebIconDatabase == null) {
mWebIconDatabase = new WebIconDatabaseAdapter();
}
}
return mWebIconDatabase;
}
public WebStorage getWebStorage() {
synchronized (mLock) {
if (mWebStorage == null) {
ensureChromiumStartedLocked(true);
}
}
return mWebStorage;
}
public WebViewDatabase getWebViewDatabase(final Context context) {
synchronized (mLock) {
ensureChromiumStartedLocked(true);
if (mWebViewDatabase == null) {
mWebViewDatabase = new WebViewDatabaseAdapter(
mFactory, HttpAuthDatabase.newInstance(context, HTTP_AUTH_DATABASE_FILE));
}
}
return mWebViewDatabase;
}
// See comments in VariationsSeedLoader.java on when it's safe to call this.
public void startVariationsInit() {
synchronized (mLock) {
if (mSeedLoader == null) {
mSeedLoader = new VariationsSeedLoader();
mSeedLoader.startVariationsInit();
}
}
}
private void finishVariationsInitLocked() {
try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped(
"WebViewChromiumAwInit.finishVariationsInitLocked")) {
assert Thread.holdsLock(mLock);
if (mSeedLoader == null) {
Log.e(TAG, "finishVariationsInitLocked() called before startVariationsInit()");
startVariationsInit();
}
mSeedLoader.finishVariationsInit();
mSeedLoader = null; // Allow this to be GC'd after its background thread finishes.
}
}
// Log extra information, for debugging purposes. Do the work asynchronously to avoid blocking
// startup.
private void logCommandLineAndActiveTrials() {
PostTask.postTask(UiThreadTaskTraits.BEST_EFFORT, () -> {
// TODO(ntfschr): CommandLine can change at any time. For simplicity, only log it
// once during startup.
AwContentsStatics.logCommandLineForDebugging();
// Field trials can be activated at any time. We'll continue logging them as they're
// activated.
FieldTrialList.logActiveTrials();
// SafeMode was already determined earlier during the startup sequence, this just
// fetches the cached boolean state. If SafeMode was enabled, we already logged detailed
// information about the SafeMode config.
Log.i(TAG, "SafeMode enabled: " + mFactory.isSafeModeEnabled());
});
}
public WebViewChromiumRunQueue getRunQueue() {
return mFactory.getRunQueue();
}
}