Home | History | Annotate | Download | only in multidex
      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 
     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      */
     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      */
    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      */
    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 
    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 */
    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 
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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 
    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          */
    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 {
    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 
    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
    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 
    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
    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 
    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
    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 
    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 
    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          */
    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          */
    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 {
    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