Initial move of code from frameworks/support/multidex to here

Also,
- Removed extraneous Eclipse-specific configuration files that are
  unnecessary in the Android build tree
- Includes the patch to support API level 19 from
  https://googleplex-android-review.git.corp.google.com/#/c/348129
- Checks for null and returns without patching the classloader when
  getting the package manager or package name from the passed in Context
  to MultiDex.install(...) since the Context object is probably a mock
  context for testing.
- Moved the test runners to package com.android.test.runner, which seems
  to be the standard place for all test runners.

Bug: 10674263
Change-Id: Idc894b360bd17db4acb50dd7daa2839ea8ea37e0
diff --git a/instrumentation/Android.mk b/instrumentation/Android.mk
new file mode 100644
index 0000000..22ee642
--- /dev/null
+++ b/instrumentation/Android.mk
@@ -0,0 +1,22 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-multidex-instrumentation
+LOCAL_JAVA_LIBRARIES := android-support-multidex android-test-lib
+LOCAL_SDK_VERSION := 4
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/instrumentation/README.txt b/instrumentation/README.txt
new file mode 100644
index 0000000..322676b
--- /dev/null
+++ b/instrumentation/README.txt
@@ -0,0 +1,6 @@
+Library Project including compatibility IntrumentationTestRunner
+for multiple dex applications.
+
+This can be used by an Android test project to set up the classloader
+of applications with multiple dexes.
+
diff --git a/instrumentation/src/com/android/test/runner/MultiDexAndroidJUnitRunner.java b/instrumentation/src/com/android/test/runner/MultiDexAndroidJUnitRunner.java
new file mode 100644
index 0000000..afb6e08
--- /dev/null
+++ b/instrumentation/src/com/android/test/runner/MultiDexAndroidJUnitRunner.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.android.test.runner;
+
+import com.android.test.runner.AndroidJUnitRunner;
+
+import android.os.Bundle;
+import android.support.multidex.MultiDex;
+
+/**
+ * Extends AndroidJUnitRunner to patch up things for GMS Core multi-dex support.
+ */
+public class MultiDexAndroidJUnitRunner extends AndroidJUnitRunner {
+    @Override
+    public void onCreate(Bundle arguments) {
+        MultiDex.install(getTargetContext());
+        super.onCreate(arguments);
+    }
+}
diff --git a/instrumentation/src/com/android/test/runner/MultiDexTestRunner.java b/instrumentation/src/com/android/test/runner/MultiDexTestRunner.java
new file mode 100644
index 0000000..a93a740
--- /dev/null
+++ b/instrumentation/src/com/android/test/runner/MultiDexTestRunner.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.android.test.runner;
+
+import android.os.Bundle;
+import android.support.multidex.MultiDex;
+import android.test.InstrumentationTestRunner;
+
+public class MultiDexTestRunner extends InstrumentationTestRunner {
+
+    @Override
+    public void onCreate(Bundle arguments) {
+        MultiDex.install(getTargetContext());
+        super.onCreate(arguments);
+    }
+
+}
diff --git a/library/Android.mk b/library/Android.mk
new file mode 100644
index 0000000..7e14a45
--- /dev/null
+++ b/library/Android.mk
@@ -0,0 +1,21 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-multidex
+LOCAL_SDK_VERSION := 4
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/library/README.txt b/library/README.txt
new file mode 100644
index 0000000..2fde7e2
--- /dev/null
+++ b/library/README.txt
@@ -0,0 +1,6 @@
+Library Project including compatibility multi dex loader.
+
+This can be used by an Android project to install classloader
+with multiple dex of applications running on API 4+.
+
+
diff --git a/library/src/android/support/multidex/MultiDex.java b/library/src/android/support/multidex/MultiDex.java
new file mode 100644
index 0000000..1fffe3d
--- /dev/null
+++ b/library/src/android/support/multidex/MultiDex.java
@@ -0,0 +1,383 @@
+/*
+ * 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 android.support.multidex;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+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.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;
+
+/**
+ * Monkey 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} file necessary for calling this class methods. secondary
+ * dex files named classes2.dex, classes".dex... found in the application apk
+ * will be added to the classloader after first call to
+ * {@link #install(Context)}.
+ *
+ * <p/>
+ * <strong>IMPORTANT:</strong>This library provides compatibility for platforms
+ * with API level 4 through 19. 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 SECONDARY_FOLDER_NAME = "secondary-dexes";
+
+    private static final int SUPPORTED_MULTIDEX_SDK_VERSION = 20;
+
+    private static final int MIN_SDK_VERSION = 4;
+
+    private static final Set<String> installedApk = new HashSet<String>();
+
+    private MultiDex() {}
+
+    /**
+     * Patches the application context class loader by appending extra dex files
+     * loaded from the application apk. Call this method first thing in your
+     * {@code Application#OnCreate}, {@code Instrumentation#OnCreate},
+     * {@code BackupAgent#OnCreate}, {@code Service#OnCreate},
+     * {@code BroadcastReceiver#onReceive}, {@code Activity#OnCreate} and
+     * {@code ContentProvider#OnCreate} .
+     *
+     * @param context application context.
+     * @throws RuntimeException if an error occurred preventing the classloader
+     *         extension.
+     */
+    public static void install(Context context) {
+
+        if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
+            throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
+                    + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
+        }
+
+
+        try {
+            PackageManager pm;
+            String packageName;
+            try {
+                pm = context.getPackageManager();
+                packageName = context.getPackageName();
+            } 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;
+            }
+            if (pm == null || packageName == null) {
+                // This is most likely a mock context, so just return without patching.
+                return;
+            }
+            ApplicationInfo applicationInfo =
+                    pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
+            if (applicationInfo == null) {
+                // This is from a mock context, so just return without patching.
+                return;
+            }
+
+            synchronized (installedApk) {
+                String apkPath = applicationInfo.sourceDir;
+                if (installedApk.contains(apkPath)) {
+                    return;
+                }
+                installedApk.add(apkPath);
+
+                if (Build.VERSION.SDK_INT >= SUPPORTED_MULTIDEX_SDK_VERSION) {
+                    // STOPSHIP: Any app that uses this class needs approval before being released
+                    // as well as figuring out what the right behavior should be here.
+                    throw new RuntimeException("Platform support of multidex for SDK " +
+                            Build.VERSION.SDK_INT + " has not been confirmed yet.");
+                }
+
+                /* 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 = context.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;
+                }
+
+                File dexDir = new File(context.getFilesDir(), SECONDARY_FOLDER_NAME);
+                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir);
+                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, dexDir);
+                    } else {
+                        V4.install(loader, files, dexDir);
+                    }
+                }
+            }
+
+        } catch (Exception e) {
+            Log.e(TAG, "Multidex installation failure", e);
+            throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
+        }
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Installer for platform versions 19.
+     */
+    private static final class V19 {
+
+        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
+                File optimizedDirectory)
+                        throws IllegalArgumentException, IllegalAccessException,
+                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
+            /* 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) {
+                Field suppressedExceptionsField =
+                        findField(loader, "dexElementsSuppressedExceptions");
+                IOException[] dexElementsSuppressedExceptions =
+                        (IOException[]) suppressedExceptionsField.get(loader);
+
+                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(loader, dexElementsSuppressedExceptions);
+            }
+        }
+
+        /**
+         * 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 static void install(ClassLoader loader, List<File> additionalClassPathEntries,
+                File optimizedDirectory)
+                        throws IllegalArgumentException, IllegalAccessException,
+                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
+            /* 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);
+            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
+                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
+        }
+
+        /**
+         * A wrapper around
+         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
+         */
+        private static Object[] makeDexElements(
+                Object dexPathList, ArrayList<File> files, File optimizedDirectory)
+                        throws IllegalAccessException, InvocationTargetException,
+                        NoSuchMethodException {
+            Method makeDexElements =
+                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
+
+            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
+        }
+    }
+
+    /**
+     * Installer for platform versions 4 to 13.
+     */
+    private static final class V4 {
+        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
+                File optimizedDirectory)
+                        throws IllegalArgumentException, IllegalAccessException,
+                        NoSuchFieldException, IOException {
+            /* The patched class loader is expected to be a descendant of
+             * dalvik.system.DexClassLoader. We modify its
+             * dalvik.system.DexPathList pathList field 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];
+            DexFile[] extraDexs = new DexFile[extraSize];
+            for (ListIterator<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;
+                extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
+            }
+
+            pathField.set(loader, path.toString());
+            expandFieldArray(loader, "mPaths", extraPaths);
+            expandFieldArray(loader, "mFiles", extraFiles);
+            expandFieldArray(loader, "mDexs", extraDexs);
+        }
+    }
+
+}
diff --git a/library/src/android/support/multidex/MultiDexExtractor.java b/library/src/android/support/multidex/MultiDexExtractor.java
new file mode 100644
index 0000000..220f2aa
--- /dev/null
+++ b/library/src/android/support/multidex/MultiDexExtractor.java
@@ -0,0 +1,175 @@
+/*
+ * 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 android.support.multidex;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+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.
+ */
+final class MultiDexExtractor {
+
+    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";
+    private static final String DEX_SUFFIX = ".dex";
+
+    private static final String EXTRACTED_NAME_EXT = ".classes";
+    private static final String EXTRACTED_SUFFIX = ".zip";
+
+    private static final int BUFFER_SIZE = 0x4000;
+
+    /**
+     * Extracts application secondary dexes into files in the application data
+     * directory.
+     *
+     * @param dexDir
+     *
+     * @return a list of files that were created. The list may be empty if there
+     *         are no secondary dex files.
+     * @throws IOException if encounters a problem while reading or writing
+     *         secondary dex files
+     */
+    static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir)
+            throws IOException {
+
+        String extractedFilePrefix = new File(applicationInfo.sourceDir).getName()
+                + EXTRACTED_NAME_EXT;
+
+        prepareDexDir(dexDir, extractedFilePrefix);
+
+        final List<File> files = new ArrayList<File>();
+        ZipFile apk = new ZipFile(applicationInfo.sourceDir);
+        try {
+
+            int secondaryNumber = 2;
+
+            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
+            while (dexFile != null) {
+                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
+                File extractedFile = new File(dexDir, fileName);
+                files.add(extractedFile);
+
+                if (!extractedFile.isFile()) {
+                    extract(context, apk, dexFile, extractedFile, extractedFilePrefix);
+                }
+                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;
+    }
+
+    private static void prepareDexDir(File dexDir, final String extractedFilePrefix)
+            throws IOException {
+        dexDir.mkdir();
+        if (!dexDir.isDirectory()) {
+            throw new IOException("Failed to create dex directory " + dexDir.getPath());
+        }
+
+        // Clean possible old files
+        FilenameFilter filter = new FilenameFilter() {
+            @Override
+            public boolean accept(File dir, String name) {
+                return !name.startsWith(extractedFilePrefix);
+            }
+        };
+        File[] files = dexDir.listFiles(filter);
+        if (files == null) {
+            Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
+            return;
+        }
+        for (File oldFile : files) {
+            if (!oldFile.delete()) {
+                Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
+            }
+        }
+    }
+
+    private static void extract(
+            Context context, ZipFile apk, ZipEntry dexFile, File extractTo,
+            String extractedFilePrefix) throws IOException, FileNotFoundException {
+
+        InputStream in = apk.getInputStream(dexFile);
+        ZipOutputStream out = null;
+        File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
+                extractTo.getParentFile());
+        Log.i(TAG, "Extracting " + tmp.getPath());
+        try {
+            out = new ZipOutputStream(new FileOutputStream(tmp));
+            try {
+                ZipEntry classesDex = new ZipEntry("classes.dex");
+                out.putNextEntry(classesDex);
+
+                byte[] buffer = new byte[BUFFER_SIZE];
+                int length = in.read(buffer);
+                while (length > 0) {
+                    out.write(buffer, 0, length);
+                    length = in.read(buffer);
+                }
+            } finally {
+                closeQuietly(out);
+            }
+            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);
+        }
+    }
+}