• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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