blob: 10c193de45615c22f181d5fceba60b49528ae28b [file] [log] [blame]
// Copyright 2019 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.weblayer_private;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.util.SparseArray;
import android.webkit.ValueCallback;
import android.webkit.WebViewDelegate;
import android.webkit.WebViewFactory;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import org.chromium.base.BuildInfo;
import org.chromium.base.BundleUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.ContentUriUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.StrictModeContext;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.components.embedder_support.application.ClassLoaderContextWrapperFactory;
import org.chromium.content_public.browser.BrowserStartupController;
import org.chromium.content_public.browser.ChildProcessCreationParams;
import org.chromium.content_public.browser.DeviceUtils;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.ui.base.ResourceBundle;
import org.chromium.weblayer_private.interfaces.APICallException;
import org.chromium.weblayer_private.interfaces.IBrowserFragment;
import org.chromium.weblayer_private.interfaces.ICrashReporterController;
import org.chromium.weblayer_private.interfaces.IObjectWrapper;
import org.chromium.weblayer_private.interfaces.IProfile;
import org.chromium.weblayer_private.interfaces.IRemoteFragmentClient;
import org.chromium.weblayer_private.interfaces.IWebLayer;
import org.chromium.weblayer_private.interfaces.IWebLayerClient;
import org.chromium.weblayer_private.interfaces.ObjectWrapper;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import org.chromium.weblayer_private.metrics.MetricsServiceClient;
import org.chromium.weblayer_private.metrics.UmaUtils;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Root implementation class for WebLayer.
*/
@JNINamespace("weblayer")
public final class WebLayerImpl extends IWebLayer.Stub {
// TODO: should there be one tag for all this code?
private static final String TAG = "WebLayer";
private static final String PRIVATE_DIRECTORY_SUFFIX = "weblayer";
// Command line flags are only read in debug builds.
// WARNING: this file is written to by testing code in chrome (see
// "//chrome/test/chromedriver/chrome/device_manager.cc"). If you change this variable, update
// "device_manager.cc" too. If the command line file exists in the app's private files
// directory, it will be read from there, otherwise it will be read from
// PUBLIC_COMMAND_LINE_FILE.
private static final String COMMAND_LINE_FILE = "weblayer-command-line";
private static final String PUBLIC_COMMAND_LINE_FILE = "/data/local/tmp/" + COMMAND_LINE_FILE;
// This metadata key, if defined, overrides the default behaviour of loading WebLayer from the
// current WebView implementation. This is only intended for testing, and does not enforce any
// signature requirements on the implementation, nor does it use the production code path to
// load the code. Do not set this in production APKs!
private static final String PACKAGE_MANIFEST_KEY = "org.chromium.weblayer.WebLayerPackage";
// SharedPreferences key storing the versionCode of the most recently loaded WebLayer library.
public static final String PREF_LAST_VERSION_CODE =
"org.chromium.weblayer.last_version_code_used";
// The required package ID for WebLayer when loaded as a shared library, hardcoded in the
// resources. If this value changes make sure to change _SHARED_LIBRARY_HARDCODED_ID in
// //build/android/gyp/util/protoresources.py.
private static final int REQUIRED_PACKAGE_IDENTIFIER = 12;
private final ProfileManager mProfileManager = new ProfileManager();
private boolean mInited;
private static IWebLayerClient sClient;
// Whether WebView is running in process. Set in init().
private boolean mIsWebViewCompatMode;
private static class FileProviderHelper implements ContentUriUtils.FileProviderUtil {
// Keep this variable in sync with the value defined in AndroidManifest.xml.
private static final String API_AUTHORITY_SUFFIX =
".org.chromium.weblayer.client.FileProvider";
@Override
public Uri getContentUriFromFile(File file) {
Context appContext = ContextUtils.getApplicationContext();
return FileProvider.getUriForFile(
appContext, appContext.getPackageName() + API_AUTHORITY_SUFFIX, file);
}
}
WebLayerImpl() {}
@Override
public void loadAsyncV80(
IObjectWrapper appContextWrapper, IObjectWrapper loadedCallbackWrapper) {
loadAsync(appContextWrapper, null, loadedCallbackWrapper);
}
@Override
public void loadAsync(IObjectWrapper appContextWrapper, IObjectWrapper remoteContextWrapper,
IObjectWrapper loadedCallbackWrapper) {
StrictModeWorkaround.apply();
init(appContextWrapper, remoteContextWrapper);
final ValueCallback<Boolean> loadedCallback = (ValueCallback<Boolean>) ObjectWrapper.unwrap(
loadedCallbackWrapper, ValueCallback.class);
BrowserStartupController.getInstance().startBrowserProcessesAsync(
LibraryProcessType.PROCESS_WEBLAYER,
/* startGpu */ false, /* startServiceManagerOnly */ false,
new BrowserStartupController.StartupCallback() {
@Override
public void onSuccess() {
onNativeLoaded(appContextWrapper);
loadedCallback.onReceiveValue(true);
}
@Override
public void onFailure() {
loadedCallback.onReceiveValue(false);
}
});
}
@Override
public void loadSyncV80(IObjectWrapper appContextWrapper) {
loadSync(appContextWrapper, null);
}
@Override
public void loadSync(IObjectWrapper appContextWrapper, IObjectWrapper remoteContextWrapper) {
StrictModeWorkaround.apply();
init(appContextWrapper, remoteContextWrapper);
BrowserStartupController.getInstance().startBrowserProcessesSync(
LibraryProcessType.PROCESS_WEBLAYER,
/* singleProcess*/ false);
onNativeLoaded(appContextWrapper);
}
private void onNativeLoaded(IObjectWrapper appContextWrapper) {
CrashReporterControllerImpl.getInstance().notifyNativeInitialized();
NetworkChangeNotifier.init();
NetworkChangeNotifier.registerToReceiveNotificationsAlways();
// This issues JNI calls which require native code to be loaded.
MetricsServiceClient.init();
assert mInited;
WebLayerImplJni.get().setIsWebViewCompatMode(mIsWebViewCompatMode);
}
private void init(IObjectWrapper appContextWrapper, IObjectWrapper remoteContextWrapper) {
if (mInited) {
return;
}
mInited = true;
UmaUtils.recordMainEntryPointTime();
LibraryLoader.getInstance().setLibraryProcessType(LibraryProcessType.PROCESS_WEBLAYER);
Context remoteContext = ObjectWrapper.unwrap(remoteContextWrapper, Context.class);
// The remote context will have a different class loader than WebLayerImpl here if we are in
// WebView compat mode, since WebView compat mode creates it's own class loader. The class
// loader from remoteContext will actually never be used, since
// ClassLoaderContextWrapperFactory will override the class loader, and all contexts used in
// WebLayer should come from ClassLoaderContextWrapperFactory.
mIsWebViewCompatMode = remoteContext != null
&& !remoteContext.getClassLoader().equals(WebLayerImpl.class.getClassLoader());
if (mIsWebViewCompatMode) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
// Load the library with the crazy linker.
LibraryLoader.getInstance().setLinkerImplementation(true, false);
WebViewCompatibilityHelperImpl.setRequiresManualJniRegistration(true);
}
notifyWebViewRunningInProcess(remoteContext.getClassLoader());
}
Context appContext = minimalInitForContext(appContextWrapper, remoteContextWrapper);
PackageInfo packageInfo = WebViewFactory.getLoadedPackageInfo();
// If a remote context is not provided, the client is an older version that loads the native
// library on the client side.
if (remoteContextWrapper != null) {
loadNativeLibrary(packageInfo.packageName);
}
BuildInfo.setBrowserPackageInfo(packageInfo);
// TODO: The call to onResourcesLoaded() can be slow, we may need to parallelize this with
// other expensive startup tasks.
R.onResourcesLoaded(forceCorrectPackageId(remoteContext));
SelectionPopupController.setMustUseWebContentsContext();
ResourceBundle.setAvailablePakLocales(new String[] {}, ProductConfig.UNCOMPRESSED_LOCALES);
BundleUtils.setIsBundle(ProductConfig.IS_BUNDLE);
setChildProcessCreationParams(appContext, packageInfo.packageName);
if (!CommandLine.isInitialized()) {
if (BuildInfo.isDebugAndroid()) {
// This disk read in the critical path is for development purposes only.
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
File localCommandLineFile =
new File(appContext.getFilesDir(), COMMAND_LINE_FILE);
if (localCommandLineFile.exists()) {
CommandLine.initFromFile(localCommandLineFile.getPath());
} else {
CommandLine.initFromFile(PUBLIC_COMMAND_LINE_FILE);
}
}
} else {
CommandLine.init(null);
}
}
// Creating the Android shared preferences object causes I/O.
try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
deleteDataIfPackageDowngrade(prefs, packageInfo);
}
DeviceUtils.addDeviceSpecificUserAgentSwitch();
ContentUriUtils.setFileProviderUtil(new FileProviderHelper());
// TODO: Validate that doing this disk IO on the main thread is necessary.
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
LibraryLoader.getInstance().ensureInitialized();
}
GmsBridge.getInstance().setSafeBrowsingHandler();
MediaStreamManager.onWebLayerInit();
WebLayerNotificationChannels.updateChannelsIfNecessary();
}
@Override
public IBrowserFragment createBrowserFragmentImpl(
IRemoteFragmentClient fragmentClient, IObjectWrapper fragmentArgs) {
StrictModeWorkaround.apply();
Bundle unwrappedArgs = ObjectWrapper.unwrap(fragmentArgs, Bundle.class);
BrowserFragmentImpl fragment =
new BrowserFragmentImpl(mProfileManager, fragmentClient, unwrappedArgs);
return fragment.asIBrowserFragment();
}
@Override
public IProfile getProfile(String profileName) {
StrictModeWorkaround.apply();
return mProfileManager.getProfile(profileName);
}
@Override
public void setRemoteDebuggingEnabled(boolean enabled) {
StrictModeWorkaround.apply();
WebLayerImplJni.get().setRemoteDebuggingEnabled(enabled);
}
@Override
public boolean isRemoteDebuggingEnabled() {
StrictModeWorkaround.apply();
return WebLayerImplJni.get().isRemoteDebuggingEnabled();
}
@Override
public ICrashReporterController getCrashReporterControllerV80(IObjectWrapper appContext) {
StrictModeWorkaround.apply();
return getCrashReporterController(appContext, null);
}
@Override
public ICrashReporterController getCrashReporterController(
IObjectWrapper appContext, IObjectWrapper remoteContext) {
StrictModeWorkaround.apply();
// This is a no-op if init has already happened.
WebLayerImpl.minimalInitForContext(appContext, remoteContext);
return CrashReporterControllerImpl.getInstance();
}
@Override
public void onReceivedBroadcast(IObjectWrapper appContextWrapper, Intent intent) {
StrictModeWorkaround.apply();
Context context = ObjectWrapper.unwrap(appContextWrapper, Context.class);
if (intent.getAction().startsWith(DownloadImpl.getIntentPrefix())) {
DownloadImpl.forwardIntent(context, intent, mProfileManager);
} else if (intent.getAction().startsWith(MediaStreamManager.getIntentPrefix())) {
MediaStreamManager.forwardIntent(intent);
}
}
@Override
public void enumerateAllProfileNames(IObjectWrapper valueCallback) {
StrictModeWorkaround.apply();
final ValueCallback<String[]> callback =
(ValueCallback<String[]>) ObjectWrapper.unwrap(valueCallback, ValueCallback.class);
ProfileImpl.enumerateAllProfileNames(callback);
}
@Override
public void setClient(IWebLayerClient client) {
StrictModeWorkaround.apply();
sClient = client;
}
@Override
public String getUserAgentString() {
StrictModeWorkaround.apply();
return WebLayerImplJni.get().getUserAgentString();
}
/**
* Creates a remote context. This should only be used for backwards compatibility when the
* client was not sending the remote context.
*/
public static Context createRemoteContextV80(Context appContext) {
try {
return appContext.createPackageContext(
WebViewFactory.getLoadedPackageInfo().packageName,
Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
} catch (PackageManager.NameNotFoundException e) {
throw new AndroidRuntimeException(e);
}
}
public static Intent createIntent() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.createIntent();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static String getClientApplicationName() {
Context context = ContextUtils.getApplicationContext();
return new StringBuilder()
.append(context.getPackageManager().getApplicationLabel(
context.getApplicationInfo()))
.toString();
}
/**
* Converts the given id into a resource ID that can be shown in system UI, such as
* notifications.
*/
public static int getResourceIdForSystemUi(int id) {
if (isAndroidResource(id)) {
return id;
}
Context context = ContextUtils.getApplicationContext();
try {
// String may be missing translations, since they are loaded at a different package ID
// by default in standalone WebView.
assert !context.getResources().getResourceTypeName(id).equals("string");
} catch (Resources.NotFoundException e) {
}
id &= 0x00ffffff;
id |= (0x01000000
* getPackageId(context, WebViewFactory.getLoadedPackageInfo().packageName));
return id;
}
/** Returns whether this ID is from the android system package. */
public static boolean isAndroidResource(int id) {
try {
return ContextUtils.getApplicationContext()
.getResources()
.getResourcePackageName(id)
.equals("android");
} catch (Resources.NotFoundException e) {
return false;
}
}
/**
* Performs the minimal initialization needed for a context. This is used for example in
* CrashReporterControllerImpl, so it can be used before full WebLayer initialization.
*/
private static Context minimalInitForContext(
IObjectWrapper appContextWrapper, IObjectWrapper remoteContextWrapper) {
if (ContextUtils.getApplicationContext() != null) {
return ContextUtils.getApplicationContext();
}
Context appContext = ObjectWrapper.unwrap(appContextWrapper, Context.class);
Context remoteContext = ObjectWrapper.unwrap(remoteContextWrapper, Context.class);
if (remoteContext == null) {
remoteContext = createRemoteContextV80(appContext);
}
ClassLoaderContextWrapperFactory.setResourceOverrideContext(remoteContext);
// Wrap the app context so that it can be used to load WebLayer implementation classes.
appContext = ClassLoaderContextWrapperFactory.get(appContext);
ContextUtils.initApplicationContext(appContext);
PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DIRECTORY_SUFFIX, PRIVATE_DIRECTORY_SUFFIX);
return appContext;
}
/** Forces the correct package ID or dies with a runtime exception. */
private static int forceCorrectPackageId(Context remoteContext) {
int packageId = getPackageId(remoteContext, remoteContext.getPackageName());
// This is using app_as_shared_lib, no change needed.
if (packageId >= 0x7f) {
return packageId;
}
if (packageId > REQUIRED_PACKAGE_IDENTIFIER) {
throw new AndroidRuntimeException(
"WebLayer can't be used with other shared libraries. Loaded packages: "
+ getLoadedPackageNames(remoteContext));
}
forceAddAssetPaths(remoteContext, packageId);
return REQUIRED_PACKAGE_IDENTIFIER;
}
/** Forces adding entries to the package identifiers array until we hit the required ID. */
private static void forceAddAssetPaths(Context remoteContext, int packageId) {
try {
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
String path = remoteContext.getApplicationInfo().sourceDir;
// Add enough paths to make sure we reach the required ID.
for (int i = packageId; i < REQUIRED_PACKAGE_IDENTIFIER; i++) {
// Change the path to ensure the asset path is re-added and grabs a new package ID.
path = "/." + path;
addAssetPath.invoke(remoteContext.getAssets(), path);
}
} catch (ReflectiveOperationException e) {
throw new AndroidRuntimeException(e);
}
}
/**
* Returns the package ID to use when calling R.onResourcesLoaded().
*/
private static int getPackageId(Context appContext, String implPackageName) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
Constructor<WebViewDelegate> constructor =
WebViewDelegate.class.getDeclaredConstructor();
constructor.setAccessible(true);
WebViewDelegate delegate = constructor.newInstance();
return delegate.getPackageId(appContext.getResources(), implPackageName);
} else {
// In L WebViewDelegate did not yet exist, so we have to look inside AssetManager.
Method getAssignedPackageIdentifiers =
AssetManager.class.getMethod("getAssignedPackageIdentifiers");
SparseArray<String> packageIdentifiers =
(SparseArray) getAssignedPackageIdentifiers.invoke(
appContext.getResources().getAssets());
for (int i = 0; i < packageIdentifiers.size(); i++) {
final String name = packageIdentifiers.valueAt(i);
if (implPackageName.equals(name)) {
return packageIdentifiers.keyAt(i);
}
}
throw new RuntimeException("Package not found: " + implPackageName);
}
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
/** Gets a string with all the loaded package names in this context. */
private static String getLoadedPackageNames(Context appContext) {
try {
Method getAssignedPackageIdentifiers =
AssetManager.class.getMethod("getAssignedPackageIdentifiers");
SparseArray<String> packageIdentifiers =
(SparseArray) getAssignedPackageIdentifiers.invoke(
appContext.getResources().getAssets());
List<String> packageNames = new ArrayList<>();
for (int i = 0; i < packageIdentifiers.size(); i++) {
String name = packageIdentifiers.valueAt(i);
int key = packageIdentifiers.keyAt(i);
// This is the android package.
if (key == 1) {
continue;
}
// Make sure this doesn't look like a URL so it doesn't get removed from crashes.
packageNames.add(name.replace(".", "_") + " -> " + key);
}
return TextUtils.join(",", packageNames);
} catch (ReflectiveOperationException e) {
return "unknown";
}
}
private void loadNativeLibrary(String packageName) {
// Loading the library triggers disk access.
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
WebViewFactory.loadWebViewNativeLibraryFromPackage(
packageName, getClass().getClassLoader());
} else {
try {
Method loadNativeLibrary =
WebViewFactory.class.getDeclaredMethod("loadNativeLibrary");
loadNativeLibrary.setAccessible(true);
loadNativeLibrary.invoke(null);
} catch (ReflectiveOperationException e) {
Log.e(TAG, "Failed to load native library.", e);
}
}
}
}
private void setChildProcessCreationParams(Context appContext, String implPackageName) {
final boolean bindToCaller = true;
final boolean ignoreVisibilityForImportance = false;
final String privilegedServicesPackageName = appContext.getPackageName();
final String privilegedServicesName =
"org.chromium.weblayer.ChildProcessService$Privileged";
String sandboxedServicesPackageName = appContext.getPackageName();
String sandboxedServicesName = "org.chromium.weblayer.ChildProcessService$Sandboxed";
boolean isExternalService = false;
boolean loadedFromWebView = wasLoadedFromWebView(appContext);
if (loadedFromWebView && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// On O+ when loading from a WebView implementation, we can just use WebView's declared
// external services as our renderers, which means we benefit from the webview zygote
// process. We still need to use the client's privileged services, as only isolated
// services can be external.
isExternalService = true;
sandboxedServicesPackageName = implPackageName;
sandboxedServicesName = null;
}
ChildProcessCreationParams.set(privilegedServicesPackageName, privilegedServicesName,
sandboxedServicesPackageName, sandboxedServicesName, isExternalService,
LibraryProcessType.PROCESS_WEBLAYER_CHILD, bindToCaller,
ignoreVisibilityForImportance);
}
private static boolean wasLoadedFromWebView(Context appContext) {
try {
Bundle metaData = appContext.getPackageManager()
.getApplicationInfo(appContext.getPackageName(),
PackageManager.GET_META_DATA)
.metaData;
if (metaData != null && metaData.getString(PACKAGE_MANIFEST_KEY) != null) {
return false;
}
return true;
} catch (PackageManager.NameNotFoundException e) {
// This would indicate the client app doesn't exist;
// just return true as there's nothing sensible to do here.
return true;
}
}
private static void deleteDataIfPackageDowngrade(
SharedPreferences prefs, PackageInfo packageInfo) {
int previousVersion = prefs.getInt(PREF_LAST_VERSION_CODE, 0);
int currentVersion = packageInfo.versionCode;
if (getBranchFromVersionCode(currentVersion) < getBranchFromVersionCode(previousVersion)) {
// WebLayer was downgraded since the last run. Delete the data and cache directories.
File dataDir = new File(PathUtils.getDataDirectory());
Log.i(TAG,
"WebLayer package downgraded from " + previousVersion + " to " + currentVersion
+ "; deleting contents of " + dataDir);
deleteDirectoryContents(dataDir);
}
if (previousVersion != currentVersion) {
prefs.edit().putInt(PREF_LAST_VERSION_CODE, currentVersion).apply();
}
}
/**
* Chromium versionCodes follow the 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.
*
* @return The branch number of versionCode.
*/
private static int getBranchFromVersionCode(int versionCode) {
return versionCode / 1_000_00;
}
private static void deleteDirectoryContents(File directory) {
File[] files = directory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (!FileUtils.recursivelyDeleteFile(file, FileUtils.DELETE_ALL)) {
Log.w(TAG, "Failed to delete " + file);
}
}
}
private static void notifyWebViewRunningInProcess(ClassLoader webViewClassLoader) {
try {
Class<?> webViewChromiumFactoryProviderClass =
Class.forName("com.android.webview.chromium.WebViewChromiumFactoryProvider",
true, webViewClassLoader);
Method setter = webViewChromiumFactoryProviderClass.getDeclaredMethod(
"setWebLayerRunningInSameProcess");
setter.invoke(null);
} catch (Exception e) {
Log.w(TAG, "Unable to notify WebView running in process", e);
}
}
@CalledByNative
@Nullable
private static String getEmbedderName() {
return getClientApplicationName();
}
@NativeMethods
interface Natives {
void setRemoteDebuggingEnabled(boolean enabled);
boolean isRemoteDebuggingEnabled();
void setIsWebViewCompatMode(boolean value);
String getUserAgentString();
}
}