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