blob: fffde3116ea8b015a581b01fb93aa78bb79dc331 [file] [log] [blame]
// 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 com.android.webview.chromium;
import android.Manifest;
import android.app.ActivityManager;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import android.os.Process;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions;
import android.webkit.ServiceWorkerController;
import android.webkit.TokenBindingService;
import android.webkit.ValueCallback;
import android.webkit.WebStorage;
import android.webkit.WebView;
import android.webkit.WebViewDatabase;
import android.webkit.WebViewFactory;
import android.webkit.WebViewFactoryProvider;
import android.webkit.WebViewProvider;
import com.android.webview.chromium.WebViewDelegateFactory.WebViewDelegate;
import org.chromium.android_webview.AwBrowserContext;
import org.chromium.android_webview.AwBrowserProcess;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwContentsClient;
import org.chromium.android_webview.AwContentsStatics;
import org.chromium.android_webview.AwCookieManager;
import org.chromium.android_webview.AwDevToolsServer;
import org.chromium.android_webview.AwMetricsServiceClient;
import org.chromium.android_webview.AwNetworkChangeNotifierRegistrationPolicy;
import org.chromium.android_webview.AwQuotaManagerBridge;
import org.chromium.android_webview.AwResource;
import org.chromium.android_webview.AwSettings;
import org.chromium.android_webview.HttpAuthDatabase;
import org.chromium.android_webview.PlatformServiceBridge;
import org.chromium.android_webview.ResourcesContextWrapperFactory;
import org.chromium.android_webview.command_line.CommandLineUtil;
import org.chromium.base.BuildConfig;
import org.chromium.base.BuildInfo;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.MemoryPressureListener;
import org.chromium.base.PackageUtils;
import org.chromium.base.PathService;
import org.chromium.base.PathUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.library_loader.NativeLibraries;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.content.browser.ContentViewStatics;
import org.chromium.content.browser.input.LGEmailActionModeWorkaround;
import org.chromium.net.NetworkChangeNotifier;
import java.io.File;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
* Entry point to the WebView. The system framework talks to this class to get instances of the
* implementation classes.
*/
@SuppressWarnings("deprecation")
public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
private static final String TAG = "WebViewChromiumFactoryProvider";
private static final String CHROMIUM_PREFS_NAME = "WebViewChromiumPrefs";
private static final String VERSION_CODE_PREF = "lastVersionCodeUsed";
private static final String HTTP_AUTH_DATABASE_FILE = "http_auth.db";
private class WebViewChromiumRunQueue {
public WebViewChromiumRunQueue() {
mQueue = new ConcurrentLinkedQueue<Runnable>();
}
public void addTask(Runnable task) {
mQueue.add(task);
if (WebViewChromiumFactoryProvider.this.hasStarted()) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
drainQueue();
}
});
}
}
public void drainQueue() {
if (mQueue == null || mQueue.isEmpty()) {
return;
}
Runnable task = mQueue.poll();
while (task != null) {
task.run();
task = mQueue.poll();
}
}
private final Queue<Runnable> mQueue;
}
private final WebViewChromiumRunQueue mRunQueue = new WebViewChromiumRunQueue();
private <T> T runBlockingFuture(FutureTask<T> task) {
if (!hasStarted()) throw new RuntimeException("Must be started before we block!");
if (ThreadUtils.runningOnUiThread()) {
throw new IllegalStateException("This method should only be called off the UI thread");
}
mRunQueue.addTask(task);
try {
return task.get(4, TimeUnit.SECONDS);
} catch (java.util.concurrent.TimeoutException e) {
throw new RuntimeException("Probable deadlock detected due to WebView API being called "
+ "on incorrect thread while the UI thread is blocked.", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// We have a 4 second timeout to try to detect deadlocks to detect and aid in debuggin
// deadlocks.
// Do not call this method while on the UI thread!
/* package */ void runVoidTaskOnUiThreadBlocking(Runnable r) {
FutureTask<Void> task = new FutureTask<Void>(r, null);
runBlockingFuture(task);
}
/* package */ <T> T runOnUiThreadBlocking(Callable<T> c) {
return runBlockingFuture(new FutureTask<T>(c));
}
/* package */ void addTask(Runnable task) {
mRunQueue.addTask(task);
}
// Guards accees to the other members, and is notifyAll() signalled on the UI thread
// when the chromium process has been started.
private final Object mLock = new Object();
// Initialization guarded by mLock.
private AwBrowserContext mBrowserContext;
private Statics mStaticMethods;
private GeolocationPermissionsAdapter mGeolocationPermissions;
private CookieManagerAdapter mCookieManager;
private Object mTokenBindingManager;
private WebIconDatabaseAdapter mWebIconDatabase;
private WebStorageAdapter mWebStorage;
private WebViewDatabaseAdapter mWebViewDatabase;
private AwDevToolsServer mDevToolsServer;
private Object mServiceWorkerController;
// Read/write protected by mLock.
private boolean mStarted;
private SharedPreferences mWebViewPrefs;
private WebViewDelegate mWebViewDelegate;
boolean mShouldDisableThreadChecking;
/**
* Entry point for newer versions of Android.
*/
public static WebViewChromiumFactoryProvider create(android.webkit.WebViewDelegate delegate) {
return new WebViewChromiumFactoryProvider(delegate);
}
/**
* Constructor called by the API 21 version of {@link WebViewFactory} and earlier.
*/
public WebViewChromiumFactoryProvider() {
initialize(WebViewDelegateFactory.createApi21CompatibilityDelegate());
}
/**
* Constructor called by the API 22 version of {@link WebViewFactory} and later.
*/
public WebViewChromiumFactoryProvider(android.webkit.WebViewDelegate delegate) {
initialize(WebViewDelegateFactory.createProxyDelegate(delegate));
}
/**
* Constructor for internal use when a proxy delegate has already been created.
*/
WebViewChromiumFactoryProvider(WebViewDelegate delegate) {
initialize(delegate);
}
private void initialize(WebViewDelegate webViewDelegate) {
mWebViewDelegate = webViewDelegate;
Context ctx = mWebViewDelegate.getApplication().getApplicationContext();
// If the application context is DE, but we have credentials, use a CE context instead
try {
checkStorageIsNotDeviceProtected(mWebViewDelegate.getApplication());
} catch (IllegalArgumentException e) {
if (!ctx.getSystemService(UserManager.class).isUserUnlocked()) {
throw e;
}
ctx = ctx.createCredentialProtectedStorageContext();
}
// WebView needs to make sure to always use the wrapped application context.
ContextUtils.initApplicationContext(ResourcesContextWrapperFactory.get(ctx));
CommandLineUtil.initCommandLine();
boolean multiProcess = false;
if (BuildInfo.isAtLeastO()) {
// Ask the system if multiprocess should be enabled on O+.
multiProcess = mWebViewDelegate.isMultiProcessEnabled();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Check the multiprocess developer setting directly on N.
multiProcess = Settings.Global.getInt(
ContextUtils.getApplicationContext().getContentResolver(),
Settings.Global.WEBVIEW_MULTIPROCESS, 0) == 1;
}
if (multiProcess) {
CommandLine cl = CommandLine.getInstance();
cl.appendSwitch("webview-sandboxed-renderer");
}
ThreadUtils.setWillOverrideUiThread();
// Load chromium library.
AwBrowserProcess.loadLibrary();
final PackageInfo packageInfo = WebViewFactory.getLoadedPackageInfo();
// Load glue-layer support library.
System.loadLibrary("webviewchromium_plat_support");
// Use shared preference to check for package downgrade.
mWebViewPrefs = ContextUtils.getApplicationContext().getSharedPreferences(
CHROMIUM_PREFS_NAME, Context.MODE_PRIVATE);
int lastVersion = mWebViewPrefs.getInt(VERSION_CODE_PREF, 0);
int currentVersion = packageInfo.versionCode;
if (!versionCodeGE(currentVersion, lastVersion)) {
// The WebView package has been downgraded since we last ran in this application.
// Delete the WebView data directory's contents.
String dataDir = PathUtils.getDataDirectory();
Log.i(TAG, "WebView package downgraded from " + lastVersion + " to " + currentVersion
+ "; deleting contents of " + dataDir);
deleteContents(new File(dataDir));
}
if (lastVersion != currentVersion) {
mWebViewPrefs.edit().putInt(VERSION_CODE_PREF, currentVersion).apply();
}
mShouldDisableThreadChecking =
shouldDisableThreadChecking(ContextUtils.getApplicationContext());
// Now safe to use WebView data directory.
}
static void checkStorageIsNotDeviceProtected(Context context) {
if ((Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M)
&& context.isDeviceProtectedStorage()) {
throw new IllegalArgumentException(
"WebView cannot be used with device protected storage");
}
}
/**
* Both versionCodes should be from a WebView provider package implemented by Chromium.
* VersionCodes from other kinds of packages won't make any sense in this method.
*
* An introduction to Chromium versionCode scheme:
* "BBBBPPPAX"
* BBBB: 4 digit branch number. It monotonically increases over time.
* PPP: patch number in the branch. It is padded with zeroes to the left. These three digits may
* change their meaning in the future.
* A: architecture digit.
* X: A digit to differentiate APKs for other reasons.
*
* This method takes the "BBBB" of versionCodes and compare them.
*
* @return true if versionCode1 is higher than or equal to versionCode2.
*/
private static boolean versionCodeGE(int versionCode1, int versionCode2) {
int v1 = versionCode1 / 100000;
int v2 = versionCode2 / 100000;
return v1 >= v2;
}
private static void deleteContents(File dir) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteContents(file);
}
if (!file.delete()) {
Log.w(TAG, "Failed to delete " + file);
}
}
}
}
public static boolean preloadInZygote() {
for (String library : NativeLibraries.LIBRARIES) {
System.loadLibrary(library);
}
return true;
}
private void initPlatSupportLibrary() {
DrawGLFunctor.setChromiumAwDrawGLFunction(AwContents.getAwDrawGLFunction());
AwContents.setAwDrawSWFunctionTable(GraphicsUtils.getDrawSWFunctionTable());
AwContents.setAwDrawGLFunctionTable(GraphicsUtils.getDrawGLFunctionTable());
}
private void doNetworkInitializations(Context applicationContext) {
if (applicationContext.checkPermission(Manifest.permission.ACCESS_NETWORK_STATE,
Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED) {
NetworkChangeNotifier.init(applicationContext);
NetworkChangeNotifier.setAutoDetectConnectivityState(
new AwNetworkChangeNotifierRegistrationPolicy());
}
AwContentsStatics.setCheckClearTextPermitted(BuildInfo.targetsAtLeastO(applicationContext));
}
private void ensureChromiumStartedLocked(boolean onMainThread) {
assert Thread.holdsLock(mLock);
if (mStarted) { // Early-out for the common case.
return;
}
Looper looper = !onMainThread ? Looper.myLooper() : Looper.getMainLooper();
Log.v(TAG, "Binding Chromium to "
+ (Looper.getMainLooper().equals(looper) ? "main" : "background")
+ " looper " + looper);
ThreadUtils.setUiThread(looper);
if (ThreadUtils.runningOnUiThread()) {
startChromiumLocked();
return;
}
// We must post to the UI thread to cover the case that the user has invoked Chromium
// startup by using the (thread-safe) CookieManager rather than creating a WebView.
ThreadUtils.postOnUiThread(new Runnable() {
@Override
public void run() {
synchronized (mLock) {
startChromiumLocked();
}
}
});
while (!mStarted) {
try {
// Important: wait() releases |mLock| the UI thread can take it :-)
mLock.wait();
} catch (InterruptedException e) {
// Keep trying... eventually the UI thread will process the task we sent it.
}
}
}
// 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() {
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 (mStarted) {
return;
}
try {
LibraryLoader.get(LibraryProcessType.PROCESS_WEBVIEW).ensureInitialized();
} catch (ProcessInitException e) {
throw new RuntimeException("Error initializing WebView library", e);
}
PathService.override(PathService.DIR_MODULE, "/system/lib/");
PathService.override(DIR_RESOURCE_PAKS_ANDROID, "/system/framework/webview/paks");
// Make sure that ResourceProvider is initialized before starting the browser process.
final String webViewPackageName = WebViewFactory.getLoadedPackageInfo().packageName;
final Context context = ContextUtils.getApplicationContext();
setUpResources(webViewPackageName, context);
initPlatSupportLibrary();
doNetworkInitializations(context);
final boolean isExternalService = true;
AwBrowserProcess.configureChildProcessLauncher(webViewPackageName, isExternalService);
AwBrowserProcess.start();
final boolean enableMinidumpUploadingForTesting = CommandLine.getInstance().hasSwitch(
CommandLineUtil.CRASH_UPLOADS_ENABLED_FOR_TESTING_SWITCH);
if (enableMinidumpUploadingForTesting) {
AwBrowserProcess.handleMinidumps(webViewPackageName, true /* enabled */);
}
// Actions conditioned on whether the Android Checkbox is toggled on
PlatformServiceBridge.getInstance(context)
.queryMetricsSetting(new ValueCallback<Boolean>() {
public void onReceiveValue(Boolean enabled) {
ThreadUtils.assertOnUiThread();
AwMetricsServiceClient.setConsentSetting(context, enabled);
if (!enableMinidumpUploadingForTesting) {
AwBrowserProcess.handleMinidumps(webViewPackageName, enabled);
}
}
});
if (CommandLineUtil.isBuildDebuggable()) {
setWebContentsDebuggingEnabled(true);
}
TraceEvent.setATraceEnabled(mWebViewDelegate.isTraceTagEnabled());
mWebViewDelegate.setOnTraceEnabledChangeListener(
new WebViewDelegate.OnTraceEnabledChangeListener() {
@Override
public void onTraceEnabledChange(boolean enabled) {
TraceEvent.setATraceEnabled(enabled);
}
});
mStarted = true;
// Initialize thread-unsafe singletons.
AwBrowserContext awBrowserContext = getBrowserContextOnUiThread();
mGeolocationPermissions = new GeolocationPermissionsAdapter(
this, awBrowserContext.getGeolocationPermissions());
mWebStorage = new WebStorageAdapter(this, AwQuotaManagerBridge.getInstance());
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
mServiceWorkerController = new ServiceWorkerControllerAdapter(
awBrowserContext.getServiceWorkerController());
}
mRunQueue.drainQueue();
}
boolean hasStarted() {
return mStarted;
}
void startYourEngines(boolean onMainThread) {
synchronized (mLock) {
ensureChromiumStartedLocked(onMainThread);
}
}
// Only on UI thread.
AwBrowserContext getBrowserContextOnUiThread() {
assert mStarted;
if (BuildConfig.DCHECK_IS_ON && !ThreadUtils.runningOnUiThread()) {
throw new RuntimeException(
"getBrowserContextOnUiThread called on " + Thread.currentThread());
}
if (mBrowserContext == null) {
mBrowserContext =
new AwBrowserContext(mWebViewPrefs, ContextUtils.getApplicationContext());
}
return mBrowserContext;
}
private void setWebContentsDebuggingEnabled(boolean enable) {
if (Looper.myLooper() != ThreadUtils.getUiThreadLooper()) {
throw new RuntimeException(
"Toggling of Web Contents Debugging must be done on the UI thread");
}
if (mDevToolsServer == null) {
if (!enable) return;
mDevToolsServer = new AwDevToolsServer();
}
mDevToolsServer.setRemoteDebuggingEnabled(enable);
}
private void setUpResources(String webViewPackageName, Context context) {
ResourceRewriter.rewriteRValues(
mWebViewDelegate.getPackageId(context.getResources(), webViewPackageName));
AwResource.setResources(context.getResources());
AwResource.setConfigKeySystemUuidMapping(android.R.array.config_keySystemUuidMapping);
}
@Override
public Statics getStatics() {
synchronized (mLock) {
if (mStaticMethods == 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);
mStaticMethods = new WebViewFactoryProvider.Statics() {
@Override
public String findAddress(String addr) {
return ContentViewStatics.findAddress(addr);
}
@Override
public String getDefaultUserAgent(Context context) {
return AwSettings.getDefaultUserAgent();
}
@Override
public void setWebContentsDebuggingEnabled(boolean enable) {
// Web Contents debugging is always enabled on debug builds.
if (!CommandLineUtil.isBuildDebuggable()) {
WebViewChromiumFactoryProvider.this.setWebContentsDebuggingEnabled(
enable);
}
}
@Override
public void clearClientCertPreferences(Runnable onCleared) {
AwContentsStatics.clearClientCertPreferences(onCleared);
}
@Override
public void freeMemoryForTests() {
if (ActivityManager.isRunningInTestHarness()) {
MemoryPressureListener.maybeNotifyMemoryPresure(
ComponentCallbacks2.TRIM_MEMORY_COMPLETE);
}
}
@Override
public void enableSlowWholeDocumentDraw() {
WebViewChromium.enableSlowWholeDocumentDraw();
}
@Override
public Uri[] parseFileChooserResult(int resultCode, Intent intent) {
return AwContentsClient.parseFileChooserResult(resultCode, intent);
}
};
}
}
return mStaticMethods;
}
@Override
public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) {
return new WebViewChromium(this, webView, privateAccess, mShouldDisableThreadChecking);
}
// Workaround for IME thread crashes on grandfathered OEM apps.
private boolean shouldDisableThreadChecking(Context context) {
String appName = context.getPackageName();
int versionCode = PackageUtils.getPackageVersion(context, appName);
int appTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
if (versionCode == -1) return false;
boolean shouldDisable = false;
// crbug.com/651706
final String lgeMailPackageId = "com.lge.email";
if (lgeMailPackageId.equals(appName)) {
if (appTargetSdkVersion > Build.VERSION_CODES.N) return false;
// This is the last broken version shipped on LG V20/NRD90M.
if (versionCode > LGEmailActionModeWorkaround.LGEmailWorkaroundMaxVersion) return false;
shouldDisable = true;
}
// crbug.com/655759
// Also want to cover ".att" variant suffix package name.
final String yahooMailPackageId = "com.yahoo.mobile.client.android.mail";
if (appName.startsWith(yahooMailPackageId)) {
if (appTargetSdkVersion > Build.VERSION_CODES.M) return false;
if (versionCode > 1315850) return false;
shouldDisable = true;
}
// crbug.com/622151
final String htcMailPackageId = "com.htc.android.mail";
if (htcMailPackageId.equals(appName)) {
if (appTargetSdkVersion > Build.VERSION_CODES.M) return false;
// This value is provided by HTC.
if (versionCode >= 866001861) return false;
shouldDisable = true;
}
if (shouldDisable) {
Log.w(TAG, "Disabling thread check in WebView. "
+ "APK name: " + appName + ", versionCode: " + versionCode
+ ", targetSdkVersion: " + appTargetSdkVersion);
}
return shouldDisable;
}
@Override
public GeolocationPermissions getGeolocationPermissions() {
synchronized (mLock) {
if (mGeolocationPermissions == null) {
ensureChromiumStartedLocked(true);
}
}
return mGeolocationPermissions;
}
@Override
public CookieManager getCookieManager() {
synchronized (mLock) {
if (mCookieManager == null) {
mCookieManager = new CookieManagerAdapter(new AwCookieManager());
}
}
return mCookieManager;
}
@Override
public ServiceWorkerController getServiceWorkerController() {
synchronized (mLock) {
if (mServiceWorkerController == null) {
ensureChromiumStartedLocked(true);
}
}
return (ServiceWorkerController) mServiceWorkerController;
}
public TokenBindingService getTokenBindingService() {
synchronized (mLock) {
if (mTokenBindingManager == null) {
mTokenBindingManager = new TokenBindingManagerAdapter(this);
}
}
return (TokenBindingService) mTokenBindingManager;
}
@Override
public android.webkit.WebIconDatabase getWebIconDatabase() {
synchronized (mLock) {
if (mWebIconDatabase == null) {
ensureChromiumStartedLocked(true);
mWebIconDatabase = new WebIconDatabaseAdapter();
}
}
return mWebIconDatabase;
}
@Override
public WebStorage getWebStorage() {
synchronized (mLock) {
if (mWebStorage == null) {
ensureChromiumStartedLocked(true);
}
}
return mWebStorage;
}
@Override
public WebViewDatabase getWebViewDatabase(final Context context) {
synchronized (mLock) {
if (mWebViewDatabase == null) {
ensureChromiumStartedLocked(true);
mWebViewDatabase = new WebViewDatabaseAdapter(
this, HttpAuthDatabase.newInstance(context, HTTP_AUTH_DATABASE_FILE));
}
}
return mWebViewDatabase;
}
WebViewDelegate getWebViewDelegate() {
return mWebViewDelegate;
}
// The method to support unreleased Android.
WebViewContentsClientAdapter createWebViewContentsClientAdapter(WebView webView,
Context context) {
return new WebViewContentsClientAdapter(webView, context, mWebViewDelegate);
}
}