1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.multidex;
18 
19 import android.app.Application;
20 import android.content.Context;
21 import android.content.pm.ApplicationInfo;
22 import android.content.pm.PackageManager;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.os.Build;
25 import android.util.Log;
26 
27 import dalvik.system.DexFile;
28 
29 import java.io.File;
30 import java.io.IOException;
31 import java.lang.reflect.Array;
32 import java.lang.reflect.Field;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.ListIterator;
40 import java.util.Set;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43 import java.util.zip.ZipFile;
44 
45 /**
46  * Monkey patches {@link Context#getClassLoader() the application context class
47  * loader} in order to load classes from more than one dex file. The primary
48  * {@code classes.dex} must contain the classes necessary for calling this
49  * class methods. Secondary dex files named classes2.dex, classes3.dex... found
50  * in the application apk will be added to the classloader after first call to
51  * {@link #install(Context)}.
52  *
53  * <p/>
54  * This library provides compatibility for platforms with API level 4 through 20. This library does
55  * nothing on newer versions of the platform which provide built-in support for secondary dex files.
56  */
57 public final class MultiDex {
58 
59     static final String TAG = "MultiDex";
60 
61     private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
62 
63     private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +
64         "secondary-dexes";
65 
66     private static final int MAX_SUPPORTED_SDK_VERSION = 20;
67 
68     private static final int MIN_SDK_VERSION = 4;
69 
70     private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
71 
72     private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
73 
74     private static final Set<String> installedApk = new HashSet<String>();
75 
76     private static final boolean IS_VM_MULTIDEX_CAPABLE =
77             isVMMultidexCapable(System.getProperty("java.vm.version"));
78 
MultiDex()79     private MultiDex() {}
80 
81     /**
82      * Patches the application context class loader by appending extra dex files
83      * loaded from the application apk. This method should be called in the
84      * attachBaseContext of your {@link Application}, see
85      * {@link MultiDexApplication} for more explanation and an example.
86      *
87      * @param context application context.
88      * @throws RuntimeException if an error occurred preventing the classloader
89      *         extension.
90      */
install(Context context)91     public static void install(Context context) {
92         Log.i(TAG, "install");
93         if (IS_VM_MULTIDEX_CAPABLE) {
94             Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
95             return;
96         }
97 
98         if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
99             throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
100                     + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
101         }
102 
103         try {
104             ApplicationInfo applicationInfo = getApplicationInfo(context);
105             if (applicationInfo == null) {
106                 // Looks like running on a test Context, so just return without patching.
107                 return;
108             }
109 
110             synchronized (installedApk) {
111                 String apkPath = applicationInfo.sourceDir;
112                 if (installedApk.contains(apkPath)) {
113                     return;
114                 }
115                 installedApk.add(apkPath);
116 
117                 if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
118                     Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
119                             + Build.VERSION.SDK_INT + ": SDK version higher than "
120                             + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
121                             + "runtime with built-in multidex capabilty but it's not the "
122                             + "case here: java.vm.version=\""
123                             + System.getProperty("java.vm.version") + "\"");
124                 }
125 
126                 /* The patched class loader is expected to be a descendant of
127                  * dalvik.system.BaseDexClassLoader. We modify its
128                  * dalvik.system.DexPathList pathList field to append additional DEX
129                  * file entries.
130                  */
131                 ClassLoader loader;
132                 try {
133                     loader = context.getClassLoader();
134                 } catch (RuntimeException e) {
135                     /* Ignore those exceptions so that we don't break tests relying on Context like
136                      * a android.test.mock.MockContext or a android.content.ContextWrapper with a
137                      * null base Context.
138                      */
139                     Log.w(TAG, "Failure while trying to obtain Context class loader. " +
140                             "Must be running in test mode. Skip patching.", e);
141                     return;
142                 }
143                 if (loader == null) {
144                     // Note, the context class loader is null when running Robolectric tests.
145                     Log.e(TAG,
146                             "Context class loader is null. Must be running in test mode. "
147                             + "Skip patching.");
148                     return;
149                 }
150 
151                 try {
152                   clearOldDexDir(context);
153                 } catch (Throwable t) {
154                   Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
155                       + "continuing without cleaning.", t);
156                 }
157 
158                 File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
159                 List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
160                 if (checkValidZipFiles(files)) {
161                     installSecondaryDexes(loader, dexDir, files);
162                 } else {
163                     Log.w(TAG, "Files were not valid zip files.  Forcing a reload.");
164                     // Try again, but this time force a reload of the zip file.
165                     files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
166 
167                     if (checkValidZipFiles(files)) {
168                         installSecondaryDexes(loader, dexDir, files);
169                     } else {
170                         // Second time didn't work, give up
171                         throw new RuntimeException("Zip files were not valid.");
172                     }
173                 }
174             }
175 
176         } catch (Exception e) {
177             Log.e(TAG, "Multidex installation failure", e);
178             throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
179         }
180         Log.i(TAG, "install done");
181     }
182 
getApplicationInfo(Context context)183     private static ApplicationInfo getApplicationInfo(Context context)
184             throws NameNotFoundException {
185         PackageManager pm;
186         String packageName;
187         try {
188             pm = context.getPackageManager();
189             packageName = context.getPackageName();
190         } catch (RuntimeException e) {
191             /* Ignore those exceptions so that we don't break tests relying on Context like
192              * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
193              * base Context.
194              */
195             Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
196                     "Must be running in test mode. Skip patching.", e);
197             return null;
198         }
199         if (pm == null || packageName == null) {
200             // This is most likely a mock context, so just return without patching.
201             return null;
202         }
203         ApplicationInfo applicationInfo =
204                 pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
205         return applicationInfo;
206     }
207 
208     /**
209      * Identifies if the current VM has a native support for multidex, meaning there is no need for
210      * additional installation by this library.
211      * @return true if the VM handles multidex
212      */
213     /* package visible for test */
isVMMultidexCapable(String versionString)214     static boolean isVMMultidexCapable(String versionString) {
215         boolean isMultidexCapable = false;
216         if (versionString != null) {
217             Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
218             if (matcher.matches()) {
219                 try {
220                     int major = Integer.parseInt(matcher.group(1));
221                     int minor = Integer.parseInt(matcher.group(2));
222                     isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
223                             || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
224                                     && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
225                 } catch (NumberFormatException e) {
226                     // let isMultidexCapable be false
227                 }
228             }
229         }
230         Log.i(TAG, "VM with version " + versionString +
231                 (isMultidexCapable ?
232                         " has multidex support" :
233                         " does not have multidex support"));
234         return isMultidexCapable;
235     }
236 
installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)237     private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
238             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
239             InvocationTargetException, NoSuchMethodException, IOException {
240         if (!files.isEmpty()) {
241             if (Build.VERSION.SDK_INT >= 19) {
242                 V19.install(loader, files, dexDir);
243             } else if (Build.VERSION.SDK_INT >= 14) {
244                 V14.install(loader, files, dexDir);
245             } else {
246                 V4.install(loader, files);
247             }
248         }
249     }
250 
251     /**
252      * Returns whether all files in the list are valid zip files.  If {@code files} is empty, then
253      * returns true.
254      */
checkValidZipFiles(List<File> files)255     private static boolean checkValidZipFiles(List<File> files) {
256         for (File file : files) {
257             if (!MultiDexExtractor.verifyZipFile(file)) {
258                 return false;
259             }
260         }
261         return true;
262     }
263 
264     /**
265      * Locates a given field anywhere in the class inheritance hierarchy.
266      *
267      * @param instance an object to search the field into.
268      * @param name field name
269      * @return a field object
270      * @throws NoSuchFieldException if the field cannot be located
271      */
findField(Object instance, String name)272     private static Field findField(Object instance, String name) throws NoSuchFieldException {
273         for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
274             try {
275                 Field field = clazz.getDeclaredField(name);
276 
277 
278                 if (!field.isAccessible()) {
279                     field.setAccessible(true);
280                 }
281 
282                 return field;
283             } catch (NoSuchFieldException e) {
284                 // ignore and search next
285             }
286         }
287 
288         throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
289     }
290 
291     /**
292      * Locates a given method anywhere in the class inheritance hierarchy.
293      *
294      * @param instance an object to search the method into.
295      * @param name method name
296      * @param parameterTypes method parameter types
297      * @return a method object
298      * @throws NoSuchMethodException if the method cannot be located
299      */
findMethod(Object instance, String name, Class<?>... parameterTypes)300     private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
301             throws NoSuchMethodException {
302         for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
303             try {
304                 Method method = clazz.getDeclaredMethod(name, parameterTypes);
305 
306 
307                 if (!method.isAccessible()) {
308                     method.setAccessible(true);
309                 }
310 
311                 return method;
312             } catch (NoSuchMethodException e) {
313                 // ignore and search next
314             }
315         }
316 
317         throw new NoSuchMethodException("Method " + name + " with parameters " +
318                 Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
319     }
320 
321     /**
322      * Replace the value of a field containing a non null array, by a new array containing the
323      * elements of the original array plus the elements of extraElements.
324      * @param instance the instance whose field is to be modified.
325      * @param fieldName the field to modify.
326      * @param extraElements elements to append at the end of the array.
327      */
expandFieldArray(Object instance, String fieldName, Object[] extraElements)328     private static void expandFieldArray(Object instance, String fieldName,
329             Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
330             IllegalAccessException {
331         Field jlrField = findField(instance, fieldName);
332         Object[] original = (Object[]) jlrField.get(instance);
333         Object[] combined = (Object[]) Array.newInstance(
334                 original.getClass().getComponentType(), original.length + extraElements.length);
335         System.arraycopy(original, 0, combined, 0, original.length);
336         System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
337         jlrField.set(instance, combined);
338     }
339 
clearOldDexDir(Context context)340     private static void clearOldDexDir(Context context) throws Exception {
341         File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
342         if (dexDir.isDirectory()) {
343             Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
344             File[] files = dexDir.listFiles();
345             if (files == null) {
346                 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
347                 return;
348             }
349             for (File oldFile : files) {
350                 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size "
351                         + oldFile.length());
352                 if (!oldFile.delete()) {
353                     Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
354                 } else {
355                     Log.i(TAG, "Deleted old file " + oldFile.getPath());
356                 }
357             }
358             if (!dexDir.delete()) {
359                 Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath());
360             } else {
361                 Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath());
362             }
363         }
364     }
365 
366     /**
367      * Installer for platform versions 19.
368      */
369     private static final class V19 {
370 
install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory)371         private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
372                 File optimizedDirectory)
373                         throws IllegalArgumentException, IllegalAccessException,
374                         NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
375             /* The patched class loader is expected to be a descendant of
376              * dalvik.system.BaseDexClassLoader. We modify its
377              * dalvik.system.DexPathList pathList field to append additional DEX
378              * file entries.
379              */
380             Field pathListField = findField(loader, "pathList");
381             Object dexPathList = pathListField.get(loader);
382             ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
383             expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
384                     new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
385                     suppressedExceptions));
386             if (suppressedExceptions.size() > 0) {
387                 for (IOException e : suppressedExceptions) {
388                     Log.w(TAG, "Exception in makeDexElement", e);
389                 }
390                 Field suppressedExceptionsField =
391                         findField(loader, "dexElementsSuppressedExceptions");
392                 IOException[] dexElementsSuppressedExceptions =
393                         (IOException[]) suppressedExceptionsField.get(loader);
394 
395                 if (dexElementsSuppressedExceptions == null) {
396                     dexElementsSuppressedExceptions =
397                             suppressedExceptions.toArray(
398                                     new IOException[suppressedExceptions.size()]);
399                 } else {
400                     IOException[] combined =
401                             new IOException[suppressedExceptions.size() +
402                                             dexElementsSuppressedExceptions.length];
403                     suppressedExceptions.toArray(combined);
404                     System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
405                             suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
406                     dexElementsSuppressedExceptions = combined;
407                 }
408 
409                 suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
410             }
411         }
412 
413         /**
414          * A wrapper around
415          * {@code private static final dalvik.system.DexPathList#makeDexElements}.
416          */
makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions)417         private static Object[] makeDexElements(
418                 Object dexPathList, ArrayList<File> files, File optimizedDirectory,
419                 ArrayList<IOException> suppressedExceptions)
420                         throws IllegalAccessException, InvocationTargetException,
421                         NoSuchMethodException {
422             Method makeDexElements =
423                     findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
424                             ArrayList.class);
425 
426             return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
427                     suppressedExceptions);
428         }
429     }
430 
431     /**
432      * Installer for platform versions 14, 15, 16, 17 and 18.
433      */
434     private static final class V14 {
435 
install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory)436         private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
437                 File optimizedDirectory)
438                         throws IllegalArgumentException, IllegalAccessException,
439                         NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
440             /* The patched class loader is expected to be a descendant of
441              * dalvik.system.BaseDexClassLoader. We modify its
442              * dalvik.system.DexPathList pathList field to append additional DEX
443              * file entries.
444              */
445             Field pathListField = findField(loader, "pathList");
446             Object dexPathList = pathListField.get(loader);
447             expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
448                     new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
449         }
450 
451         /**
452          * A wrapper around
453          * {@code private static final dalvik.system.DexPathList#makeDexElements}.
454          */
makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory)455         private static Object[] makeDexElements(
456                 Object dexPathList, ArrayList<File> files, File optimizedDirectory)
457                         throws IllegalAccessException, InvocationTargetException,
458                         NoSuchMethodException {
459             Method makeDexElements =
460                     findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
461 
462             return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
463         }
464     }
465 
466     /**
467      * Installer for platform versions 4 to 13.
468      */
469     private static final class V4 {
install(ClassLoader loader, List<File> additionalClassPathEntries)470         private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
471                         throws IllegalArgumentException, IllegalAccessException,
472                         NoSuchFieldException, IOException {
473             /* The patched class loader is expected to be a descendant of
474              * dalvik.system.DexClassLoader. We modify its
475              * fields mPaths, mFiles, mZips and mDexs to append additional DEX
476              * file entries.
477              */
478             int extraSize = additionalClassPathEntries.size();
479 
480             Field pathField = findField(loader, "path");
481 
482             StringBuilder path = new StringBuilder((String) pathField.get(loader));
483             String[] extraPaths = new String[extraSize];
484             File[] extraFiles = new File[extraSize];
485             ZipFile[] extraZips = new ZipFile[extraSize];
486             DexFile[] extraDexs = new DexFile[extraSize];
487             for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
488                     iterator.hasNext();) {
489                 File additionalEntry = iterator.next();
490                 String entryPath = additionalEntry.getAbsolutePath();
491                 path.append(':').append(entryPath);
492                 int index = iterator.previousIndex();
493                 extraPaths[index] = entryPath;
494                 extraFiles[index] = additionalEntry;
495                 extraZips[index] = new ZipFile(additionalEntry);
496                 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
497             }
498 
499             pathField.set(loader, path.toString());
500             expandFieldArray(loader, "mPaths", extraPaths);
501             expandFieldArray(loader, "mFiles", extraFiles);
502             expandFieldArray(loader, "mZips", extraZips);
503             expandFieldArray(loader, "mDexs", extraDexs);
504         }
505     }
506 
507 }
508