blob: 2d4fe3f32a4035c65306352151fcd742fe0ed7e3 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package 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 androidx.annotation.Nullable;
import androidx.collection.SimpleArrayMap;
import dalvik.system.BaseDexClassLoader;
import dalvik.system.PathClassLoader;
import org.chromium.base.annotations.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.Arrays;
/**
* 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 final class BundleUtils {
private static final String TAG = "BundleUtils";
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 SimpleArrayMap<String, ClassLoader> sCachedClassLoaders =
new SimpleArrayMap<>();
/**
* {@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;
}
/**
* 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(Context context, String splitName) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false;
}
String[] splitNames = ApiHelperForO.getSplitNames(context.getApplicationInfo());
return splitNames != null && Arrays.asList(splitNames).contains(splitName);
}
/**
* 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[] splitNames = ApiHelperForO.getSplitNames(context.getApplicationInfo());
int idx = Arrays.binarySearch(splitNames, splitName);
assert idx >= 0;
// 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(context.getApplicationInfo().splitSourceDirs[idx],
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);
}
}
// TODO(crbug.com/1150459): Remove this once //clank callers have been converted to the new
// version.
@Nullable
public static String getNativeLibraryPath(String libraryName) {
return getNativeLibraryPath(libraryName, "");
}
@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;
}
ApplicationInfo info = ContextUtils.getApplicationContext().getApplicationInfo();
String[] splitNames = ApiHelperForO.getSplitNames(info);
if (splitNames == null) {
return null;
}
int idx = Arrays.binarySearch(splitNames, splitName);
if (idx < 0) {
return null;
}
try {
String primaryCpuAbi = (String) info.getClass().getField("primaryCpuAbi").get(info);
// This matches the logic LoadedApk.java uses to construct library paths.
return info.splitSourceDirs[idx] + "!/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;
}
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);
}
}
}