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