1 // Copyright 2018 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base; 6 7 import android.app.Activity; 8 import android.app.Application; 9 import android.content.Context; 10 import android.content.ContextWrapper; 11 import android.content.pm.ApplicationInfo; 12 import android.content.pm.PackageManager; 13 import android.os.Build; 14 import android.os.Bundle; 15 import android.util.ArrayMap; 16 import android.view.LayoutInflater; 17 18 import androidx.annotation.Nullable; 19 import androidx.annotation.RequiresApi; 20 21 import dalvik.system.BaseDexClassLoader; 22 import dalvik.system.PathClassLoader; 23 24 import org.chromium.base.annotations.CalledByNative; 25 import org.chromium.base.compat.ApiHelperForO; 26 import org.chromium.base.metrics.RecordHistogram; 27 import org.chromium.build.BuildConfig; 28 29 import java.lang.reflect.Field; 30 import java.util.ArrayList; 31 import java.util.Arrays; 32 import java.util.Collections; 33 import java.util.Map; 34 35 /** 36 * Utils for working with android app bundles. 37 * 38 * Important notes about bundle status as interpreted by this class: 39 * 40 * <ul> 41 * <li>If {@link BuildConfig#BUNDLES_SUPPORTED} is false, then we are definitely not in a bundle, 42 * and ProGuard is able to strip out the bundle support library.</li> 43 * <li>If {@link BuildConfig#BUNDLES_SUPPORTED} is true, then we MIGHT be in a bundle. 44 * {@link BundleUtils#sIsBundle} is the source of truth.</li> 45 * </ul> 46 * 47 * We need two fields to store one bit of information here to ensure that ProGuard can optimize out 48 * the bundle support library (since {@link BuildConfig#BUNDLES_SUPPORTED} is final) and so that 49 * we can dynamically set whether or not we're in a bundle for targets that use static shared 50 * library APKs. 51 */ 52 public class BundleUtils { 53 private static final String TAG = "BundleUtils"; 54 private static final String LOADED_SPLITS_KEY = "split_compat_loaded_splits"; 55 private static Boolean sIsBundle; 56 private static final Object sSplitLock = new Object(); 57 58 // This cache is needed to support the workaround for b/172602571, see 59 // createIsolatedSplitContext() for more info. 60 private static final ArrayMap<String, ClassLoader> sCachedClassLoaders = new ArrayMap<>(); 61 62 private static final Map<String, ClassLoader> sInflationClassLoaders = 63 Collections.synchronizedMap(new ArrayMap<>()); 64 private static SplitCompatClassLoader sSplitCompatClassLoaderInstance; 65 66 // List of splits that were loaded during the last run of chrome when 67 // restoring from recents. 68 private static ArrayList<String> sSplitsToRestore; 69 resetForTesting()70 public static void resetForTesting() { 71 sIsBundle = null; 72 sCachedClassLoaders.clear(); 73 sInflationClassLoaders.clear(); 74 sSplitCompatClassLoaderInstance = null; 75 sSplitsToRestore = null; 76 } 77 78 /** 79 * {@link BundleUtils#isBundle()} is not called directly by native because 80 * {@link CalledByNative} prevents inlining, causing the bundle support lib to not be 81 * removed non-bundle builds. 82 * 83 * @return true if the current build is a bundle. 84 */ 85 @CalledByNative isBundleForNative()86 public static boolean isBundleForNative() { 87 return isBundle(); 88 } 89 90 /** 91 * @return true if the current build is a bundle. 92 */ isBundle()93 public static boolean isBundle() { 94 if (!BuildConfig.BUNDLES_SUPPORTED) { 95 return false; 96 } 97 assert sIsBundle != null; 98 return sIsBundle; 99 } 100 setIsBundle(boolean isBundle)101 public static void setIsBundle(boolean isBundle) { 102 sIsBundle = isBundle; 103 } 104 isolatedSplitsEnabled()105 public static boolean isolatedSplitsEnabled() { 106 return BuildConfig.ISOLATED_SPLITS_ENABLED; 107 } 108 109 @RequiresApi(api = Build.VERSION_CODES.O) getSplitApkPath(String splitName)110 private static String getSplitApkPath(String splitName) { 111 ApplicationInfo appInfo = ContextUtils.getApplicationContext().getApplicationInfo(); 112 String[] splitNames = appInfo.splitNames; 113 if (splitNames == null) { 114 return null; 115 } 116 int idx = Arrays.binarySearch(splitNames, splitName); 117 return idx < 0 ? null : appInfo.splitSourceDirs[idx]; 118 } 119 120 /** 121 * Returns whether splitName is installed. Note, this will return false on Android versions 122 * below O, where isolated splits are not supported. 123 */ isIsolatedSplitInstalled(String splitName)124 public static boolean isIsolatedSplitInstalled(String splitName) { 125 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 126 return false; 127 } 128 return getSplitApkPath(splitName) != null; 129 } 130 131 /** 132 * The lock to hold when calling {@link Context#createContextForSplit(String)}. 133 */ getSplitContextLock()134 public static Object getSplitContextLock() { 135 return sSplitLock; 136 } 137 138 /** 139 * Returns a context for the isolated split with the name splitName. This will throw a 140 * RuntimeException if isolated splits are enabled and the split is not installed. If the 141 * current Android version does not support isolated splits, the original context will be 142 * returned. If isolated splits are not enabled for this APK/bundle, the underlying ContextImpl 143 * from the base context will be returned. 144 */ createIsolatedSplitContext(Context base, String splitName)145 public static Context createIsolatedSplitContext(Context base, String splitName) { 146 // Isolated splits are only supported in O+, so just return the base context on other 147 // versions, since this will have access to all splits. 148 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 149 return base; 150 } 151 152 try { 153 Context context; 154 // The Application class handles locking itself using the split context lock. This is 155 // necessary to prevent a possible deadlock, since the application waits for splits 156 // preloading on a background thread. 157 // TODO(crbug.com/1172950): Consider moving preloading logic into //base so we can lock 158 // here. 159 if (isApplicationContext(base)) { 160 context = ApiHelperForO.createContextForSplit(base, splitName); 161 } else { 162 synchronized (getSplitContextLock()) { 163 context = ApiHelperForO.createContextForSplit(base, splitName); 164 } 165 } 166 ClassLoader parent = context.getClassLoader().getParent(); 167 Context appContext = ContextUtils.getApplicationContext(); 168 // If the ClassLoader from the newly created context does not equal either the 169 // BundleUtils ClassLoader (the base module ClassLoader) or the app context ClassLoader 170 // (the chrome module ClassLoader) there must be something messed up in the ClassLoader 171 // cache, see b/172602571. This should be solved for the chrome ClassLoader by 172 // SplitCompatAppComponentFactory, but modules which depend on the chrome module need 173 // special handling here to make sure they have the correct parent. 174 boolean shouldReplaceClassLoader = isolatedSplitsEnabled() 175 && !parent.equals(BundleUtils.class.getClassLoader()) && appContext != null 176 && !parent.equals(appContext.getClassLoader()); 177 synchronized (sCachedClassLoaders) { 178 if (shouldReplaceClassLoader && !sCachedClassLoaders.containsKey(splitName)) { 179 String apkPath = getSplitApkPath(splitName); 180 // The librarySearchPath argument to PathClassLoader is not needed here 181 // because the framework doesn't pass it either, see b/171269960. 182 sCachedClassLoaders.put( 183 splitName, new PathClassLoader(apkPath, appContext.getClassLoader())); 184 } 185 // Always replace the ClassLoader if we have a cached version to make sure all 186 // ClassLoaders are consistent. 187 ClassLoader cachedClassLoader = sCachedClassLoaders.get(splitName); 188 if (cachedClassLoader != null) { 189 if (!cachedClassLoader.equals(context.getClassLoader())) { 190 // Set this for recording the histogram below. 191 shouldReplaceClassLoader = true; 192 replaceClassLoader(context, cachedClassLoader); 193 } 194 } else { 195 sCachedClassLoaders.put(splitName, context.getClassLoader()); 196 } 197 } 198 RecordHistogram.recordBooleanHistogram( 199 "Android.IsolatedSplits.ClassLoaderReplaced." + splitName, 200 shouldReplaceClassLoader); 201 return context; 202 } catch (PackageManager.NameNotFoundException e) { 203 throw new RuntimeException(e); 204 } 205 } 206 207 /** Replaces the ClassLoader of the passed in Context. */ replaceClassLoader(Context baseContext, ClassLoader classLoader)208 public static void replaceClassLoader(Context baseContext, ClassLoader classLoader) { 209 while (baseContext instanceof ContextWrapper) { 210 baseContext = ((ContextWrapper) baseContext).getBaseContext(); 211 } 212 213 try { 214 // baseContext should now be an instance of ContextImpl. 215 Field classLoaderField = baseContext.getClass().getDeclaredField("mClassLoader"); 216 classLoaderField.setAccessible(true); 217 classLoaderField.set(baseContext, classLoader); 218 } catch (ReflectiveOperationException e) { 219 throw new RuntimeException("Error setting ClassLoader.", e); 220 } 221 } 222 223 /* Returns absolute path to a native library in a feature module. */ 224 @CalledByNative 225 @Nullable getNativeLibraryPath(String libraryName, String splitName)226 public static String getNativeLibraryPath(String libraryName, String splitName) { 227 try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { 228 // Due to b/171269960 isolated split class loaders have an empty library path, so check 229 // the base module class loader first which loaded BundleUtils. If the library is not 230 // found there, attempt to construct the correct library path from the split. 231 String path = ((BaseDexClassLoader) BundleUtils.class.getClassLoader()) 232 .findLibrary(libraryName); 233 if (path != null) { 234 return path; 235 } 236 237 // SplitCompat is installed on the application context, so check there for library paths 238 // which were added to that ClassLoader. 239 ClassLoader classLoader = ContextUtils.getApplicationContext().getClassLoader(); 240 // In WebLayer, the class loader will be a WrappedClassLoader. 241 if (classLoader instanceof BaseDexClassLoader) { 242 path = ((BaseDexClassLoader) classLoader).findLibrary(libraryName); 243 } else if (classLoader instanceof WrappedClassLoader) { 244 path = ((WrappedClassLoader) classLoader).findLibrary(libraryName); 245 } 246 if (path != null) { 247 return path; 248 } 249 250 return getSplitApkLibraryPath(libraryName, splitName); 251 } 252 } 253 checkContextClassLoader(Context baseContext, Activity activity)254 public static void checkContextClassLoader(Context baseContext, Activity activity) { 255 ClassLoader activityClassLoader = activity.getClass().getClassLoader(); 256 ClassLoader contextClassLoader = baseContext.getClassLoader(); 257 if (activityClassLoader != contextClassLoader) { 258 Log.w(TAG, "Mismatched ClassLoaders between Activity and context (fixing): %s", 259 activity.getClass()); 260 replaceClassLoader(baseContext, activityClassLoader); 261 } 262 } 263 264 /** 265 * Constructs a new instance of the given class name. If the application context class loader 266 * can load the class, that class loader will be used, otherwise the class loader from the 267 * passed in context will be used. 268 */ newInstance(Context context, String className)269 public static Object newInstance(Context context, String className) { 270 Context appContext = ContextUtils.getApplicationContext(); 271 if (appContext != null && canLoadClass(appContext.getClassLoader(), className)) { 272 context = appContext; 273 } 274 try { 275 return context.getClassLoader().loadClass(className).newInstance(); 276 } catch (ReflectiveOperationException e) { 277 throw new RuntimeException(e); 278 } 279 } 280 281 /** 282 * Creates a context which can access classes from the specified split, but inherits theme 283 * resources from the passed in context. This is useful if a context is needed to inflate 284 * layouts which reference classes from a split. 285 */ createContextForInflation(Context context, String splitName)286 public static Context createContextForInflation(Context context, String splitName) { 287 if (!isIsolatedSplitInstalled(splitName)) { 288 return context; 289 } 290 ClassLoader splitClassLoader = registerSplitClassLoaderForInflation(splitName); 291 return new ContextWrapper(context) { 292 @Override 293 public ClassLoader getClassLoader() { 294 return splitClassLoader; 295 } 296 297 @Override 298 public Object getSystemService(String name) { 299 Object ret = super.getSystemService(name); 300 if (Context.LAYOUT_INFLATER_SERVICE.equals(name)) { 301 ret = ((LayoutInflater) ret).cloneInContext(this); 302 } 303 return ret; 304 } 305 }; 306 } 307 308 /** 309 * Returns the ClassLoader for the given split, loading the split if it has not yet been 310 * loaded. 311 */ 312 public static ClassLoader getOrCreateSplitClassLoader(String splitName) { 313 ClassLoader ret; 314 synchronized (sCachedClassLoaders) { 315 ret = sCachedClassLoaders.get(splitName); 316 } 317 318 if (ret == null) { 319 // Do not hold lock since split loading can be slow. 320 createIsolatedSplitContext(ContextUtils.getApplicationContext(), splitName); 321 synchronized (sCachedClassLoaders) { 322 ret = sCachedClassLoaders.get(splitName); 323 assert ret != null; 324 } 325 } 326 return ret; 327 } 328 329 public static ClassLoader registerSplitClassLoaderForInflation(String splitName) { 330 ClassLoader splitClassLoader = getOrCreateSplitClassLoader(splitName); 331 sInflationClassLoaders.put(splitName, splitClassLoader); 332 return splitClassLoader; 333 } 334 335 public static boolean canLoadClass(ClassLoader classLoader, String className) { 336 try { 337 Class.forName(className, false, classLoader); 338 return true; 339 } catch (ClassNotFoundException e) { 340 return false; 341 } 342 } 343 344 public static ClassLoader getSplitCompatClassLoader() { 345 // SplitCompatClassLoader needs to be lazy loaded to ensure the Chrome 346 // context is loaded and its class loader is set as the parent 347 // classloader for the SplitCompatClassLoader. This happens in 348 // Application#attachBaseContext. 349 if (sSplitCompatClassLoaderInstance == null) { 350 sSplitCompatClassLoaderInstance = new SplitCompatClassLoader(); 351 } 352 return sSplitCompatClassLoaderInstance; 353 } 354 355 public static void saveLoadedSplits(Bundle outState) { 356 outState.putStringArrayList( 357 LOADED_SPLITS_KEY, new ArrayList(sInflationClassLoaders.keySet())); 358 } 359 360 public static void restoreLoadedSplits(Bundle savedInstanceState) { 361 if (savedInstanceState == null) { 362 return; 363 } 364 sSplitsToRestore = savedInstanceState.getStringArrayList(LOADED_SPLITS_KEY); 365 } 366 367 private static class SplitCompatClassLoader extends ClassLoader { 368 private static final String TAG = "SplitCompatClassLoader"; 369 370 public SplitCompatClassLoader() { 371 // The chrome split classloader if the chrome split exists, otherwise 372 // the base module class loader. 373 super(ContextUtils.getApplicationContext().getClassLoader()); 374 Log.i(TAG, "Splits: %s", sSplitsToRestore); 375 } 376 377 private Class<?> checkSplitsClassLoaders(String className) throws ClassNotFoundException { 378 for (ClassLoader cl : sInflationClassLoaders.values()) { 379 try { 380 return cl.loadClass(className); 381 } catch (ClassNotFoundException ignore) { 382 } 383 } 384 return null; 385 } 386 387 /** 388 * Loads the class with the specified binary name. 389 */ 390 @Override 391 public Class<?> findClass(String cn) throws ClassNotFoundException { 392 Class<?> foundClass = checkSplitsClassLoaders(cn); 393 if (foundClass != null) { 394 return foundClass; 395 } 396 // We will never have android.* classes in isolated split class loaders, 397 // but android framework inflater does sometimes try loading classes 398 // that do not exist when inflating xml files on startup. 399 if (sSplitsToRestore != null && !cn.startsWith("android.")) { 400 // If we fail from all the currently loaded classLoaders, lets 401 // try loading some splits that were loaded when chrome was last 402 // run and check again. 403 restoreSplitsClassLoaders(); 404 foundClass = checkSplitsClassLoaders(cn); 405 if (foundClass != null) { 406 return foundClass; 407 } 408 } 409 Log.w(TAG, "No class %s amongst %s", cn, sInflationClassLoaders.keySet()); 410 throw new ClassNotFoundException(cn); 411 } 412 413 private void restoreSplitsClassLoaders() { 414 // Load splits that were stored in the SavedInstanceState Bundle. 415 for (String splitName : sSplitsToRestore) { 416 if (!sInflationClassLoaders.containsKey(splitName)) { 417 registerSplitClassLoaderForInflation(splitName); 418 } 419 } 420 sSplitsToRestore = null; 421 } 422 } 423 424 @Nullable 425 private static String getSplitApkLibraryPath(String libraryName, String splitName) { 426 // If isolated splits aren't supported, the library should have already been found. 427 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 428 return null; 429 } 430 431 String apkPath = getSplitApkPath(splitName); 432 if (apkPath == null) { 433 return null; 434 } 435 436 try { 437 ApplicationInfo info = ContextUtils.getApplicationContext().getApplicationInfo(); 438 String primaryCpuAbi = (String) info.getClass().getField("primaryCpuAbi").get(info); 439 // This matches the logic LoadedApk.java uses to construct library paths. 440 return apkPath + "!/lib/" + primaryCpuAbi + "/" + System.mapLibraryName(libraryName); 441 } catch (ReflectiveOperationException e) { 442 throw new RuntimeException(e); 443 } 444 } 445 446 private static boolean isApplicationContext(Context context) { 447 while (context instanceof ContextWrapper) { 448 if (context instanceof Application) return true; 449 context = ((ContextWrapper) context).getBaseContext(); 450 } 451 return false; 452 } 453 } 454