• 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.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