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