463 lines
19 KiB
Java
463 lines
19 KiB
Java
![]() |
// Copyright 2018 The Chromium Authors
|
||
|
// Use of this source code is governed by a BSD-style license that can be
|
||
|
// found in the LICENSE file.
|
||
|
|
||
|
package org.chromium.base;
|
||
|
|
||
|
import android.app.Activity;
|
||
|
import android.app.Application;
|
||
|
import android.content.Context;
|
||
|
import android.content.ContextWrapper;
|
||
|
import android.content.pm.ApplicationInfo;
|
||
|
import android.content.pm.PackageManager;
|
||
|
import android.os.Build;
|
||
|
import android.os.Bundle;
|
||
|
import android.text.TextUtils;
|
||
|
import android.util.ArrayMap;
|
||
|
import android.view.LayoutInflater;
|
||
|
|
||
|
import androidx.annotation.Nullable;
|
||
|
import androidx.annotation.RequiresApi;
|
||
|
|
||
|
import dalvik.system.BaseDexClassLoader;
|
||
|
import dalvik.system.PathClassLoader;
|
||
|
|
||
|
import org.jni_zero.CalledByNative;
|
||
|
|
||
|
import org.chromium.base.compat.ApiHelperForO;
|
||
|
import org.chromium.base.metrics.RecordHistogram;
|
||
|
import org.chromium.build.BuildConfig;
|
||
|
|
||
|
import java.lang.reflect.Field;
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Arrays;
|
||
|
import java.util.Collections;
|
||
|
import java.util.Map;
|
||
|
|
||
|
/**
|
||
|
* Utils for working with android app bundles.
|
||
|
*
|
||
|
* Important notes about bundle status as interpreted by this class:
|
||
|
*
|
||
|
* <ul>
|
||
|
* <li>If {@link BuildConfig#BUNDLES_SUPPORTED} is false, then we are definitely not in a bundle,
|
||
|
* and ProGuard is able to strip out the bundle support library.</li>
|
||
|
* <li>If {@link BuildConfig#BUNDLES_SUPPORTED} is true, then we MIGHT be in a bundle.
|
||
|
* {@link BundleUtils#sIsBundle} is the source of truth.</li>
|
||
|
* </ul>
|
||
|
*
|
||
|
* We need two fields to store one bit of information here to ensure that ProGuard can optimize out
|
||
|
* the bundle support library (since {@link BuildConfig#BUNDLES_SUPPORTED} is final) and so that
|
||
|
* we can dynamically set whether or not we're in a bundle for targets that use static shared
|
||
|
* library APKs.
|
||
|
*/
|
||
|
public class BundleUtils {
|
||
|
private static final String TAG = "BundleUtils";
|
||
|
private static final String LOADED_SPLITS_KEY = "split_compat_loaded_splits";
|
||
|
private static Boolean sIsBundle;
|
||
|
private static final Object sSplitLock = new Object();
|
||
|
|
||
|
// This cache is needed to support the workaround for b/172602571, see
|
||
|
// createIsolatedSplitContext() for more info.
|
||
|
private static final ArrayMap<String, ClassLoader> sCachedClassLoaders = new ArrayMap<>();
|
||
|
|
||
|
private static final Map<String, ClassLoader> sInflationClassLoaders =
|
||
|
Collections.synchronizedMap(new ArrayMap<>());
|
||
|
private static SplitCompatClassLoader sSplitCompatClassLoaderInstance;
|
||
|
|
||
|
// List of splits that were loaded during the last run of chrome when
|
||
|
// restoring from recents.
|
||
|
private static ArrayList<String> sSplitsToRestore;
|
||
|
|
||
|
public static void resetForTesting() {
|
||
|
sIsBundle = null;
|
||
|
sCachedClassLoaders.clear();
|
||
|
sInflationClassLoaders.clear();
|
||
|
sSplitCompatClassLoaderInstance = null;
|
||
|
sSplitsToRestore = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* {@link BundleUtils#isBundle()} is not called directly by native because
|
||
|
* {@link CalledByNative} prevents inlining, causing the bundle support lib to not be
|
||
|
* removed non-bundle builds.
|
||
|
*
|
||
|
* @return true if the current build is a bundle.
|
||
|
*/
|
||
|
@CalledByNative
|
||
|
public static boolean isBundleForNative() {
|
||
|
return isBundle();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return true if the current build is a bundle.
|
||
|
*/
|
||
|
public static boolean isBundle() {
|
||
|
if (!BuildConfig.BUNDLES_SUPPORTED) {
|
||
|
return false;
|
||
|
}
|
||
|
assert sIsBundle != null;
|
||
|
return sIsBundle;
|
||
|
}
|
||
|
|
||
|
public static void setIsBundle(boolean isBundle) {
|
||
|
sIsBundle = isBundle;
|
||
|
}
|
||
|
|
||
|
public static boolean isolatedSplitsEnabled() {
|
||
|
return BuildConfig.ISOLATED_SPLITS_ENABLED;
|
||
|
}
|
||
|
|
||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||
|
private static String getSplitApkPath(String splitName) {
|
||
|
ApplicationInfo appInfo = ContextUtils.getApplicationContext().getApplicationInfo();
|
||
|
String[] splitNames = appInfo.splitNames;
|
||
|
if (splitNames == null) {
|
||
|
return null;
|
||
|
}
|
||
|
int idx = Arrays.binarySearch(splitNames, splitName);
|
||
|
return idx < 0 ? null : appInfo.splitSourceDirs[idx];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether splitName is installed. Note, this will return false on Android versions
|
||
|
* below O, where isolated splits are not supported.
|
||
|
*/
|
||
|
public static boolean isIsolatedSplitInstalled(String splitName) {
|
||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||
|
return false;
|
||
|
}
|
||
|
return getSplitApkPath(splitName) != null;
|
||
|
}
|
||
|
|
||
|
/** The lock to hold when calling {@link Context#createContextForSplit(String)}. */
|
||
|
public static Object getSplitContextLock() {
|
||
|
return sSplitLock;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a context for the isolated split with the name splitName. This will throw a
|
||
|
* RuntimeException if isolated splits are enabled and the split is not installed. If the
|
||
|
* current Android version does not support isolated splits, the original context will be
|
||
|
* returned. If isolated splits are not enabled for this APK/bundle, the underlying ContextImpl
|
||
|
* from the base context will be returned.
|
||
|
*/
|
||
|
public static Context createIsolatedSplitContext(Context base, String splitName) {
|
||
|
// Isolated splits are only supported in O+, so just return the base context on other
|
||
|
// versions, since this will have access to all splits.
|
||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||
|
return base;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
Context context;
|
||
|
// The Application class handles locking itself using the split context lock. This is
|
||
|
// necessary to prevent a possible deadlock, since the application waits for splits
|
||
|
// preloading on a background thread.
|
||
|
// TODO(crbug.com/1172950): Consider moving preloading logic into //base so we can lock
|
||
|
// here.
|
||
|
if (isApplicationContext(base)) {
|
||
|
context = ApiHelperForO.createContextForSplit(base, splitName);
|
||
|
} else {
|
||
|
synchronized (getSplitContextLock()) {
|
||
|
context = ApiHelperForO.createContextForSplit(base, splitName);
|
||
|
}
|
||
|
}
|
||
|
ClassLoader parent = context.getClassLoader().getParent();
|
||
|
Context appContext = ContextUtils.getApplicationContext();
|
||
|
// If the ClassLoader from the newly created context does not equal either the
|
||
|
// BundleUtils ClassLoader (the base module ClassLoader) or the app context ClassLoader
|
||
|
// (the chrome module ClassLoader) there must be something messed up in the ClassLoader
|
||
|
// cache, see b/172602571. This should be solved for the chrome ClassLoader by
|
||
|
// SplitCompatAppComponentFactory, but modules which depend on the chrome module need
|
||
|
// special handling here to make sure they have the correct parent.
|
||
|
boolean shouldReplaceClassLoader =
|
||
|
isolatedSplitsEnabled()
|
||
|
&& !parent.equals(BundleUtils.class.getClassLoader())
|
||
|
&& appContext != null
|
||
|
&& !parent.equals(appContext.getClassLoader());
|
||
|
synchronized (sCachedClassLoaders) {
|
||
|
if (shouldReplaceClassLoader && !sCachedClassLoaders.containsKey(splitName)) {
|
||
|
String apkPath = getSplitApkPath(splitName);
|
||
|
// The librarySearchPath argument to PathClassLoader is not needed here
|
||
|
// because the framework doesn't pass it either, see b/171269960.
|
||
|
sCachedClassLoaders.put(
|
||
|
splitName, new PathClassLoader(apkPath, appContext.getClassLoader()));
|
||
|
}
|
||
|
// Always replace the ClassLoader if we have a cached version to make sure all
|
||
|
// ClassLoaders are consistent.
|
||
|
ClassLoader cachedClassLoader = sCachedClassLoaders.get(splitName);
|
||
|
if (cachedClassLoader != null) {
|
||
|
if (!cachedClassLoader.equals(context.getClassLoader())) {
|
||
|
// Set this for recording the histogram below.
|
||
|
shouldReplaceClassLoader = true;
|
||
|
replaceClassLoader(context, cachedClassLoader);
|
||
|
}
|
||
|
} else {
|
||
|
sCachedClassLoaders.put(splitName, context.getClassLoader());
|
||
|
}
|
||
|
}
|
||
|
RecordHistogram.recordBooleanHistogram(
|
||
|
"Android.IsolatedSplits.ClassLoaderReplaced." + splitName,
|
||
|
shouldReplaceClassLoader);
|
||
|
return context;
|
||
|
} catch (PackageManager.NameNotFoundException e) {
|
||
|
throw new RuntimeException(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Replaces the ClassLoader of the passed in Context. */
|
||
|
public static void replaceClassLoader(Context baseContext, ClassLoader classLoader) {
|
||
|
while (baseContext instanceof ContextWrapper) {
|
||
|
baseContext = ((ContextWrapper) baseContext).getBaseContext();
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
// baseContext should now be an instance of ContextImpl.
|
||
|
Field classLoaderField = baseContext.getClass().getDeclaredField("mClassLoader");
|
||
|
classLoaderField.setAccessible(true);
|
||
|
classLoaderField.set(baseContext, classLoader);
|
||
|
} catch (ReflectiveOperationException e) {
|
||
|
throw new RuntimeException("Error setting ClassLoader.", e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* Returns absolute path to a native library in a feature module. */
|
||
|
@CalledByNative
|
||
|
@Nullable
|
||
|
public static String getNativeLibraryPath(String libraryName, String splitName) {
|
||
|
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
|
||
|
// Due to b/171269960 isolated split class loaders have an empty library path, so check
|
||
|
// the base module class loader first which loaded BundleUtils. If the library is not
|
||
|
// found there, attempt to construct the correct library path from the split.
|
||
|
String path =
|
||
|
((BaseDexClassLoader) BundleUtils.class.getClassLoader())
|
||
|
.findLibrary(libraryName);
|
||
|
if (path != null) {
|
||
|
return path;
|
||
|
}
|
||
|
|
||
|
// SplitCompat is installed on the application context, so check there for library paths
|
||
|
// which were added to that ClassLoader.
|
||
|
ClassLoader classLoader = ContextUtils.getApplicationContext().getClassLoader();
|
||
|
// In WebLayer, the class loader will be a WrappedClassLoader.
|
||
|
if (classLoader instanceof BaseDexClassLoader) {
|
||
|
path = ((BaseDexClassLoader) classLoader).findLibrary(libraryName);
|
||
|
} else if (classLoader instanceof WrappedClassLoader) {
|
||
|
path = ((WrappedClassLoader) classLoader).findLibrary(libraryName);
|
||
|
}
|
||
|
if (path != null) {
|
||
|
return path;
|
||
|
}
|
||
|
|
||
|
return getSplitApkLibraryPath(libraryName, splitName);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void checkContextClassLoader(Context baseContext, Activity activity) {
|
||
|
ClassLoader activityClassLoader = activity.getClass().getClassLoader();
|
||
|
ClassLoader contextClassLoader = baseContext.getClassLoader();
|
||
|
if (activityClassLoader != contextClassLoader) {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
"Mismatched ClassLoaders between Activity and context (fixing): %s",
|
||
|
activity.getClass());
|
||
|
replaceClassLoader(baseContext, activityClassLoader);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Constructs a new instance of the given class name. If the application context class loader
|
||
|
* can load the class, that class loader will be used, otherwise the class loader from the
|
||
|
* passed in context will be used.
|
||
|
*/
|
||
|
public static Object newInstance(Context context, String className) {
|
||
|
Context appContext = ContextUtils.getApplicationContext();
|
||
|
if (appContext != null && canLoadClass(appContext.getClassLoader(), className)) {
|
||
|
context = appContext;
|
||
|
}
|
||
|
try {
|
||
|
return context.getClassLoader().loadClass(className).newInstance();
|
||
|
} catch (ReflectiveOperationException e) {
|
||
|
throw new RuntimeException(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a context which can access classes from the specified split, but inherits theme
|
||
|
* resources from the passed in context. This is useful if a context is needed to inflate
|
||
|
* layouts which reference classes from a split.
|
||
|
*/
|
||
|
public static Context createContextForInflation(Context context, String splitName) {
|
||
|
if (!isIsolatedSplitInstalled(splitName)) {
|
||
|
return context;
|
||
|
}
|
||
|
ClassLoader splitClassLoader = registerSplitClassLoaderForInflation(splitName);
|
||
|
return new ContextWrapper(context) {
|
||
|
@Override
|
||
|
public ClassLoader getClassLoader() {
|
||
|
return splitClassLoader;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Object getSystemService(String name) {
|
||
|
Object ret = super.getSystemService(name);
|
||
|
if (Context.LAYOUT_INFLATER_SERVICE.equals(name)) {
|
||
|
ret = ((LayoutInflater) ret).cloneInContext(this);
|
||
|
}
|
||
|
return ret;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the ClassLoader for the given split, loading the split if it has not yet been
|
||
|
* loaded.
|
||
|
*/
|
||
|
public static ClassLoader getOrCreateSplitClassLoader(String splitName) {
|
||
|
ClassLoader ret;
|
||
|
synchronized (sCachedClassLoaders) {
|
||
|
ret = sCachedClassLoaders.get(splitName);
|
||
|
}
|
||
|
|
||
|
if (ret == null) {
|
||
|
// Do not hold lock since split loading can be slow.
|
||
|
createIsolatedSplitContext(ContextUtils.getApplicationContext(), splitName);
|
||
|
synchronized (sCachedClassLoaders) {
|
||
|
ret = sCachedClassLoaders.get(splitName);
|
||
|
assert ret != null;
|
||
|
}
|
||
|
}
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
public static ClassLoader registerSplitClassLoaderForInflation(String splitName) {
|
||
|
ClassLoader splitClassLoader = getOrCreateSplitClassLoader(splitName);
|
||
|
sInflationClassLoaders.put(splitName, splitClassLoader);
|
||
|
return splitClassLoader;
|
||
|
}
|
||
|
|
||
|
public static boolean canLoadClass(ClassLoader classLoader, String className) {
|
||
|
try {
|
||
|
Class.forName(className, false, classLoader);
|
||
|
return true;
|
||
|
} catch (ClassNotFoundException e) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static ClassLoader getSplitCompatClassLoader() {
|
||
|
// SplitCompatClassLoader needs to be lazy loaded to ensure the Chrome
|
||
|
// context is loaded and its class loader is set as the parent
|
||
|
// classloader for the SplitCompatClassLoader. This happens in
|
||
|
// Application#attachBaseContext.
|
||
|
if (sSplitCompatClassLoaderInstance == null) {
|
||
|
sSplitCompatClassLoaderInstance = new SplitCompatClassLoader();
|
||
|
}
|
||
|
return sSplitCompatClassLoaderInstance;
|
||
|
}
|
||
|
|
||
|
public static void saveLoadedSplits(Bundle outState) {
|
||
|
outState.putStringArrayList(
|
||
|
LOADED_SPLITS_KEY, new ArrayList(sInflationClassLoaders.keySet()));
|
||
|
}
|
||
|
|
||
|
public static void restoreLoadedSplits(Bundle savedInstanceState) {
|
||
|
if (savedInstanceState == null) {
|
||
|
return;
|
||
|
}
|
||
|
sSplitsToRestore = savedInstanceState.getStringArrayList(LOADED_SPLITS_KEY);
|
||
|
}
|
||
|
|
||
|
private static class SplitCompatClassLoader extends ClassLoader {
|
||
|
private static final String TAG = "SplitCompatClassLoader";
|
||
|
|
||
|
public SplitCompatClassLoader() {
|
||
|
// The chrome split classloader if the chrome split exists, otherwise
|
||
|
// the base module class loader.
|
||
|
super(ContextUtils.getApplicationContext().getClassLoader());
|
||
|
Log.i(TAG, "Splits: %s", sSplitsToRestore);
|
||
|
}
|
||
|
|
||
|
private Class<?> checkSplitsClassLoaders(String className) throws ClassNotFoundException {
|
||
|
for (ClassLoader cl : sInflationClassLoaders.values()) {
|
||
|
try {
|
||
|
return cl.loadClass(className);
|
||
|
} catch (ClassNotFoundException ignore) {
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/** Loads the class with the specified binary name. */
|
||
|
@Override
|
||
|
public Class<?> findClass(String cn) throws ClassNotFoundException {
|
||
|
Class<?> foundClass = checkSplitsClassLoaders(cn);
|
||
|
if (foundClass != null) {
|
||
|
return foundClass;
|
||
|
}
|
||
|
// We will never have android.* classes in isolated split class loaders,
|
||
|
// but android framework inflater does sometimes try loading classes
|
||
|
// that do not exist when inflating xml files on startup.
|
||
|
if (!cn.startsWith("android.")) {
|
||
|
// If we fail from all the currently loaded classLoaders, lets
|
||
|
// try loading some splits that were loaded when chrome was last
|
||
|
// run and check again.
|
||
|
if (sSplitsToRestore != null) {
|
||
|
restoreSplitsClassLoaders();
|
||
|
foundClass = checkSplitsClassLoaders(cn);
|
||
|
if (foundClass != null) {
|
||
|
return foundClass;
|
||
|
}
|
||
|
}
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
"No class %s amongst %s",
|
||
|
cn,
|
||
|
TextUtils.join("\n", sInflationClassLoaders.keySet()));
|
||
|
}
|
||
|
throw new ClassNotFoundException(cn);
|
||
|
}
|
||
|
|
||
|
private void restoreSplitsClassLoaders() {
|
||
|
// Load splits that were stored in the SavedInstanceState Bundle.
|
||
|
for (String splitName : sSplitsToRestore) {
|
||
|
if (!sInflationClassLoaders.containsKey(splitName)) {
|
||
|
registerSplitClassLoaderForInflation(splitName);
|
||
|
}
|
||
|
}
|
||
|
sSplitsToRestore = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private static String getSplitApkLibraryPath(String libraryName, String splitName) {
|
||
|
// If isolated splits aren't supported, the library should have already been found.
|
||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
String apkPath = getSplitApkPath(splitName);
|
||
|
if (apkPath == null) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
ApplicationInfo info = ContextUtils.getApplicationContext().getApplicationInfo();
|
||
|
String primaryCpuAbi = (String) info.getClass().getField("primaryCpuAbi").get(info);
|
||
|
// This matches the logic LoadedApk.java uses to construct library paths.
|
||
|
return apkPath + "!/lib/" + primaryCpuAbi + "/" + System.mapLibraryName(libraryName);
|
||
|
} catch (ReflectiveOperationException e) {
|
||
|
throw new RuntimeException(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static boolean isApplicationContext(Context context) {
|
||
|
while (context instanceof ContextWrapper) {
|
||
|
if (context instanceof Application) return true;
|
||
|
context = ((ContextWrapper) context).getBaseContext();
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
}
|