Migrate multidex to androidx

Bug: 74397601
Test: make
Change-Id: Iddea6f92cc7796125cf4d1ba86cf9b7425daef72
diff --git a/library/src/androidx/multidex/MultiDex.java b/library/src/androidx/multidex/MultiDex.java
new file mode 100644
index 0000000..69ff889
--- /dev/null
+++ b/library/src/androidx/multidex/MultiDex.java
@@ -0,0 +1,765 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.multidex;
+
+import android.app.Application;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.os.Build;
+import android.util.Log;
+import dalvik.system.DexFile;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipFile;
+
+/**
+ * MultiDex patches {@link Context#getClassLoader() the application context class
+ * loader} in order to load classes from more than one dex file. The primary
+ * {@code classes.dex} must contain the classes necessary for calling this
+ * class methods. Secondary dex files named classes2.dex, classes3.dex... found
+ * in the application apk will be added to the classloader after first call to
+ * {@link #install(Context)}.
+ *
+ * <p/>
+ * This library provides compatibility for platforms with API level 4 through 20. This library does
+ * nothing on newer versions of the platform which provide built-in support for secondary dex files.
+ */
+public final class MultiDex {
+
+    static final String TAG = "MultiDex";
+
+    private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
+
+    private static final String CODE_CACHE_NAME = "code_cache";
+
+    private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "secondary-dexes";
+
+    private static final int MAX_SUPPORTED_SDK_VERSION = 20;
+
+    private static final int MIN_SDK_VERSION = 4;
+
+    private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
+
+    private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
+
+    private static final String NO_KEY_PREFIX = "";
+
+    private static final Set<File> installedApk = new HashSet<File>();
+
+    private static final boolean IS_VM_MULTIDEX_CAPABLE =
+            isVMMultidexCapable(System.getProperty("java.vm.version"));
+
+    private MultiDex() {}
+
+    /**
+     * Patches the application context class loader by appending extra dex files
+     * loaded from the application apk. This method should be called in the
+     * attachBaseContext of your {@link Application}, see
+     * {@link MultiDexApplication} for more explanation and an example.
+     *
+     * @param context application context.
+     * @throws RuntimeException if an error occurred preventing the classloader
+     *         extension.
+     */
+    public static void install(Context context) {
+        Log.i(TAG, "Installing application");
+        if (IS_VM_MULTIDEX_CAPABLE) {
+            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
+            return;
+        }
+
+        if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
+            throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
+                    + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
+        }
+
+        try {
+            ApplicationInfo applicationInfo = getApplicationInfo(context);
+            if (applicationInfo == null) {
+              Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
+                  + " MultiDex support library is disabled.");
+              return;
+            }
+
+            doInstallation(context,
+                    new File(applicationInfo.sourceDir),
+                    new File(applicationInfo.dataDir),
+                    CODE_CACHE_SECONDARY_FOLDER_NAME,
+                    NO_KEY_PREFIX,
+                    true);
+
+        } catch (Exception e) {
+            Log.e(TAG, "MultiDex installation failure", e);
+            throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
+        }
+        Log.i(TAG, "install done");
+    }
+
+    /**
+     * Patches the instrumentation context class loader by appending extra dex files
+     * loaded from the instrumentation apk and the application apk. This method should be called in
+     * the onCreate of your {@link Instrumentation}, see
+     * {@link com.android.test.runner.MultiDexTestRunner} for an example.
+     *
+     * @param instrumentationContext instrumentation context.
+     * @param targetContext target application context.
+     * @throws RuntimeException if an error occurred preventing the classloader
+     *         extension.
+     */
+    public static void installInstrumentation(Context instrumentationContext,
+            Context targetContext) {
+        Log.i(TAG, "Installing instrumentation");
+
+        if (IS_VM_MULTIDEX_CAPABLE) {
+            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
+            return;
+        }
+
+        if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
+            throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
+                    + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
+        }
+        try {
+
+            ApplicationInfo instrumentationInfo = getApplicationInfo(instrumentationContext);
+            if (instrumentationInfo == null) {
+                Log.i(TAG, "No ApplicationInfo available for instrumentation, i.e. running on a"
+                    + " test Context: MultiDex support library is disabled.");
+                return;
+            }
+
+            ApplicationInfo applicationInfo = getApplicationInfo(targetContext);
+            if (applicationInfo == null) {
+                Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
+                    + " MultiDex support library is disabled.");
+                return;
+            }
+
+            String instrumentationPrefix = instrumentationContext.getPackageName() + ".";
+
+            File dataDir = new File(applicationInfo.dataDir);
+
+            doInstallation(targetContext,
+                    new File(instrumentationInfo.sourceDir),
+                    dataDir,
+                    instrumentationPrefix + CODE_CACHE_SECONDARY_FOLDER_NAME,
+                    instrumentationPrefix,
+                    false);
+
+            doInstallation(targetContext,
+                    new File(applicationInfo.sourceDir),
+                    dataDir,
+                    CODE_CACHE_SECONDARY_FOLDER_NAME,
+                    NO_KEY_PREFIX,
+                    false);
+        } catch (Exception e) {
+            Log.e(TAG, "MultiDex installation failure", e);
+            throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
+        }
+        Log.i(TAG, "Installation done");
+    }
+
+    /**
+     * @param mainContext context used to get filesDir, to save preference and to get the
+     * classloader to patch.
+     * @param sourceApk Apk file.
+     * @param dataDir data directory to use for code cache simulation.
+     * @param secondaryFolderName name of the folder for storing extractions.
+     * @param prefsKeyPrefix prefix of all stored preference keys.
+     * @param reinstallOnPatchRecoverableException if set to true, will attempt a clean extraction
+     * if a possibly recoverable exception occurs during classloader patching.
+     */
+    private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
+            String secondaryFolderName, String prefsKeyPrefix,
+            boolean reinstallOnPatchRecoverableException) throws IOException,
+                IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
+                InvocationTargetException, NoSuchMethodException, SecurityException,
+                ClassNotFoundException, InstantiationException {
+        synchronized (installedApk) {
+            if (installedApk.contains(sourceApk)) {
+                return;
+            }
+            installedApk.add(sourceApk);
+
+            if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
+                Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
+                        + Build.VERSION.SDK_INT + ": SDK version higher than "
+                        + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
+                        + "runtime with built-in multidex capabilty but it's not the "
+                        + "case here: java.vm.version=\""
+                        + System.getProperty("java.vm.version") + "\"");
+            }
+
+            /* The patched class loader is expected to be a descendant of
+             * dalvik.system.BaseDexClassLoader. We modify its
+             * dalvik.system.DexPathList pathList field to append additional DEX
+             * file entries.
+             */
+            ClassLoader loader;
+            try {
+                loader = mainContext.getClassLoader();
+            } catch (RuntimeException e) {
+                /* Ignore those exceptions so that we don't break tests relying on Context like
+                 * a android.test.mock.MockContext or a android.content.ContextWrapper with a
+                 * null base Context.
+                 */
+                Log.w(TAG, "Failure while trying to obtain Context class loader. " +
+                        "Must be running in test mode. Skip patching.", e);
+                return;
+            }
+            if (loader == null) {
+                // Note, the context class loader is null when running Robolectric tests.
+                Log.e(TAG,
+                        "Context class loader is null. Must be running in test mode. "
+                        + "Skip patching.");
+                return;
+            }
+
+            try {
+              clearOldDexDir(mainContext);
+            } catch (Throwable t) {
+              Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
+                  + "continuing without cleaning.", t);
+            }
+
+            File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
+            // MultiDexExtractor is taking the file lock and keeping it until it is closed.
+            // Keep it open during installSecondaryDexes and through forced extraction to ensure no
+            // extraction or optimizing dexopt is running in parallel.
+            MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
+            IOException closeException = null;
+            try {
+                List<? extends File> files =
+                        extractor.load(mainContext, prefsKeyPrefix, false);
+                try {
+                    installSecondaryDexes(loader, dexDir, files);
+                // Some IOException causes may be fixed by a clean extraction.
+                } catch (IOException e) {
+                    if (!reinstallOnPatchRecoverableException) {
+                        throw e;
+                    }
+                    Log.w(TAG, "Failed to install extracted secondary dex files, retrying with "
+                            + "forced extraction", e);
+                    files = extractor.load(mainContext, prefsKeyPrefix, true);
+                    installSecondaryDexes(loader, dexDir, files);
+                }
+            } finally {
+                try {
+                    extractor.close();
+                } catch (IOException e) {
+                    // Delay throw of close exception to ensure we don't override some exception
+                    // thrown during the try block.
+                    closeException = e;
+                }
+            }
+            if (closeException != null) {
+                throw closeException;
+            }
+        }
+    }
+
+    private static ApplicationInfo getApplicationInfo(Context context) {
+        try {
+            /* Due to package install races it is possible for a process to be started from an old
+             * apk even though that apk has been replaced. Querying for ApplicationInfo by package
+             * name may return information for the new apk, leading to a runtime with the old main
+             * dex file and new secondary dex files. This leads to various problems like
+             * ClassNotFoundExceptions. Using context.getApplicationInfo() should result in the
+             * process having a consistent view of the world (even if it is of the old world). The
+             * package install races are eventually resolved and old processes are killed.
+             */
+            return context.getApplicationInfo();
+        } catch (RuntimeException e) {
+            /* Ignore those exceptions so that we don't break tests relying on Context like
+             * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
+             * base Context.
+             */
+            Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
+                    "Must be running in test mode. Skip patching.", e);
+            return null;
+        }
+    }
+
+    /**
+     * Identifies if the current VM has a native support for multidex, meaning there is no need for
+     * additional installation by this library.
+     * @return true if the VM handles multidex
+     */
+    /* package visible for test */
+    static boolean isVMMultidexCapable(String versionString) {
+        boolean isMultidexCapable = false;
+        if (versionString != null) {
+            Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
+            if (matcher.matches()) {
+                try {
+                    int major = Integer.parseInt(matcher.group(1));
+                    int minor = Integer.parseInt(matcher.group(2));
+                    isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
+                            || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
+                                    && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
+                } catch (NumberFormatException e) {
+                    // let isMultidexCapable be false
+                }
+            }
+        }
+        Log.i(TAG, "VM with version " + versionString +
+                (isMultidexCapable ?
+                        " has multidex support" :
+                        " does not have multidex support"));
+        return isMultidexCapable;
+    }
+
+    private static void installSecondaryDexes(ClassLoader loader, File dexDir,
+        List<? extends File> files)
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
+            InvocationTargetException, NoSuchMethodException, IOException, SecurityException,
+            ClassNotFoundException, InstantiationException {
+        if (!files.isEmpty()) {
+            if (Build.VERSION.SDK_INT >= 19) {
+                V19.install(loader, files, dexDir);
+            } else if (Build.VERSION.SDK_INT >= 14) {
+                V14.install(loader, files);
+            } else {
+                V4.install(loader, files);
+            }
+        }
+    }
+
+    /**
+     * Locates a given field anywhere in the class inheritance hierarchy.
+     *
+     * @param instance an object to search the field into.
+     * @param name field name
+     * @return a field object
+     * @throws NoSuchFieldException if the field cannot be located
+     */
+    private static Field findField(Object instance, String name) throws NoSuchFieldException {
+        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+            try {
+                Field field = clazz.getDeclaredField(name);
+
+
+                if (!field.isAccessible()) {
+                    field.setAccessible(true);
+                }
+
+                return field;
+            } catch (NoSuchFieldException e) {
+                // ignore and search next
+            }
+        }
+
+        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
+    }
+
+    /**
+     * Locates a given method anywhere in the class inheritance hierarchy.
+     *
+     * @param instance an object to search the method into.
+     * @param name method name
+     * @param parameterTypes method parameter types
+     * @return a method object
+     * @throws NoSuchMethodException if the method cannot be located
+     */
+    private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
+            throws NoSuchMethodException {
+        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+            try {
+                Method method = clazz.getDeclaredMethod(name, parameterTypes);
+
+
+                if (!method.isAccessible()) {
+                    method.setAccessible(true);
+                }
+
+                return method;
+            } catch (NoSuchMethodException e) {
+                // ignore and search next
+            }
+        }
+
+        throw new NoSuchMethodException("Method " + name + " with parameters " +
+                Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
+    }
+
+    /**
+     * Replace the value of a field containing a non null array, by a new array containing the
+     * elements of the original array plus the elements of extraElements.
+     * @param instance the instance whose field is to be modified.
+     * @param fieldName the field to modify.
+     * @param extraElements elements to append at the end of the array.
+     */
+    private static void expandFieldArray(Object instance, String fieldName,
+            Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
+            IllegalAccessException {
+        Field jlrField = findField(instance, fieldName);
+        Object[] original = (Object[]) jlrField.get(instance);
+        Object[] combined = (Object[]) Array.newInstance(
+                original.getClass().getComponentType(), original.length + extraElements.length);
+        System.arraycopy(original, 0, combined, 0, original.length);
+        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
+        jlrField.set(instance, combined);
+    }
+
+    private static void clearOldDexDir(Context context) throws Exception {
+        File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
+        if (dexDir.isDirectory()) {
+            Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
+            File[] files = dexDir.listFiles();
+            if (files == null) {
+                Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
+                return;
+            }
+            for (File oldFile : files) {
+                Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size "
+                        + oldFile.length());
+                if (!oldFile.delete()) {
+                    Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
+                } else {
+                    Log.i(TAG, "Deleted old file " + oldFile.getPath());
+                }
+            }
+            if (!dexDir.delete()) {
+                Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath());
+            } else {
+                Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath());
+            }
+        }
+    }
+
+    private static File getDexDir(Context context, File dataDir, String secondaryFolderName)
+            throws IOException {
+        File cache = new File(dataDir, CODE_CACHE_NAME);
+        try {
+            mkdirChecked(cache);
+        } catch (IOException e) {
+            /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless
+             * files on disk if the device ever updates to android 5+. But since this seems to
+             * happen only on some devices running android 2, this should cause no pollution.
+             */
+            cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
+            mkdirChecked(cache);
+        }
+        File dexDir = new File(cache, secondaryFolderName);
+        mkdirChecked(dexDir);
+        return dexDir;
+    }
+
+    private static void mkdirChecked(File dir) throws IOException {
+        dir.mkdir();
+        if (!dir.isDirectory()) {
+            File parent = dir.getParentFile();
+            if (parent == null) {
+                Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null.");
+            } else {
+                Log.e(TAG, "Failed to create dir " + dir.getPath() +
+                        ". parent file is a dir " + parent.isDirectory() +
+                        ", a file " + parent.isFile() +
+                        ", exists " + parent.exists() +
+                        ", readable " + parent.canRead() +
+                        ", writable " + parent.canWrite());
+            }
+            throw new IOException("Failed to create directory " + dir.getPath());
+        }
+    }
+
+    /**
+     * Installer for platform versions 19.
+     */
+    private static final class V19 {
+
+        static void install(ClassLoader loader,
+                List<? extends File> additionalClassPathEntries,
+                File optimizedDirectory)
+                        throws IllegalArgumentException, IllegalAccessException,
+                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
+                        IOException {
+            /* The patched class loader is expected to be a descendant of
+             * dalvik.system.BaseDexClassLoader. We modify its
+             * dalvik.system.DexPathList pathList field to append additional DEX
+             * file entries.
+             */
+            Field pathListField = findField(loader, "pathList");
+            Object dexPathList = pathListField.get(loader);
+            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
+            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
+                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
+                    suppressedExceptions));
+            if (suppressedExceptions.size() > 0) {
+                for (IOException e : suppressedExceptions) {
+                    Log.w(TAG, "Exception in makeDexElement", e);
+                }
+                Field suppressedExceptionsField =
+                        findField(dexPathList, "dexElementsSuppressedExceptions");
+                IOException[] dexElementsSuppressedExceptions =
+                        (IOException[]) suppressedExceptionsField.get(dexPathList);
+
+                if (dexElementsSuppressedExceptions == null) {
+                    dexElementsSuppressedExceptions =
+                            suppressedExceptions.toArray(
+                                    new IOException[suppressedExceptions.size()]);
+                } else {
+                    IOException[] combined =
+                            new IOException[suppressedExceptions.size() +
+                                            dexElementsSuppressedExceptions.length];
+                    suppressedExceptions.toArray(combined);
+                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
+                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
+                    dexElementsSuppressedExceptions = combined;
+                }
+
+                suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
+
+                IOException exception = new IOException("I/O exception during makeDexElement");
+                exception.initCause(suppressedExceptions.get(0));
+                throw exception;
+            }
+        }
+
+        /**
+         * A wrapper around
+         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
+         */
+        private static Object[] makeDexElements(
+                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
+                ArrayList<IOException> suppressedExceptions)
+                        throws IllegalAccessException, InvocationTargetException,
+                        NoSuchMethodException {
+            Method makeDexElements =
+                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
+                            ArrayList.class);
+
+            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
+                    suppressedExceptions);
+        }
+    }
+
+    /**
+     * Installer for platform versions 14, 15, 16, 17 and 18.
+     */
+    private static final class V14 {
+
+        private interface ElementConstructor {
+            Object newInstance(File file, DexFile dex)
+                    throws IllegalArgumentException, InstantiationException,
+                    IllegalAccessException, InvocationTargetException, IOException;
+        }
+
+        /**
+         * Applies for ICS and early JB (initial release and MR1).
+         */
+        private static class ICSElementConstructor implements ElementConstructor {
+            private final Constructor<?> elementConstructor;
+
+            ICSElementConstructor(Class<?> elementClass)
+                    throws SecurityException, NoSuchMethodException {
+                elementConstructor =
+                        elementClass.getConstructor(File.class, ZipFile.class, DexFile.class);
+                elementConstructor.setAccessible(true);
+            }
+
+            @Override
+            public Object newInstance(File file, DexFile dex)
+                    throws IllegalArgumentException, InstantiationException,
+                    IllegalAccessException, InvocationTargetException, IOException {
+                return elementConstructor.newInstance(file, new ZipFile(file), dex);
+            }
+        }
+
+        /**
+         * Applies for some intermediate JB (MR1.1).
+         *
+         * See Change-Id: I1a5b5d03572601707e1fb1fd4424c1ae2fd2217d
+         */
+        private static class JBMR11ElementConstructor implements ElementConstructor {
+            private final Constructor<?> elementConstructor;
+
+            JBMR11ElementConstructor(Class<?> elementClass)
+                    throws SecurityException, NoSuchMethodException {
+                elementConstructor = elementClass
+                        .getConstructor(File.class, File.class, DexFile.class);
+                elementConstructor.setAccessible(true);
+            }
+
+            @Override
+            public Object newInstance(File file, DexFile dex)
+                    throws IllegalArgumentException, InstantiationException,
+                    IllegalAccessException, InvocationTargetException {
+                return elementConstructor.newInstance(file, file, dex);
+            }
+        }
+
+        /**
+         * Applies for latest JB (MR2).
+         *
+         * See Change-Id: Iec4dca2244db9c9c793ac157e258fd61557a7a5d
+         */
+        private static class JBMR2ElementConstructor implements ElementConstructor {
+            private final Constructor<?> elementConstructor;
+
+            JBMR2ElementConstructor(Class<?> elementClass)
+                    throws SecurityException, NoSuchMethodException {
+                elementConstructor = elementClass
+                        .getConstructor(File.class, Boolean.TYPE, File.class, DexFile.class);
+                elementConstructor.setAccessible(true);
+            }
+
+            @Override
+            public Object newInstance(File file, DexFile dex)
+                    throws IllegalArgumentException, InstantiationException,
+                    IllegalAccessException, InvocationTargetException {
+                return elementConstructor.newInstance(file, Boolean.FALSE, file, dex);
+            }
+        }
+
+        private static final int EXTRACTED_SUFFIX_LENGTH =
+                MultiDexExtractor.EXTRACTED_SUFFIX.length();
+
+        private final ElementConstructor elementConstructor;
+
+        static void install(ClassLoader loader,
+                List<? extends File> additionalClassPathEntries)
+                        throws  IOException, SecurityException, IllegalArgumentException,
+                        ClassNotFoundException, NoSuchMethodException, InstantiationException,
+                        IllegalAccessException, InvocationTargetException, NoSuchFieldException {
+            /* The patched class loader is expected to be a descendant of
+             * dalvik.system.BaseDexClassLoader. We modify its
+             * dalvik.system.DexPathList pathList field to append additional DEX
+             * file entries.
+             */
+            Field pathListField = findField(loader, "pathList");
+            Object dexPathList = pathListField.get(loader);
+            Object[] elements = new V14().makeDexElements(additionalClassPathEntries);
+            try {
+                expandFieldArray(dexPathList, "dexElements", elements);
+            } catch (NoSuchFieldException e) {
+                // dexElements was renamed pathElements for a short period during JB development,
+                // eventually it was renamed back shortly after.
+                Log.w(TAG, "Failed find field 'dexElements' attempting 'pathElements'", e);
+                expandFieldArray(dexPathList, "pathElements", elements);
+            }
+        }
+
+        private  V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException {
+            ElementConstructor constructor;
+            Class<?> elementClass = Class.forName("dalvik.system.DexPathList$Element");
+            try {
+                constructor = new ICSElementConstructor(elementClass);
+            } catch (NoSuchMethodException e1) {
+                try {
+                    constructor = new JBMR11ElementConstructor(elementClass);
+                } catch (NoSuchMethodException e2) {
+                    constructor = new JBMR2ElementConstructor(elementClass);
+                }
+            }
+            this.elementConstructor = constructor;
+        }
+
+        /**
+         * An emulation of {@code private static final dalvik.system.DexPathList#makeDexElements}
+         * accepting only extracted secondary dex files.
+         * OS version is catching IOException and just logging some of them, this version is letting
+         * them through.
+         */
+        private Object[] makeDexElements(List<? extends File> files)
+                throws IOException, SecurityException, IllegalArgumentException,
+                InstantiationException, IllegalAccessException, InvocationTargetException {
+            Object[] elements = new Object[files.size()];
+            for (int i = 0; i < elements.length; i++) {
+                File file = files.get(i);
+                elements[i] = elementConstructor.newInstance(
+                        file,
+                        DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0));
+            }
+            return elements;
+        }
+
+        /**
+         * Converts a zip file path of an extracted secondary dex to an output file path for an
+         * associated optimized dex file.
+         */
+        private static String optimizedPathFor(File path) {
+            // Any reproducible name ending with ".dex" should do but lets keep the same name
+            // as DexPathList.optimizedPathFor
+
+            File optimizedDirectory = path.getParentFile();
+            String fileName = path.getName();
+            String optimizedFileName =
+                    fileName.substring(0, fileName.length() - EXTRACTED_SUFFIX_LENGTH)
+                    + MultiDexExtractor.DEX_SUFFIX;
+            File result = new File(optimizedDirectory, optimizedFileName);
+            return result.getPath();
+        }
+    }
+
+    /**
+     * Installer for platform versions 4 to 13.
+     */
+    private static final class V4 {
+        static void install(ClassLoader loader,
+                List<? extends File> additionalClassPathEntries)
+                        throws IllegalArgumentException, IllegalAccessException,
+                        NoSuchFieldException, IOException {
+            /* The patched class loader is expected to be a descendant of
+             * dalvik.system.DexClassLoader. We modify its
+             * fields mPaths, mFiles, mZips and mDexs to append additional DEX
+             * file entries.
+             */
+            int extraSize = additionalClassPathEntries.size();
+
+            Field pathField = findField(loader, "path");
+
+            StringBuilder path = new StringBuilder((String) pathField.get(loader));
+            String[] extraPaths = new String[extraSize];
+            File[] extraFiles = new File[extraSize];
+            ZipFile[] extraZips = new ZipFile[extraSize];
+            DexFile[] extraDexs = new DexFile[extraSize];
+            for (ListIterator<? extends File> iterator = additionalClassPathEntries.listIterator();
+                    iterator.hasNext();) {
+                File additionalEntry = iterator.next();
+                String entryPath = additionalEntry.getAbsolutePath();
+                path.append(':').append(entryPath);
+                int index = iterator.previousIndex();
+                extraPaths[index] = entryPath;
+                extraFiles[index] = additionalEntry;
+                extraZips[index] = new ZipFile(additionalEntry);
+                extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
+            }
+
+            pathField.set(loader, path.toString());
+            expandFieldArray(loader, "mPaths", extraPaths);
+            expandFieldArray(loader, "mFiles", extraFiles);
+            expandFieldArray(loader, "mZips", extraZips);
+            expandFieldArray(loader, "mDexs", extraDexs);
+        }
+    }
+
+}
diff --git a/library/src/androidx/multidex/MultiDexApplication.java b/library/src/androidx/multidex/MultiDexApplication.java
new file mode 100644
index 0000000..4c759e3
--- /dev/null
+++ b/library/src/androidx/multidex/MultiDexApplication.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.multidex;
+
+import android.app.Application;
+import android.content.Context;
+
+/**
+ * Minimal MultiDex capable application. To use the legacy multidex library there is 3 possibility:
+ * <ul>
+ * <li>Declare this class as the application in your AndroidManifest.xml.</li>
+ * <li>Have your {@link Application} extends this class.</li>
+ * <li>Have your {@link Application} override attachBaseContext starting with<br>
+ * <code>
+  protected void attachBaseContext(Context base) {<br>
+    super.attachBaseContext(base);<br>
+    MultiDex.install(this);
+    </code></li>
+ *   <ul>
+ */
+public class MultiDexApplication extends Application {
+  @Override
+  protected void attachBaseContext(Context base) {
+    super.attachBaseContext(base);
+    MultiDex.install(this);
+  }
+}
diff --git a/library/src/androidx/multidex/MultiDexExtractor.java b/library/src/androidx/multidex/MultiDexExtractor.java
new file mode 100644
index 0000000..2b96113
--- /dev/null
+++ b/library/src/androidx/multidex/MultiDexExtractor.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.multidex;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.util.Log;
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Exposes application secondary dex files as files in the application data
+ * directory.
+ * {@link MultiDexExtractor} is taking the file lock in the dex dir on creation and release it
+ * during close.
+ */
+final class MultiDexExtractor implements Closeable {
+
+    /**
+     * Zip file containing one secondary dex file.
+     */
+    private static class ExtractedDex extends File {
+        public long crc = NO_VALUE;
+
+        public ExtractedDex(File dexDir, String fileName) {
+            super(dexDir, fileName);
+        }
+    }
+
+    private static final String TAG = MultiDex.TAG;
+
+    /**
+     * We look for additional dex files named {@code classes2.dex},
+     * {@code classes3.dex}, etc.
+     */
+    private static final String DEX_PREFIX = "classes";
+    static final String DEX_SUFFIX = ".dex";
+
+    private static final String EXTRACTED_NAME_EXT = ".classes";
+    static final String EXTRACTED_SUFFIX = ".zip";
+    private static final int MAX_EXTRACT_ATTEMPTS = 3;
+
+    private static final String PREFS_FILE = "multidex.version";
+    private static final String KEY_TIME_STAMP = "timestamp";
+    private static final String KEY_CRC = "crc";
+    private static final String KEY_DEX_NUMBER = "dex.number";
+    private static final String KEY_DEX_CRC = "dex.crc.";
+    private static final String KEY_DEX_TIME = "dex.time.";
+
+    /**
+     * Size of reading buffers.
+     */
+    private static final int BUFFER_SIZE = 0x4000;
+    /* Keep value away from 0 because it is a too probable time stamp value */
+    private static final long NO_VALUE = -1L;
+
+    private static final String LOCK_FILENAME = "MultiDex.lock";
+    private final File sourceApk;
+    private final long sourceCrc;
+    private final File dexDir;
+    private final RandomAccessFile lockRaf;
+    private final FileChannel lockChannel;
+    private final FileLock cacheLock;
+
+    MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
+        Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")");
+        this.sourceApk = sourceApk;
+        this.dexDir = dexDir;
+        sourceCrc = getZipCrc(sourceApk);
+        File lockFile = new File(dexDir, LOCK_FILENAME);
+        lockRaf = new RandomAccessFile(lockFile, "rw");
+        try {
+            lockChannel = lockRaf.getChannel();
+            try {
+                Log.i(TAG, "Blocking on lock " + lockFile.getPath());
+                cacheLock = lockChannel.lock();
+            } catch (IOException | RuntimeException | Error e) {
+                closeQuietly(lockChannel);
+                throw e;
+            }
+            Log.i(TAG, lockFile.getPath() + " locked");
+        } catch (IOException | RuntimeException | Error e) {
+            closeQuietly(lockRaf);
+            throw e;
+        }
+    }
+
+    /**
+     * Extracts application secondary dexes into files in the application data
+     * directory.
+     *
+     * @return a list of files that were created. The list may be empty if there
+     *         are no secondary dex files. Never return null.
+     * @throws IOException if encounters a problem while reading or writing
+     *         secondary dex files
+     */
+    List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
+            throws IOException {
+        Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +
+                prefsKeyPrefix + ")");
+
+        if (!cacheLock.isValid()) {
+            throw new IllegalStateException("MultiDexExtractor was closed");
+        }
+
+        List<ExtractedDex> files;
+        if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
+            try {
+                files = loadExistingExtractions(context, prefsKeyPrefix);
+            } catch (IOException ioe) {
+                Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+                        + " falling back to fresh extraction", ioe);
+                files = performExtractions();
+                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
+                        files);
+            }
+        } else {
+            if (forceReload) {
+                Log.i(TAG, "Forced extraction must be performed.");
+            } else {
+                Log.i(TAG, "Detected that extraction must be performed.");
+            }
+            files = performExtractions();
+            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
+                    files);
+        }
+
+        Log.i(TAG, "load found " + files.size() + " secondary dex files");
+        return files;
+    }
+
+    @Override
+    public void close() throws IOException {
+        cacheLock.release();
+        lockChannel.close();
+        lockRaf.close();
+    }
+
+    /**
+     * Load previously extracted secondary dex files. Should be called only while owning the lock on
+     * {@link #LOCK_FILENAME}.
+     */
+    private List<ExtractedDex> loadExistingExtractions(
+            Context context,
+            String prefsKeyPrefix)
+            throws IOException {
+        Log.i(TAG, "loading existing secondary dex files");
+
+        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
+        SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
+        int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 1);
+        final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1);
+
+        for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
+            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
+            ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
+            if (extractedFile.isFile()) {
+                extractedFile.crc = getZipCrc(extractedFile);
+                long expectedCrc = multiDexPreferences.getLong(
+                        prefsKeyPrefix + KEY_DEX_CRC + secondaryNumber, NO_VALUE);
+                long expectedModTime = multiDexPreferences.getLong(
+                        prefsKeyPrefix + KEY_DEX_TIME + secondaryNumber, NO_VALUE);
+                long lastModified = extractedFile.lastModified();
+                if ((expectedModTime != lastModified)
+                        || (expectedCrc != extractedFile.crc)) {
+                    throw new IOException("Invalid extracted dex: " + extractedFile +
+                            " (key \"" + prefsKeyPrefix + "\"), expected modification time: "
+                            + expectedModTime + ", modification time: "
+                            + lastModified + ", expected crc: "
+                            + expectedCrc + ", file crc: " + extractedFile.crc);
+                }
+                files.add(extractedFile);
+            } else {
+                throw new IOException("Missing extracted secondary dex file '" +
+                        extractedFile.getPath() + "'");
+            }
+        }
+
+        return files;
+    }
+
+
+    /**
+     * Compare current archive and crc with values stored in {@link SharedPreferences}. Should be
+     * called only while owning the lock on {@link #LOCK_FILENAME}.
+     */
+    private static boolean isModified(Context context, File archive, long currentCrc,
+            String prefsKeyPrefix) {
+        SharedPreferences prefs = getMultiDexPreferences(context);
+        return (prefs.getLong(prefsKeyPrefix + KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
+                || (prefs.getLong(prefsKeyPrefix + KEY_CRC, NO_VALUE) != currentCrc);
+    }
+
+    private static long getTimeStamp(File archive) {
+        long timeStamp = archive.lastModified();
+        if (timeStamp == NO_VALUE) {
+            // never return NO_VALUE
+            timeStamp--;
+        }
+        return timeStamp;
+    }
+
+
+    private static long getZipCrc(File archive) throws IOException {
+        long computedValue = ZipUtil.getZipCrc(archive);
+        if (computedValue == NO_VALUE) {
+            // never return NO_VALUE
+            computedValue--;
+        }
+        return computedValue;
+    }
+
+    private List<ExtractedDex> performExtractions() throws IOException {
+
+        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
+
+        // It is safe to fully clear the dex dir because we own the file lock so no other process is
+        // extracting or running optimizing dexopt. It may cause crash of already running
+        // applications if for whatever reason we end up extracting again over a valid extraction.
+        clearDexDir();
+
+        List<ExtractedDex> files = new ArrayList<ExtractedDex>();
+
+        final ZipFile apk = new ZipFile(sourceApk);
+        try {
+
+            int secondaryNumber = 2;
+
+            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
+            while (dexFile != null) {
+                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
+                ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
+                files.add(extractedFile);
+
+                Log.i(TAG, "Extraction is needed for file " + extractedFile);
+                int numAttempts = 0;
+                boolean isExtractionSuccessful = false;
+                while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
+                    numAttempts++;
+
+                    // Create a zip file (extractedFile) containing only the secondary dex file
+                    // (dexFile) from the apk.
+                    extract(apk, dexFile, extractedFile, extractedFilePrefix);
+
+                    // Read zip crc of extracted dex
+                    try {
+                        extractedFile.crc = getZipCrc(extractedFile);
+                        isExtractionSuccessful = true;
+                    } catch (IOException e) {
+                        isExtractionSuccessful = false;
+                        Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e);
+                    }
+
+                    // Log size and crc of the extracted zip file
+                    Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
+                            + " '" + extractedFile.getAbsolutePath() + "': length "
+                            + extractedFile.length() + " - crc: " + extractedFile.crc);
+                    if (!isExtractionSuccessful) {
+                        // Delete the extracted file
+                        extractedFile.delete();
+                        if (extractedFile.exists()) {
+                            Log.w(TAG, "Failed to delete corrupted secondary dex '" +
+                                    extractedFile.getPath() + "'");
+                        }
+                    }
+                }
+                if (!isExtractionSuccessful) {
+                    throw new IOException("Could not create zip file " +
+                            extractedFile.getAbsolutePath() + " for secondary dex (" +
+                            secondaryNumber + ")");
+                }
+                secondaryNumber++;
+                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
+            }
+        } finally {
+            try {
+                apk.close();
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to close resource", e);
+            }
+        }
+
+        return files;
+    }
+
+    /**
+     * Save {@link SharedPreferences}. Should be called only while owning the lock on
+     * {@link #LOCK_FILENAME}.
+     */
+    private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp,
+            long crc, List<ExtractedDex> extractedDexes) {
+        SharedPreferences prefs = getMultiDexPreferences(context);
+        SharedPreferences.Editor edit = prefs.edit();
+        edit.putLong(keyPrefix + KEY_TIME_STAMP, timeStamp);
+        edit.putLong(keyPrefix + KEY_CRC, crc);
+        edit.putInt(keyPrefix + KEY_DEX_NUMBER, extractedDexes.size() + 1);
+
+        int extractedDexId = 2;
+        for (ExtractedDex dex : extractedDexes) {
+            edit.putLong(keyPrefix + KEY_DEX_CRC + extractedDexId, dex.crc);
+            edit.putLong(keyPrefix + KEY_DEX_TIME + extractedDexId, dex.lastModified());
+            extractedDexId++;
+        }
+        /* Use commit() and not apply() as advised by the doc because we need synchronous writing of
+         * the editor content and apply is doing an "asynchronous commit to disk".
+         */
+        edit.commit();
+    }
+
+    /**
+     * Get the MuliDex {@link SharedPreferences} for the current application. Should be called only
+     * while owning the lock on {@link #LOCK_FILENAME}.
+     */
+    private static SharedPreferences getMultiDexPreferences(Context context) {
+        return context.getSharedPreferences(PREFS_FILE,
+                Build.VERSION.SDK_INT < 11 /* Build.VERSION_CODES.HONEYCOMB */
+                        ? Context.MODE_PRIVATE
+                        : Context.MODE_PRIVATE | 0x0004 /* Context.MODE_MULTI_PROCESS */);
+    }
+
+    /**
+     * Clear the dex dir from all files but the lock.
+     */
+    private void clearDexDir() {
+        File[] files = dexDir.listFiles(new FileFilter() {
+            @Override
+            public boolean accept(File pathname) {
+                return !pathname.getName().equals(LOCK_FILENAME);
+            }
+        });
+        if (files == null) {
+            Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
+            return;
+        }
+        for (File oldFile : files) {
+            Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
+                    oldFile.length());
+            if (!oldFile.delete()) {
+                Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
+            } else {
+                Log.i(TAG, "Deleted old file " + oldFile.getPath());
+            }
+        }
+    }
+
+    private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
+            String extractedFilePrefix) throws IOException, FileNotFoundException {
+
+        InputStream in = apk.getInputStream(dexFile);
+        ZipOutputStream out = null;
+        // Temp files must not start with extractedFilePrefix to get cleaned up in prepareDexDir()
+        File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX,
+                extractTo.getParentFile());
+        Log.i(TAG, "Extracting " + tmp.getPath());
+        try {
+            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
+            try {
+                ZipEntry classesDex = new ZipEntry("classes.dex");
+                // keep zip entry time since it is the criteria used by Dalvik
+                classesDex.setTime(dexFile.getTime());
+                out.putNextEntry(classesDex);
+
+                byte[] buffer = new byte[BUFFER_SIZE];
+                int length = in.read(buffer);
+                while (length != -1) {
+                    out.write(buffer, 0, length);
+                    length = in.read(buffer);
+                }
+                out.closeEntry();
+            } finally {
+                out.close();
+            }
+            if (!tmp.setReadOnly()) {
+                throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() +
+                        "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
+            }
+            Log.i(TAG, "Renaming to " + extractTo.getPath());
+            if (!tmp.renameTo(extractTo)) {
+                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
+                        "\" to \"" + extractTo.getAbsolutePath() + "\"");
+            }
+        } finally {
+            closeQuietly(in);
+            tmp.delete(); // return status ignored
+        }
+    }
+
+    /**
+     * Closes the given {@code Closeable}. Suppresses any IO exceptions.
+     */
+    private static void closeQuietly(Closeable closeable) {
+        try {
+            closeable.close();
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to close resource", e);
+        }
+    }
+}
diff --git a/library/src/androidx/multidex/ZipUtil.java b/library/src/androidx/multidex/ZipUtil.java
new file mode 100644
index 0000000..fc33623
--- /dev/null
+++ b/library/src/androidx/multidex/ZipUtil.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* Apache Harmony HEADER because the code in this class comes mostly from ZipFile, ZipEntry and
+ * ZipConstants from android libcore.
+ */
+
+package androidx.multidex;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.zip.CRC32;
+import java.util.zip.ZipException;
+
+/**
+ * Tools to build a quick partial crc of zip files.
+ */
+final class ZipUtil {
+    static class CentralDirectory {
+        long offset;
+        long size;
+    }
+
+    /* redefine those constant here because of bug 13721174 preventing to compile using the
+     * constants defined in ZipFile */
+    private static final int ENDHDR = 22;
+    private static final int ENDSIG = 0x6054b50;
+
+    /**
+     * Size of reading buffers.
+     */
+    private static final int BUFFER_SIZE = 0x4000;
+
+    /**
+     * Compute crc32 of the central directory of an apk. The central directory contains
+     * the crc32 of each entries in the zip so the computed result is considered valid for the whole
+     * zip file. Does not support zip64 nor multidisk but it should be OK for now since ZipFile does
+     * not either.
+     */
+    static long getZipCrc(File apk) throws IOException {
+        RandomAccessFile raf = new RandomAccessFile(apk, "r");
+        try {
+            CentralDirectory dir = findCentralDirectory(raf);
+
+            return computeCrcOfCentralDir(raf, dir);
+        } finally {
+            raf.close();
+        }
+    }
+
+    /* Package visible for testing */
+    static CentralDirectory findCentralDirectory(RandomAccessFile raf) throws IOException,
+            ZipException {
+        long scanOffset = raf.length() - ENDHDR;
+        if (scanOffset < 0) {
+            throw new ZipException("File too short to be a zip file: " + raf.length());
+        }
+
+        long stopOffset = scanOffset - 0x10000 /* ".ZIP file comment"'s max length */;
+        if (stopOffset < 0) {
+            stopOffset = 0;
+        }
+
+        int endSig = Integer.reverseBytes(ENDSIG);
+        while (true) {
+            raf.seek(scanOffset);
+            if (raf.readInt() == endSig) {
+                break;
+            }
+
+            scanOffset--;
+            if (scanOffset < stopOffset) {
+                throw new ZipException("End Of Central Directory signature not found");
+            }
+        }
+        // Read the End Of Central Directory. ENDHDR includes the signature
+        // bytes,
+        // which we've already read.
+
+        // Pull out the information we need.
+        raf.skipBytes(2); // diskNumber
+        raf.skipBytes(2); // diskWithCentralDir
+        raf.skipBytes(2); // numEntries
+        raf.skipBytes(2); // totalNumEntries
+        CentralDirectory dir = new CentralDirectory();
+        dir.size = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL;
+        dir.offset = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL;
+        return dir;
+    }
+
+    /* Package visible for testing */
+    static long computeCrcOfCentralDir(RandomAccessFile raf, CentralDirectory dir)
+            throws IOException {
+        CRC32 crc = new CRC32();
+        long stillToRead = dir.size;
+        raf.seek(dir.offset);
+        int length = (int) Math.min(BUFFER_SIZE, stillToRead);
+        byte[] buffer = new byte[BUFFER_SIZE];
+        length = raf.read(buffer, 0, length);
+        while (length != -1) {
+            crc.update(buffer, 0, length);
+            stillToRead -= length;
+            if (stillToRead == 0) {
+                break;
+            }
+            length = (int) Math.min(BUFFER_SIZE, stillToRead);
+            length = raf.read(buffer, 0, length);
+        }
+        return crc.getValue();
+    }
+}