blob: eb8e5c01dd5923299570e0bcb8639316d4d05267 [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.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
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 dalvik.system.DexFile;
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.TraceEvent;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.compat.ApiHelperForO;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.BackgroundOnlyAsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.components.browser_ui.contacts_picker.ContactsPickerDialog;
import org.chromium.components.browser_ui.photo_picker.DecoderServiceHost;
import org.chromium.components.browser_ui.photo_picker.ImageDecoder;
import org.chromium.components.browser_ui.photo_picker.PhotoPickerDialog;
import org.chromium.components.embedder_support.application.ClassLoaderContextWrapperFactory;
import org.chromium.components.embedder_support.application.FirebaseConfig;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.content_public.browser.BrowserStartupController;
import org.chromium.content_public.browser.ChildProcessCreationParams;
import org.chromium.content_public.browser.ChildProcessLauncherHelper;
import org.chromium.content_public.browser.ContactsPicker;
import org.chromium.content_public.browser.ContactsPickerListener;
import org.chromium.content_public.browser.DeviceUtils;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.ui.base.PhotoPicker;
import org.chromium.ui.base.PhotoPickerDelegate;
import org.chromium.ui.base.PhotoPickerListener;
import org.chromium.ui.base.ResourceBundle;
import org.chromium.ui.base.SelectFileDialog;
import org.chromium.ui.base.WindowAndroid;
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.IMediaRouteDialogFragment;
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.ISettingsFragment;
import org.chromium.weblayer_private.interfaces.ISiteSettingsFragment;
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.media.MediaRouteDialogFragmentImpl;
import org.chromium.weblayer_private.media.MediaRouterClientImpl;
import org.chromium.weblayer_private.media.MediaSessionManager;
import org.chromium.weblayer_private.media.MediaStreamManager;
import org.chromium.weblayer_private.metrics.MetricsServiceClient;
import org.chromium.weblayer_private.metrics.UmaUtils;
import org.chromium.weblayer_private.settings.SettingsFragmentImpl;
import java.io.File;
import java.io.IOException;
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 and WebViewChromiumFactoryProvider.java.
private static final int REQUIRED_PACKAGE_IDENTIFIER = 36;
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 boolean mOnNativeLoadedCalled;
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 loadAsync(IObjectWrapper appContextWrapper, IObjectWrapper remoteContextWrapper,
IObjectWrapper loadedCallbackWrapper) {
StrictModeWorkaround.apply();
init(appContextWrapper, remoteContextWrapper);
final ValueCallback<Boolean> loadedCallback = (ValueCallback<Boolean>) ObjectWrapper.unwrap(
loadedCallbackWrapper, ValueCallback.class);
// WARNING: Ensure any method calls from this guard against the possibility of being called
// multiple times (see comment in loadSync()).
BrowserStartupController.getInstance().startBrowserProcessesAsync(
LibraryProcessType.PROCESS_WEBLAYER,
/* startGpu */ true, /* startMinimalBrowser */ false,
new BrowserStartupController.StartupCallback() {
@Override
public void onSuccess() {
onNativeLoaded();
loadedCallback.onReceiveValue(true);
}
@Override
public void onFailure() {
loadedCallback.onReceiveValue(false);
}
});
}
@Override
public void loadSync(IObjectWrapper appContextWrapper, IObjectWrapper remoteContextWrapper) {
StrictModeWorkaround.apply();
init(appContextWrapper, remoteContextWrapper);
BrowserStartupController.getInstance().startBrowserProcessesSync(
LibraryProcessType.PROCESS_WEBLAYER,
/* singleProcess*/ false);
onNativeLoaded();
// WARNING: loadAsync() may be in progress, and may call methods that this does as well.
// Ensure any method calls from this guard against the possibility of being called multiple
// times.
}
private void onNativeLoaded() {
// This may be called multiple times, ensure processing only happens once.
if (mOnNativeLoadedCalled) return;
mOnNativeLoadedCalled = true;
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(
ObjectWrapper.unwrap(appContextWrapper, Context.class), remoteContext);
GmsBridge.getInstance().checkClientAppContext(appContext);
// Load library in the background since it may be expensive.
// TODO(crbug.com/1146438): Look into enabling relro sharing in browser process. It seems to
// crash when WebView is loaded in the same process.
new BackgroundOnlyAsyncTask<Void>() {
@Override
protected Void doInBackground() {
LibraryLoader.getInstance().loadNow();
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
PackageInfo packageInfo = WebViewFactory.getLoadedPackageInfo();
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);
}
}
// Enable ATRace on debug OS or app builds. Requires commandline initialization.
int applicationFlags = appContext.getApplicationInfo().flags;
boolean isAppDebuggable = (applicationFlags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
boolean isOsDebuggable = BuildInfo.isDebugAndroid();
// Requires command-line flags.
TraceEvent.maybeEnableEarlyTracing(
(isAppDebuggable || isOsDebuggable) ? TraceEvent.ATRACE_TAG_APP : 0,
/*readCommandLine=*/true);
TraceEvent.begin("WebLayer init");
BuildInfo.setBrowserPackageInfo(packageInfo);
BuildInfo.setFirebaseAppId(
FirebaseConfig.getFirebaseAppIdForPackage(packageInfo.packageName));
SelectionPopupController.setMustUseWebContentsContext();
SelectionPopupController.setShouldGetReadbackViewFromWindowAndroid();
ResourceBundle.setAvailablePakLocales(ProductConfig.LOCALES);
BundleUtils.setIsBundle(ProductConfig.IS_BUNDLE);
setChildProcessCreationParams(appContext, packageInfo.packageName);
ChildProcessLauncherHelper.warmUp(appContext, true);
// 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());
GmsBridge.getInstance().setSafeBrowsingHandler();
GmsBridge.getInstance().initializeBuiltInPaymentApps();
MediaStreamManager.onWebLayerInit();
WebLayerNotificationChannels.updateChannelsIfNecessary();
ContactsPicker.setContactsPickerDelegate(
(WindowAndroid windowAndroid, ContactsPickerListener listener,
boolean allowMultiple, boolean includeNames, boolean includeEmails,
boolean includeTel, boolean includeAddresses, boolean includeIcons,
String formattedOrigin) -> {
ContactsPickerDialog dialog = new ContactsPickerDialog(windowAndroid,
new ContactsPickerAdapter(windowAndroid), listener, allowMultiple,
includeNames, includeEmails, includeTel, includeAddresses, includeIcons,
formattedOrigin);
dialog.show();
return dialog;
});
DecoderServiceHost.setIntentSupplier(() -> { return createImageDecoderServiceIntent(); });
SelectFileDialog.setPhotoPickerDelegate(new PhotoPickerDelegate() {
@Override
public PhotoPicker showPhotoPicker(WindowAndroid windowAndroid,
PhotoPickerListener listener, boolean allowMultiple, List<String> mimeTypes) {
PhotoPickerDialog dialog = new PhotoPickerDialog(windowAndroid,
windowAndroid.getContext().get().getContentResolver(), listener,
allowMultiple, /* animatedThumbnailsSupported = */ false, mimeTypes);
dialog.show();
return dialog;
}
@Override
public boolean supportsVideos() {
return false;
}
});
performDexFixIfNecessary(packageInfo);
TraceEvent.end("WebLayer init");
}
@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 ISettingsFragment createSettingsFragmentImpl(
IRemoteFragmentClient remoteFragmentClient, IObjectWrapper fragmentArgs) {
StrictModeWorkaround.apply();
Bundle unwrappedArgs = ObjectWrapper.unwrap(fragmentArgs, Bundle.class);
return new SettingsFragmentImpl(mProfileManager, remoteFragmentClient, unwrappedArgs)
.asISettingsFragment();
}
@Override
public ISiteSettingsFragment createSiteSettingsFragmentImpl(
IRemoteFragmentClient remoteFragmentClient, IObjectWrapper fragmentArgs) {
StrictModeWorkaround.apply();
Bundle unwrappedArgs = ObjectWrapper.unwrap(fragmentArgs, Bundle.class);
return new SettingsFragmentImpl(mProfileManager, remoteFragmentClient, unwrappedArgs)
.asISiteSettingsFragment();
}
@Override
public IMediaRouteDialogFragment createMediaRouteDialogFragmentImpl(
IRemoteFragmentClient remoteFragmentClient) {
StrictModeWorkaround.apply();
MediaRouteDialogFragmentImpl fragment =
new MediaRouteDialogFragmentImpl(remoteFragmentClient);
return fragment.asIMediaRouteDialogFragment();
}
@Override
public IProfile getProfile(String profileName) {
StrictModeWorkaround.apply();
boolean isIncognito = "".equals(profileName);
return mProfileManager.getProfile(profileName, isIncognito);
}
@Override
public IProfile getIncognitoProfile(String profileName) {
StrictModeWorkaround.apply();
return mProfileManager.getProfile(profileName, true);
}
@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 getCrashReporterController(
IObjectWrapper appContext, IObjectWrapper remoteContext) {
StrictModeWorkaround.apply();
// This is a no-op if init has already happened.
minimalInitForContext(ObjectWrapper.unwrap(appContext, Context.class),
ObjectWrapper.unwrap(remoteContext, Context.class));
return CrashReporterControllerImpl.getInstance();
}
@Override
public void onReceivedBroadcast(IObjectWrapper appContextWrapper, Intent intent) {
StrictModeWorkaround.apply();
Context context = ObjectWrapper.unwrap(appContextWrapper, Context.class);
if (IntentUtils.handleIntent(intent)) return;
if (intent.getAction().startsWith(DownloadImpl.getIntentPrefix())) {
DownloadImpl.forwardIntent(context, intent, mProfileManager);
}
}
@Override
public void onMediaSessionServiceStarted(IObjectWrapper sessionService, Intent intent) {
StrictModeWorkaround.apply();
MediaSessionManager.serviceStarted(
ObjectWrapper.unwrap(sessionService, Service.class), intent);
}
@Override
public void onMediaSessionServiceDestroyed() {
StrictModeWorkaround.apply();
MediaSessionManager.serviceDestroyed();
}
@Override
public void onRemoteMediaServiceStarted(IObjectWrapper sessionService, Intent intent) {
StrictModeWorkaround.apply();
MediaRouterClientImpl.serviceStarted(
ObjectWrapper.unwrap(sessionService, Service.class), intent);
}
@Override
public void onRemoteMediaServiceDestroyed(int id) {
StrictModeWorkaround.apply();
MediaRouterClientImpl.serviceDestroyed(id);
}
@Override
public IBinder initializeImageDecoder(IObjectWrapper appContext, IObjectWrapper remoteContext) {
StrictModeWorkaround.apply();
assert ContextUtils.getApplicationContext() == null;
CommandLine.init(null);
minimalInitForContext(ObjectWrapper.unwrap(appContext, Context.class),
ObjectWrapper.unwrap(remoteContext, Context.class));
LibraryLoader.getInstance().setLibraryProcessType(
LibraryProcessType.PROCESS_WEBLAYER_CHILD);
LibraryLoader.getInstance().ensureInitialized();
ImageDecoder imageDecoder = new ImageDecoder();
imageDecoder.initializeSandbox();
return imageDecoder;
}
@Override
public IObjectWrapper createGooglePayDataCallbacksService() {
StrictModeWorkaround.apply();
return ObjectWrapper.wrap(GmsBridge.getInstance().createGooglePayDataCallbacksService());
}
@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;
if (WebLayerFactoryImpl.getClientMajorVersion() >= 88) {
try {
RecordHistogram.recordTimesHistogram("WebLayer.Startup.ClassLoaderCreationTime",
sClient.getClassLoaderCreationTime());
RecordHistogram.recordTimesHistogram(
"WebLayer.Startup.ContextCreationTime", sClient.getContextCreationTime());
RecordHistogram.recordTimesHistogram("WebLayer.Startup.WebLayerLoaderCreationTime",
sClient.getWebLayerLoaderCreationTime());
} catch (RemoteException e) {
throw new APICallException(e);
}
}
}
@Override
public String getUserAgentString() {
StrictModeWorkaround.apply();
return WebLayerImplJni.get().getUserAgentString();
}
@Override
public void registerExternalExperimentIDs(String trialName, int[] experimentIDs) {
StrictModeWorkaround.apply();
WebLayerImplJni.get().registerExternalExperimentIDs(experimentIDs);
}
@Override
public IObjectWrapper getApplicationContext() {
return ObjectWrapper.wrap(ContextUtils.getApplicationContext());
}
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 Intent createMediaSessionServiceIntent() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.createMediaSessionServiceIntent();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static Intent createImageDecoderServiceIntent() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.createImageDecoderServiceIntent();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static int getMediaSessionNotificationId() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.getMediaSessionNotificationId();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static Intent createRemoteMediaServiceIntent() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.createRemoteMediaServiceIntent();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static int getPresentationApiNotificationId() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.getPresentationApiNotificationId();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static int getRemotePlaybackApiNotificationId() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.getRemotePlaybackApiNotificationId();
} 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();
}
public static boolean isLocationPermissionManaged(Origin origin) {
if (origin == null) {
return false;
}
return WebLayerImplJni.get().isLocationPermissionManaged(origin.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(Context appContext, Context remoteContext) {
if (ContextUtils.getApplicationContext() != null) {
return ContextUtils.getApplicationContext();
}
assert remoteContext != null;
Context lightContext = createContextForMode(remoteContext, Configuration.UI_MODE_NIGHT_NO);
Context darkContext = createContextForMode(remoteContext, Configuration.UI_MODE_NIGHT_YES);
ClassLoaderContextWrapperFactory.setLightDarkResourceOverrideContext(
lightContext, darkContext);
int lightPackageId = forceCorrectPackageId(lightContext);
int darkPackageId = forceCorrectPackageId(darkContext);
assert lightPackageId == darkPackageId;
// TODO: The call to onResourcesLoaded() can be slow, we may need to parallelize this with
// other expensive startup tasks.
org.chromium.base.R.onResourcesLoaded(lightPackageId);
// 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. Package ID: " + packageId
+ ", 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 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 && supportsBindingToWebViewService(appContext, implPackageName)) {
// When loading from a WebView implementation, use WebView's declared external services
// as our renderers. This means on O+ we benefit from the webview zygote process, and on
// other versions we ensure the client app doesn't slow down isolated process startup.
// 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 supportsBindingToWebViewService(Context context, String packageName) {
// BIND_EXTERNAL_SERVICE is not supported before N.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return false;
}
// Android N has issues with WebView with the non-system user.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
try {
PackageInfo packageInfo =
context.getPackageManager().getPackageInfo(packageName, 0);
// Package may be disabled for non-system users.
if (!packageInfo.applicationInfo.enabled) {
return false;
}
} catch (PackageManager.NameNotFoundException e) {
// Package may be uninstalled for non-system users.
return false;
}
}
return true;
}
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) {
// TODO(crbug.com/1112001): Investigate why loading classes causes strict mode
// violations in some situations.
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
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.");
}
}
private static Context createContextForMode(Context remoteContext, int uiMode) {
Configuration configuration = new Configuration();
configuration.uiMode = uiMode;
return remoteContext.createConfigurationContext(configuration);
}
@CalledByNative
@Nullable
private static String getEmbedderName() {
return getClientApplicationName();
}
/*
* Android O MR1 has a bug where bg-dexopt-job will break optimized dex files for isolated
* splits. This leads to *very* slow startup on those devices. To mitigate this, we attempt
* to force a dex compile if necessary.
*/
private static void performDexFixIfNecessary(PackageInfo packageInfo) {
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1) {
return;
}
PostTask.postTask(TaskTraits.BEST_EFFORT_MAY_BLOCK, () -> {
ApplicationInfo appInfo = packageInfo.applicationInfo;
String[] splitNames = ApiHelperForO.getSplitNames(appInfo);
if (splitNames == null) {
return;
}
for (int i = 0; i < splitNames.length; i++) {
String splitName = splitNames[i];
// WebLayer depends on the "weblayer" split and "chrome" split (if running in
// Monochrome).
if (!splitName.equals("chrome") && !splitName.equals("weblayer")) {
continue;
}
String splitDir = appInfo.splitSourceDirs[i];
try {
if (DexFile.isDexOptNeeded(splitDir)) {
String cmd = String.format("cmd package compile -r shared --split %s %s",
new File(splitDir).getName(), packageInfo.packageName);
Runtime.getRuntime().exec(cmd);
}
} catch (IOException e) {
Log.e(TAG, "Error fixing dex files.", e);
}
}
});
}
@NativeMethods
interface Natives {
void setRemoteDebuggingEnabled(boolean enabled);
boolean isRemoteDebuggingEnabled();
void setIsWebViewCompatMode(boolean value);
String getUserAgentString();
void registerExternalExperimentIDs(int[] experimentIDs);
boolean isLocationPermissionManaged(String origin);
}
}