• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.multidex;
18 
19 import android.app.Application;
20 import android.app.Instrumentation;
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.os.Build;
24 import android.util.Log;
25 
26 import dalvik.system.DexFile;
27 
28 import java.io.File;
29 import java.io.IOException;
30 import java.lang.reflect.Array;
31 import java.lang.reflect.Constructor;
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.StringTokenizer;
42 import java.util.zip.ZipFile;
43 
44 /**
45  * MultiDex patches {@link Context#getClassLoader() the application context class
46  * loader} in order to load classes from more than one dex file. The primary
47  * {@code classes.dex} must contain the classes necessary for calling this
48  * class methods. Secondary dex files named classes2.dex, classes3.dex... found
49  * in the application apk will be added to the classloader after first call to
50  * {@link #install(Context)}.
51  *
52  * <p/>
53  * This library provides compatibility for platforms with API level 4 through 20. This library does
54  * nothing on newer versions of the platform which provide built-in support for secondary dex files.
55  */
56 public final class MultiDex {
57 
58     static final String TAG = "MultiDex";
59 
60     private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
61 
62     private static final String CODE_CACHE_NAME = "code_cache";
63 
64     private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "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 String NO_KEY_PREFIX = "";
75 
76     private static final Set<File> installedApk = new HashSet<File>();
77 
78     private static final boolean IS_VM_MULTIDEX_CAPABLE =
79             isVMMultidexCapable(System.getProperty("java.vm.version"));
80 
MultiDex()81     private MultiDex() {}
82 
83     /**
84      * Patches the application context class loader by appending extra dex files
85      * loaded from the application apk. This method should be called in the
86      * attachBaseContext of your {@link Application}, see
87      * {@link MultiDexApplication} for more explanation and an example.
88      *
89      * @param context application context.
90      * @throws RuntimeException if an error occurred preventing the classloader
91      *         extension.
92      */
install(Context context)93     public static void install(Context context) {
94         Log.i(TAG, "Installing application");
95         if (IS_VM_MULTIDEX_CAPABLE) {
96             Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
97             return;
98         }
99 
100         if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
101             throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
102                     + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
103         }
104 
105         try {
106             ApplicationInfo applicationInfo = getApplicationInfo(context);
107             if (applicationInfo == null) {
108               Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
109                   + " MultiDex support library is disabled.");
110               return;
111             }
112 
113             doInstallation(context,
114                     new File(applicationInfo.sourceDir),
115                     new File(applicationInfo.dataDir),
116                     CODE_CACHE_SECONDARY_FOLDER_NAME,
117                     NO_KEY_PREFIX,
118                     true);
119 
120         } catch (Exception e) {
121             Log.e(TAG, "MultiDex installation failure", e);
122             throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
123         }
124         Log.i(TAG, "install done");
125     }
126 
127     /**
128      * Patches the instrumentation context class loader by appending extra dex files
129      * loaded from the instrumentation apk and the application apk. This method should be called in
130      * the onCreate of your {@link Instrumentation}, see
131      * {@link com.android.test.runner.MultiDexTestRunner} for an example.
132      *
133      * @param instrumentationContext instrumentation context.
134      * @param targetContext target application context.
135      * @throws RuntimeException if an error occurred preventing the classloader
136      *         extension.
137      */
installInstrumentation(Context instrumentationContext, Context targetContext)138     public static void installInstrumentation(Context instrumentationContext,
139             Context targetContext) {
140         Log.i(TAG, "Installing instrumentation");
141 
142         if (IS_VM_MULTIDEX_CAPABLE) {
143             Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
144             return;
145         }
146 
147         if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
148             throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
149                     + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
150         }
151         try {
152 
153             ApplicationInfo instrumentationInfo = getApplicationInfo(instrumentationContext);
154             if (instrumentationInfo == null) {
155                 Log.i(TAG, "No ApplicationInfo available for instrumentation, i.e. running on a"
156                     + " test Context: MultiDex support library is disabled.");
157                 return;
158             }
159 
160             ApplicationInfo applicationInfo = getApplicationInfo(targetContext);
161             if (applicationInfo == null) {
162                 Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
163                     + " MultiDex support library is disabled.");
164                 return;
165             }
166 
167             String instrumentationPrefix = instrumentationContext.getPackageName() + ".";
168 
169             File dataDir = new File(applicationInfo.dataDir);
170 
171             doInstallation(targetContext,
172                     new File(instrumentationInfo.sourceDir),
173                     dataDir,
174                     instrumentationPrefix + CODE_CACHE_SECONDARY_FOLDER_NAME,
175                     instrumentationPrefix,
176                     false);
177 
178             doInstallation(targetContext,
179                     new File(applicationInfo.sourceDir),
180                     dataDir,
181                     CODE_CACHE_SECONDARY_FOLDER_NAME,
182                     NO_KEY_PREFIX,
183                     false);
184         } catch (Exception e) {
185             Log.e(TAG, "MultiDex installation failure", e);
186             throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
187         }
188         Log.i(TAG, "Installation done");
189     }
190 
191     /**
192      * @param mainContext context used to get filesDir, to save preference and to get the
193      * classloader to patch.
194      * @param sourceApk Apk file.
195      * @param dataDir data directory to use for code cache simulation.
196      * @param secondaryFolderName name of the folder for storing extractions.
197      * @param prefsKeyPrefix prefix of all stored preference keys.
198      * @param reinstallOnPatchRecoverableException if set to true, will attempt a clean extraction
199      * if a possibly recoverable exception occurs during classloader patching.
200      */
doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException)201     private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
202             String secondaryFolderName, String prefsKeyPrefix,
203             boolean reinstallOnPatchRecoverableException) throws IOException,
204                 IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
205                 InvocationTargetException, NoSuchMethodException, SecurityException,
206                 ClassNotFoundException, InstantiationException {
207         synchronized (installedApk) {
208             if (installedApk.contains(sourceApk)) {
209                 return;
210             }
211             installedApk.add(sourceApk);
212 
213             if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
214                 Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
215                         + Build.VERSION.SDK_INT + ": SDK version higher than "
216                         + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
217                         + "runtime with built-in multidex capabilty but it's not the "
218                         + "case here: java.vm.version=\""
219                         + System.getProperty("java.vm.version") + "\"");
220             }
221 
222             /* The patched class loader is expected to be a ClassLoader capable of loading DEX
223              * bytecode. We modify its pathList field to append additional DEX file entries.
224              */
225             ClassLoader loader = getDexClassloader(mainContext);
226             if (loader == null) {
227                 return;
228             }
229 
230             try {
231               clearOldDexDir(mainContext);
232             } catch (Throwable t) {
233               Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
234                   + "continuing without cleaning.", t);
235             }
236 
237             File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
238             // MultiDexExtractor is taking the file lock and keeping it until it is closed.
239             // Keep it open during installSecondaryDexes and through forced extraction to ensure no
240             // extraction or optimizing dexopt is running in parallel.
241             MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
242             IOException closeException = null;
243             try {
244                 List<? extends File> files =
245                         extractor.load(mainContext, prefsKeyPrefix, false);
246                 try {
247                     installSecondaryDexes(loader, dexDir, files);
248                 // Some IOException causes may be fixed by a clean extraction.
249                 } catch (IOException e) {
250                     if (!reinstallOnPatchRecoverableException) {
251                         throw e;
252                     }
253                     Log.w(TAG, "Failed to install extracted secondary dex files, retrying with "
254                             + "forced extraction", e);
255                     files = extractor.load(mainContext, prefsKeyPrefix, true);
256                     installSecondaryDexes(loader, dexDir, files);
257                 }
258             } finally {
259                 try {
260                     extractor.close();
261                 } catch (IOException e) {
262                     // Delay throw of close exception to ensure we don't override some exception
263                     // thrown during the try block.
264                     closeException = e;
265                 }
266             }
267             if (closeException != null) {
268                 throw closeException;
269             }
270         }
271     }
272 
273     /**
274      * Returns a {@link Classloader} from the {@link Context} that is capable of reading dex
275      * bytecode or null if the Classloader is not dex-capable e.g: when running on a JVM testing
276      * environment such as Robolectric.
277      */
getDexClassloader(Context context)278     private static ClassLoader getDexClassloader(Context context) {
279         ClassLoader loader;
280         try {
281             loader = context.getClassLoader();
282         } catch (RuntimeException e) {
283             /* Ignore those exceptions so that we don't break tests relying on Context like
284              * a android.test.mock.MockContext or a android.content.ContextWrapper with a
285              * null base Context.
286              */
287             Log.w(TAG, "Failure while trying to obtain Context class loader. "
288                     + "Must be running in test mode. Skip patching.", e);
289             return null;
290         }
291 
292         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
293             if (loader instanceof dalvik.system.BaseDexClassLoader) {
294                 return loader;
295             }
296         } else if (loader instanceof dalvik.system.DexClassLoader
297                     || loader instanceof dalvik.system.PathClassLoader) {
298             return loader;
299         }
300         Log.e(TAG, "Context class loader is null or not dex-capable. "
301                 + "Must be running in test mode. Skip patching.");
302         return null;
303     }
304 
getApplicationInfo(Context context)305     private static ApplicationInfo getApplicationInfo(Context context) {
306         try {
307             /* Due to package install races it is possible for a process to be started from an old
308              * apk even though that apk has been replaced. Querying for ApplicationInfo by package
309              * name may return information for the new apk, leading to a runtime with the old main
310              * dex file and new secondary dex files. This leads to various problems like
311              * ClassNotFoundExceptions. Using context.getApplicationInfo() should result in the
312              * process having a consistent view of the world (even if it is of the old world). The
313              * package install races are eventually resolved and old processes are killed.
314              */
315             return context.getApplicationInfo();
316         } catch (RuntimeException e) {
317             /* Ignore those exceptions so that we don't break tests relying on Context like
318              * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
319              * base Context.
320              */
321             Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
322                     "Must be running in test mode. Skip patching.", e);
323             return null;
324         }
325     }
326 
327     /**
328      * Identifies if the current VM has a native support for multidex, meaning there is no need for
329      * additional installation by this library.
330      * @return true if the VM handles multidex
331      */
332     /* package visible for test */
isVMMultidexCapable(String versionString)333     static boolean isVMMultidexCapable(String versionString) {
334         boolean isMultidexCapable = false;
335         if (versionString != null) {
336             StringTokenizer tokenizer = new StringTokenizer(versionString, ".");
337             String majorToken = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
338             String minorToken = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
339             if (majorToken != null && minorToken != null) {
340                 try {
341                     int major = Integer.parseInt(majorToken);
342                     int minor = Integer.parseInt(minorToken);
343                     isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
344                             || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
345                                     && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
346                 } catch (NumberFormatException e) {
347                     // let isMultidexCapable be false
348                 }
349             }
350         }
351         Log.i(TAG, "VM with version " + versionString +
352                 (isMultidexCapable ?
353                         " has multidex support" :
354                         " does not have multidex support"));
355         return isMultidexCapable;
356     }
357 
installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files)358     private static void installSecondaryDexes(ClassLoader loader, File dexDir,
359         List<? extends File> files)
360             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
361             InvocationTargetException, NoSuchMethodException, IOException, SecurityException,
362             ClassNotFoundException, InstantiationException {
363         if (!files.isEmpty()) {
364             if (Build.VERSION.SDK_INT >= 19) {
365                 V19.install(loader, files, dexDir);
366             } else if (Build.VERSION.SDK_INT >= 14) {
367                 V14.install(loader, files);
368             } else {
369                 V4.install(loader, files);
370             }
371         }
372     }
373 
374     /**
375      * Locates a given field anywhere in the class inheritance hierarchy.
376      *
377      * @param instance an object to search the field into.
378      * @param name field name
379      * @return a field object
380      * @throws NoSuchFieldException if the field cannot be located
381      */
findField(Object instance, String name)382     private static Field findField(Object instance, String name) throws NoSuchFieldException {
383         for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
384             try {
385                 Field field = clazz.getDeclaredField(name);
386 
387 
388                 if (!field.isAccessible()) {
389                     field.setAccessible(true);
390                 }
391 
392                 return field;
393             } catch (NoSuchFieldException e) {
394                 // ignore and search next
395             }
396         }
397 
398         throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
399     }
400 
401     /**
402      * Locates a given method anywhere in the class inheritance hierarchy.
403      *
404      * @param instance an object to search the method into.
405      * @param name method name
406      * @param parameterTypes method parameter types
407      * @return a method object
408      * @throws NoSuchMethodException if the method cannot be located
409      */
findMethod(Object instance, String name, Class<?>... parameterTypes)410     private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
411             throws NoSuchMethodException {
412         for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
413             try {
414                 Method method = clazz.getDeclaredMethod(name, parameterTypes);
415 
416 
417                 if (!method.isAccessible()) {
418                     method.setAccessible(true);
419                 }
420 
421                 return method;
422             } catch (NoSuchMethodException e) {
423                 // ignore and search next
424             }
425         }
426 
427         throw new NoSuchMethodException("Method " + name + " with parameters " +
428                 Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
429     }
430 
431     /**
432      * Replace the value of a field containing a non null array, by a new array containing the
433      * elements of the original array plus the elements of extraElements.
434      * @param instance the instance whose field is to be modified.
435      * @param fieldName the field to modify.
436      * @param extraElements elements to append at the end of the array.
437      */
expandFieldArray(Object instance, String fieldName, Object[] extraElements)438     private static void expandFieldArray(Object instance, String fieldName,
439             Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
440             IllegalAccessException {
441         Field jlrField = findField(instance, fieldName);
442         Object[] original = (Object[]) jlrField.get(instance);
443         Object[] combined = (Object[]) Array.newInstance(
444                 original.getClass().getComponentType(), original.length + extraElements.length);
445         System.arraycopy(original, 0, combined, 0, original.length);
446         System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
447         jlrField.set(instance, combined);
448     }
449 
clearOldDexDir(Context context)450     private static void clearOldDexDir(Context context) throws Exception {
451         File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
452         if (dexDir.isDirectory()) {
453             Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
454             File[] files = dexDir.listFiles();
455             if (files == null) {
456                 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
457                 return;
458             }
459             for (File oldFile : files) {
460                 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size "
461                         + oldFile.length());
462                 if (!oldFile.delete()) {
463                     Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
464                 } else {
465                     Log.i(TAG, "Deleted old file " + oldFile.getPath());
466                 }
467             }
468             if (!dexDir.delete()) {
469                 Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath());
470             } else {
471                 Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath());
472             }
473         }
474     }
475 
getDexDir(Context context, File dataDir, String secondaryFolderName)476     private static File getDexDir(Context context, File dataDir, String secondaryFolderName)
477             throws IOException {
478         File cache = new File(dataDir, CODE_CACHE_NAME);
479         try {
480             mkdirChecked(cache);
481         } catch (IOException e) {
482             /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless
483              * files on disk if the device ever updates to android 5+. But since this seems to
484              * happen only on some devices running android 2, this should cause no pollution.
485              */
486             cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
487             mkdirChecked(cache);
488         }
489         File dexDir = new File(cache, secondaryFolderName);
490         mkdirChecked(dexDir);
491         return dexDir;
492     }
493 
mkdirChecked(File dir)494     private static void mkdirChecked(File dir) throws IOException {
495         dir.mkdir();
496         if (!dir.isDirectory()) {
497             File parent = dir.getParentFile();
498             if (parent == null) {
499                 Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null.");
500             } else {
501                 Log.e(TAG, "Failed to create dir " + dir.getPath() +
502                         ". parent file is a dir " + parent.isDirectory() +
503                         ", a file " + parent.isFile() +
504                         ", exists " + parent.exists() +
505                         ", readable " + parent.canRead() +
506                         ", writable " + parent.canWrite());
507             }
508             throw new IOException("Failed to create directory " + dir.getPath());
509         }
510     }
511 
512     /**
513      * Installer for platform versions 19.
514      */
515     private static final class V19 {
516 
install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory)517         static void install(ClassLoader loader,
518                 List<? extends File> additionalClassPathEntries,
519                 File optimizedDirectory)
520                         throws IllegalArgumentException, IllegalAccessException,
521                         NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
522                         IOException {
523             /* The patched class loader is expected to be a descendant of
524              * dalvik.system.BaseDexClassLoader. We modify its
525              * dalvik.system.DexPathList pathList field to append additional DEX
526              * file entries.
527              */
528             Field pathListField = findField(loader, "pathList");
529             Object dexPathList = pathListField.get(loader);
530             ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
531             expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
532                     new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
533                     suppressedExceptions));
534             if (suppressedExceptions.size() > 0) {
535                 for (IOException e : suppressedExceptions) {
536                     Log.w(TAG, "Exception in makeDexElement", e);
537                 }
538                 Field suppressedExceptionsField =
539                         findField(dexPathList, "dexElementsSuppressedExceptions");
540                 IOException[] dexElementsSuppressedExceptions =
541                         (IOException[]) suppressedExceptionsField.get(dexPathList);
542 
543                 if (dexElementsSuppressedExceptions == null) {
544                     dexElementsSuppressedExceptions =
545                             suppressedExceptions.toArray(
546                                     new IOException[suppressedExceptions.size()]);
547                 } else {
548                     IOException[] combined =
549                             new IOException[suppressedExceptions.size() +
550                                             dexElementsSuppressedExceptions.length];
551                     suppressedExceptions.toArray(combined);
552                     System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
553                             suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
554                     dexElementsSuppressedExceptions = combined;
555                 }
556 
557                 suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
558 
559                 IOException exception = new IOException("I/O exception during makeDexElement");
560                 exception.initCause(suppressedExceptions.get(0));
561                 throw exception;
562             }
563         }
564 
565         /**
566          * A wrapper around
567          * {@code private static final dalvik.system.DexPathList#makeDexElements}.
568          */
makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions)569         private static Object[] makeDexElements(
570                 Object dexPathList, ArrayList<File> files, File optimizedDirectory,
571                 ArrayList<IOException> suppressedExceptions)
572                         throws IllegalAccessException, InvocationTargetException,
573                         NoSuchMethodException {
574             Method makeDexElements =
575                     findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
576                             ArrayList.class);
577 
578             return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
579                     suppressedExceptions);
580         }
581     }
582 
583     /**
584      * Installer for platform versions 14, 15, 16, 17 and 18.
585      */
586     private static final class V14 {
587 
588         private interface ElementConstructor {
newInstance(File file, DexFile dex)589             Object newInstance(File file, DexFile dex)
590                     throws IllegalArgumentException, InstantiationException,
591                     IllegalAccessException, InvocationTargetException, IOException;
592         }
593 
594         /**
595          * Applies for ICS and early JB (initial release and MR1).
596          */
597         private static class ICSElementConstructor implements ElementConstructor {
598             private final Constructor<?> elementConstructor;
599 
ICSElementConstructor(Class<?> elementClass)600             ICSElementConstructor(Class<?> elementClass)
601                     throws SecurityException, NoSuchMethodException {
602                 elementConstructor =
603                         elementClass.getConstructor(File.class, ZipFile.class, DexFile.class);
604                 elementConstructor.setAccessible(true);
605             }
606 
607             @Override
newInstance(File file, DexFile dex)608             public Object newInstance(File file, DexFile dex)
609                     throws IllegalArgumentException, InstantiationException,
610                     IllegalAccessException, InvocationTargetException, IOException {
611                 return elementConstructor.newInstance(file, new ZipFile(file), dex);
612             }
613         }
614 
615         /**
616          * Applies for some intermediate JB (MR1.1).
617          *
618          * See Change-Id: I1a5b5d03572601707e1fb1fd4424c1ae2fd2217d
619          */
620         private static class JBMR11ElementConstructor implements ElementConstructor {
621             private final Constructor<?> elementConstructor;
622 
JBMR11ElementConstructor(Class<?> elementClass)623             JBMR11ElementConstructor(Class<?> elementClass)
624                     throws SecurityException, NoSuchMethodException {
625                 elementConstructor = elementClass
626                         .getConstructor(File.class, File.class, DexFile.class);
627                 elementConstructor.setAccessible(true);
628             }
629 
630             @Override
newInstance(File file, DexFile dex)631             public Object newInstance(File file, DexFile dex)
632                     throws IllegalArgumentException, InstantiationException,
633                     IllegalAccessException, InvocationTargetException {
634                 return elementConstructor.newInstance(file, file, dex);
635             }
636         }
637 
638         /**
639          * Applies for latest JB (MR2).
640          *
641          * See Change-Id: Iec4dca2244db9c9c793ac157e258fd61557a7a5d
642          */
643         private static class JBMR2ElementConstructor implements ElementConstructor {
644             private final Constructor<?> elementConstructor;
645 
JBMR2ElementConstructor(Class<?> elementClass)646             JBMR2ElementConstructor(Class<?> elementClass)
647                     throws SecurityException, NoSuchMethodException {
648                 elementConstructor = elementClass
649                         .getConstructor(File.class, Boolean.TYPE, File.class, DexFile.class);
650                 elementConstructor.setAccessible(true);
651             }
652 
653             @Override
newInstance(File file, DexFile dex)654             public Object newInstance(File file, DexFile dex)
655                     throws IllegalArgumentException, InstantiationException,
656                     IllegalAccessException, InvocationTargetException {
657                 return elementConstructor.newInstance(file, Boolean.FALSE, file, dex);
658             }
659         }
660 
661         private static final int EXTRACTED_SUFFIX_LENGTH =
662                 MultiDexExtractor.EXTRACTED_SUFFIX.length();
663 
664         private final ElementConstructor elementConstructor;
665 
install(ClassLoader loader, List<? extends File> additionalClassPathEntries)666         static void install(ClassLoader loader,
667                 List<? extends File> additionalClassPathEntries)
668                         throws  IOException, SecurityException, IllegalArgumentException,
669                         ClassNotFoundException, NoSuchMethodException, InstantiationException,
670                         IllegalAccessException, InvocationTargetException, NoSuchFieldException {
671             /* The patched class loader is expected to be a descendant of
672              * dalvik.system.BaseDexClassLoader. We modify its
673              * dalvik.system.DexPathList pathList field to append additional DEX
674              * file entries.
675              */
676             Field pathListField = findField(loader, "pathList");
677             Object dexPathList = pathListField.get(loader);
678             Object[] elements = new V14().makeDexElements(additionalClassPathEntries);
679             try {
680                 expandFieldArray(dexPathList, "dexElements", elements);
681             } catch (NoSuchFieldException e) {
682                 // dexElements was renamed pathElements for a short period during JB development,
683                 // eventually it was renamed back shortly after.
684                 Log.w(TAG, "Failed find field 'dexElements' attempting 'pathElements'", e);
685                 expandFieldArray(dexPathList, "pathElements", elements);
686             }
687         }
688 
V14()689         private  V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException {
690             ElementConstructor constructor;
691             Class<?> elementClass = Class.forName("dalvik.system.DexPathList$Element");
692             try {
693                 constructor = new ICSElementConstructor(elementClass);
694             } catch (NoSuchMethodException e1) {
695                 try {
696                     constructor = new JBMR11ElementConstructor(elementClass);
697                 } catch (NoSuchMethodException e2) {
698                     constructor = new JBMR2ElementConstructor(elementClass);
699                 }
700             }
701             this.elementConstructor = constructor;
702         }
703 
704         /**
705          * An emulation of {@code private static final dalvik.system.DexPathList#makeDexElements}
706          * accepting only extracted secondary dex files.
707          * OS version is catching IOException and just logging some of them, this version is letting
708          * them through.
709          */
makeDexElements(List<? extends File> files)710         private Object[] makeDexElements(List<? extends File> files)
711                 throws IOException, SecurityException, IllegalArgumentException,
712                 InstantiationException, IllegalAccessException, InvocationTargetException {
713             Object[] elements = new Object[files.size()];
714             for (int i = 0; i < elements.length; i++) {
715                 File file = files.get(i);
716                 elements[i] = elementConstructor.newInstance(
717                         file,
718                         DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0));
719             }
720             return elements;
721         }
722 
723         /**
724          * Converts a zip file path of an extracted secondary dex to an output file path for an
725          * associated optimized dex file.
726          */
optimizedPathFor(File path)727         private static String optimizedPathFor(File path) {
728             // Any reproducible name ending with ".dex" should do but lets keep the same name
729             // as DexPathList.optimizedPathFor
730 
731             File optimizedDirectory = path.getParentFile();
732             String fileName = path.getName();
733             String optimizedFileName =
734                     fileName.substring(0, fileName.length() - EXTRACTED_SUFFIX_LENGTH)
735                     + MultiDexExtractor.DEX_SUFFIX;
736             File result = new File(optimizedDirectory, optimizedFileName);
737             return result.getPath();
738         }
739     }
740 
741     /**
742      * Installer for platform versions 4 to 13.
743      */
744     private static final class V4 {
install(ClassLoader loader, List<? extends File> additionalClassPathEntries)745         static void install(ClassLoader loader,
746                 List<? extends File> additionalClassPathEntries)
747                         throws IllegalArgumentException, IllegalAccessException,
748                         NoSuchFieldException, IOException {
749             /* The patched class loader is expected to be a descendant of
750              * dalvik.system.DexClassLoader. We modify its
751              * fields mPaths, mFiles, mZips and mDexs to append additional DEX
752              * file entries.
753              */
754             int extraSize = additionalClassPathEntries.size();
755 
756             Field pathField = findField(loader, "path");
757 
758             StringBuilder path = new StringBuilder((String) pathField.get(loader));
759             String[] extraPaths = new String[extraSize];
760             File[] extraFiles = new File[extraSize];
761             ZipFile[] extraZips = new ZipFile[extraSize];
762             DexFile[] extraDexs = new DexFile[extraSize];
763             for (ListIterator<? extends File> iterator = additionalClassPathEntries.listIterator();
764                     iterator.hasNext();) {
765                 File additionalEntry = iterator.next();
766                 String entryPath = additionalEntry.getAbsolutePath();
767                 path.append(':').append(entryPath);
768                 int index = iterator.previousIndex();
769                 extraPaths[index] = entryPath;
770                 extraFiles[index] = additionalEntry;
771                 extraZips[index] = new ZipFile(additionalEntry);
772                 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
773             }
774 
775             pathField.set(loader, path.toString());
776             expandFieldArray(loader, "mPaths", extraPaths);
777             expandFieldArray(loader, "mFiles", extraFiles);
778             expandFieldArray(loader, "mZips", extraZips);
779             expandFieldArray(loader, "mDexs", extraDexs);
780         }
781     }
782 
783 }
784