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