diff --git a/cells/switchsystem/src/com/cells/cellswitch/secure/view/SwitchActivity.java b/cells/switchsystem/src/com/cells/cellswitch/secure/view/SwitchActivity.java
index aa161adf25..873a99e1bc 100755
--- a/cells/switchsystem/src/com/cells/cellswitch/secure/view/SwitchActivity.java
+++ b/cells/switchsystem/src/com/cells/cellswitch/secure/view/SwitchActivity.java
@@ -11,8 +11,9 @@
import android.os.ServiceManager;
import android.os.Looper;
import android.os.RemoteException;
-import android.os.CellsManager;
-import android.os.ICellsManager;
+import android.os.ICellsService;
+// import android.os.CellsManager;
+// import android.os.ICellsManager;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.Animation;
diff --git a/frameworks/multidex/library/Android.bp b/frameworks/multidex/library/Android.bp
new file mode 100644
index 0000000000..6ba580467f
--- /dev/null
+++ b/frameworks/multidex/library/Android.bp
@@ -0,0 +1,48 @@
+// Copyright (C) 2016 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.
+
+genrule {
+ name: "android-support-multidex-version",
+ tools: [
+ "soong_zip",
+ ],
+ // use srcs as dependencies, otherwise, this module won't be re-run
+ // during incremental build.
+ srcs: [
+ "src/**/*.java",
+ ],
+ cmd: "echo \"git.version=`cd frameworks/multidex/library; git log --format=\"%H\" -n 1 || " +
+ "(echo git hash not available; exit 0)`\" > $(genDir)/android-support-multidex.version.txt",
+ out: [
+ "android-support-multidex.version.txt",
+ ],
+}
+
+java_library_static {
+ name: "android-support-multidex",
+ sdk_version: "15",
+ min_sdk_version: "4",
+ srcs: [
+ "src/**/*.java",
+ ],
+ java_resources: [
+ ":android-support-multidex-version",
+ ],
+ product_variables: {
+ unbundled_build: {
+ // Don't build the library in unbundled branches.
+ enabled: false,
+ },
+ },
+}
diff --git a/frameworks/multidex/library/AndroidManifest.xml b/frameworks/multidex/library/AndroidManifest.xml
new file mode 100644
index 0000000000..3a28469eca
--- /dev/null
+++ b/frameworks/multidex/library/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/frameworks/multidex/library/README b/frameworks/multidex/library/README
new file mode 100644
index 0000000000..62e8737831
--- /dev/null
+++ b/frameworks/multidex/library/README
@@ -0,0 +1,15 @@
+Library Project including a multidex loader.
+
+This can be used by an Android project to install multiple dexes
+in the classloader of an application running on API 4+.
+
+Note that multidexing will allow to go over the dex index limit.
+It can also help with the linearalloc limit during installation but it
+won't help with linearalloc at execution time. This means that
+most applications requiring multidexing because of the dex index
+limit won't execute on API below 14 because of linearalloc limit.
+
+There is technically no source, but the src folder is necessary
+to ensure that the build system works. The content is actually
+located in libs/android-support-multidex.jar.
+
diff --git a/frameworks/multidex/library/build.gradle b/frameworks/multidex/library/build.gradle
new file mode 100644
index 0000000000..fe707f06ac
--- /dev/null
+++ b/frameworks/multidex/library/build.gradle
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+apply plugin: 'com.android.library'
+
+def generatedResourceDir = project.file('generatedResource')
+def versionFile = new File(generatedResourceDir, 'androidsupportmultidexversion.txt')
+
+task makeVersionFile(type:Exec) {
+
+ doFirst {
+ versionFile.getParentFile().mkdirs()
+ }
+
+ outputs.files versionFile
+
+ commandLine 'sh', '-c', 'git log --format="%H" -n 1 || (echo git hash not available; exit 0)'
+ standardOutput = new ByteArrayOutputStream()
+
+ doLast {
+ versionFile.text = "git.version=" + standardOutput.toString()
+ }
+}
+
+android {
+ compileSdkVersion gradle.currentSdk
+
+ defaultConfig {
+ minSdkVersion 4
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ sourceSets {
+ main {
+ java.srcDirs = ['src']
+ resources.srcDirs = ['res', makeVersionFile.outputs]
+ res.srcDirs = ['src']
+ manifest.srcFile 'AndroidManifest.xml'
+ }
+ }
+}
+
+android.libraryVariants.all { variant ->
+ variant.getJavaCompiler().dependsOn(makeVersionFile)
+
+ if (!name.equals(com.android.builder.core.BuilderConstants.RELEASE)) {
+ return // Skip non-release
+ }
+
+
+ def sourceJar = project.tasks.create(name: "sourceJarRelease", type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.java.srcDirs
+ }
+ artifacts.add("archives", sourceJar)
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ repository(url: uri(rootProject.ext.supportRepoOut)) {
+ }
+
+ pom.project {
+ name 'Android Multi-Dex Library'
+ description "Library for legacy multi-dex support"
+ url ''
+ inceptionYear '2013'
+
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+
+ scm {
+ url "http://source.android.com"
+ connection "scm:git:https://android.googlesource.com/platform/frameworks/multidex"
+ }
+ developers {
+ developer {
+ name 'The Android Open Source Project'
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/frameworks/multidex/library/jack-meta/legacyMultidexInstallation.jpp b/frameworks/multidex/library/jack-meta/legacyMultidexInstallation.jpp
new file mode 100644
index 0000000000..a5a7167c68
--- /dev/null
+++ b/frameworks/multidex/library/jack-meta/legacyMultidexInstallation.jpp
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+multidexInstanciable:
+ @@com.android.jack.annotations.MultiDexInstaller
+ class *
+ is {
+ public
+ }
+ extends {
+ class android.app.Instrumentation
+ | class android.app.Activity
+ | class android.app.Service
+ | class android.content.ContentProvider
+ | class android.content.BroadcastReceiver
+ | class android.app.backup.BackupAgent
+ | class android.app.Application
+ }
+ do {
+ @@com.android.jack.annotations.MultiDexInstaller
+ method void ();
+ }
+
+multidexInstaller:
+ class *
+ is {
+ public
+ }
+ extends {
+ class android.app.Application
+ }
+ do {
+ @@com.android.jack.annotations.MultiDexInstaller
+ method void attachBaseContext(class android.content.Context)
+ ;
+ }
+
+instrumentationTestCase:
+ @@com.android.jack.annotations.ForceInMainDex
+ class *
+ extends {
+ class android.test.InstrumentationTestCase
+ }
diff --git a/frameworks/multidex/library/src/androidx/multidex/MultiDex.java b/frameworks/multidex/library/src/androidx/multidex/MultiDex.java
new file mode 100644
index 0000000000..c038709d64
--- /dev/null
+++ b/frameworks/multidex/library/src/androidx/multidex/MultiDex.java
@@ -0,0 +1,783 @@
+/*
+ * 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.StringTokenizer;
+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)}.
+ *
+ *
+ * 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 installedApk = new HashSet();
+
+ 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 ClassLoader capable of loading DEX
+ * bytecode. We modify its pathList field to append additional DEX file entries.
+ */
+ ClassLoader loader = getDexClassloader(mainContext);
+ if (loader == null) {
+ 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;
+ }
+ }
+ }
+
+ /**
+ * Returns a {@link Classloader} from the {@link Context} that is capable of reading dex
+ * bytecode or null if the Classloader is not dex-capable e.g: when running on a JVM testing
+ * environment such as Robolectric.
+ */
+ private static ClassLoader getDexClassloader(Context context) {
+ 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 null;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ if (loader instanceof dalvik.system.BaseDexClassLoader) {
+ return loader;
+ }
+ } else if (loader instanceof dalvik.system.DexClassLoader
+ || loader instanceof dalvik.system.PathClassLoader) {
+ return loader;
+ }
+ Log.e(TAG, "Context class loader is null or not dex-capable. "
+ + "Must be running in test mode. Skip patching.");
+ return null;
+ }
+
+ 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) {
+ StringTokenizer tokenizer = new StringTokenizer(versionString, ".");
+ String majorToken = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+ String minorToken = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+ if (majorToken != null && minorToken != null) {
+ try {
+ int major = Integer.parseInt(majorToken);
+ int minor = Integer.parseInt(minorToken);
+ 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 suppressedExceptions = new ArrayList();
+ expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
+ new ArrayList(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 files, File optimizedDirectory,
+ ArrayList 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/frameworks/multidex/library/src/androidx/multidex/MultiDexApplication.java b/frameworks/multidex/library/src/androidx/multidex/MultiDexApplication.java
new file mode 100644
index 0000000000..4c759e3dc3
--- /dev/null
+++ b/frameworks/multidex/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:
+ *
+ * - Declare this class as the application in your AndroidManifest.xml.
+ * - Have your {@link Application} extends this class.
+ * - Have your {@link Application} override attachBaseContext starting with
+ *
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ MultiDex.install(this);
+
+ *
+ */
+public class MultiDexApplication extends Application {
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ MultiDex.install(this);
+ }
+}
diff --git a/frameworks/multidex/library/src/androidx/multidex/MultiDexExtractor.java b/frameworks/multidex/library/src/androidx/multidex/MultiDexExtractor.java
new file mode 100644
index 0000000000..2b961138da
--- /dev/null
+++ b/frameworks/multidex/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 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 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 files = new ArrayList(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 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 files = new ArrayList();
+
+ 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 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/frameworks/multidex/library/src/androidx/multidex/ZipUtil.java b/frameworks/multidex/library/src/androidx/multidex/ZipUtil.java
new file mode 100644
index 0000000000..fc336230e6
--- /dev/null
+++ b/frameworks/multidex/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();
+ }
+}
diff --git a/frameworks/multidex/library/test/src/android/util/Log.java b/frameworks/multidex/library/test/src/android/util/Log.java
new file mode 100644
index 0000000000..b94e48b383
--- /dev/null
+++ b/frameworks/multidex/library/test/src/android/util/Log.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2006 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.util;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.UnknownHostException;
+
+/**
+ * Mock Log implementation for testing on non android host.
+ */
+public final class Log {
+
+ /**
+ * Priority constant for the println method; use Log.v.
+ */
+ public static final int VERBOSE = 2;
+
+ /**
+ * Priority constant for the println method; use Log.d.
+ */
+ public static final int DEBUG = 3;
+
+ /**
+ * Priority constant for the println method; use Log.i.
+ */
+ public static final int INFO = 4;
+
+ /**
+ * Priority constant for the println method; use Log.w.
+ */
+ public static final int WARN = 5;
+
+ /**
+ * Priority constant for the println method; use Log.e.
+ */
+ public static final int ERROR = 6;
+
+ /**
+ * Priority constant for the println method.
+ */
+ public static final int ASSERT = 7;
+
+ private Log() {
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int v(String tag, String msg) {
+ return println(LOG_ID_MAIN, VERBOSE, tag, msg);
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int v(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int d(String tag, String msg) {
+ return println(LOG_ID_MAIN, DEBUG, tag, msg);
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int d(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Send an {@link #INFO} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int i(String tag, String msg) {
+ return println(LOG_ID_MAIN, INFO, tag, msg);
+ }
+
+ /**
+ * Send a {@link #INFO} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int i(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Send a {@link #WARN} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int w(String tag, String msg) {
+ return println(LOG_ID_MAIN, WARN, tag, msg);
+ }
+
+ /**
+ * Send a {@link #WARN} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int w(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /*
+ * Send a {@link #WARN} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param tr An exception to log
+ */
+ public static int w(String tag, Throwable tr) {
+ return println(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr));
+ }
+
+ /**
+ * Send an {@link #ERROR} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int e(String tag, String msg) {
+ return println(LOG_ID_MAIN, ERROR, tag, msg);
+ }
+
+ /**
+ * Send a {@link #ERROR} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int e(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Handy function to get a loggable stack trace from a Throwable
+ * @param tr An exception to log
+ */
+ public static String getStackTraceString(Throwable tr) {
+ if (tr == null) {
+ return "";
+ }
+
+ // This is to reduce the amount of log spew that apps do in the non-error
+ // condition of the network being unavailable.
+ Throwable t = tr;
+ while (t != null) {
+ if (t instanceof UnknownHostException) {
+ return "";
+ }
+ t = t.getCause();
+ }
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ tr.printStackTrace(pw);
+ pw.flush();
+ return sw.toString();
+ }
+
+ /**
+ * Low-level logging call.
+ * @param priority The priority/type of this log message
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @return The number of bytes written.
+ */
+ public static int println(int priority, String tag, String msg) {
+ return println(LOG_ID_MAIN, priority, tag, msg);
+ }
+
+ /** @hide */ public static final int LOG_ID_MAIN = 0;
+ /** @hide */ public static final int LOG_ID_RADIO = 1;
+ /** @hide */ public static final int LOG_ID_EVENTS = 2;
+ /** @hide */ public static final int LOG_ID_SYSTEM = 3;
+ /** @hide */ public static final int LOG_ID_CRASH = 4;
+
+ /** @hide */ @SuppressWarnings("unused")
+ public static int println(int bufID,
+ int priority, String tag, String msg) {
+ return 0;
+ }
+}
diff --git a/frameworks/multidex/library/test/src/androidx/multidex/MultiDexTest.java b/frameworks/multidex/library/test/src/androidx/multidex/MultiDexTest.java
new file mode 100644
index 0000000000..656b12a1a8
--- /dev/null
+++ b/frameworks/multidex/library/test/src/androidx/multidex/MultiDexTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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 org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test for {@link MultiDex} class.
+ */
+public class MultiDexTest {
+ @Test
+ public void testVersionCheck() {
+ Assert.assertFalse(MultiDex.isVMMultidexCapable(null));
+ Assert.assertFalse(MultiDex.isVMMultidexCapable("-1.32.54"));
+ Assert.assertFalse(MultiDex.isVMMultidexCapable("1.32.54"));
+ Assert.assertFalse(MultiDex.isVMMultidexCapable("1.32"));
+ Assert.assertFalse(MultiDex.isVMMultidexCapable("2.0"));
+ Assert.assertFalse(MultiDex.isVMMultidexCapable("2.000.1254"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("2.1.1254"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("2.1"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("2.2"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("2.1.0000"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("2.2.0000"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("002.0001.0010"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("3.0"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("3.0.0"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("3.0.1"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("3.1.0"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("03.1.132645"));
+ Assert.assertTrue(MultiDex.isVMMultidexCapable("03.2"));
+ }
+}
diff --git a/frameworks/multidex/library/test/src/androidx/multidex/ZipEntryReader.java b/frameworks/multidex/library/test/src/androidx/multidex/ZipEntryReader.java
new file mode 100644
index 0000000000..a16e265ca5
--- /dev/null
+++ b/frameworks/multidex/library/test/src/androidx/multidex/ZipEntryReader.java
@@ -0,0 +1,120 @@
+/*
+ * 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.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+
+class ZipEntryReader {
+ static final Charset UTF_8 = Charset.forName("UTF-8");
+ /**
+ * General Purpose Bit Flags, Bit 0.
+ * If set, indicates that the file is encrypted.
+ */
+ private static final int GPBF_ENCRYPTED_FLAG = 1 << 0;
+
+ /**
+ * Supported General Purpose Bit Flags Mask.
+ * Bit mask of bits not supported.
+ * Note: The only bit that we will enforce at this time
+ * is the encrypted bit. Although other bits are not supported,
+ * we must not enforce them as this could break some legitimate
+ * use cases (See http://b/8617715).
+ */
+ private static final int GPBF_UNSUPPORTED_MASK = GPBF_ENCRYPTED_FLAG;
+ private static final long CENSIG = 0x2014b50;
+
+ static ZipEntry readEntry(ByteBuffer in) throws IOException {
+
+ int sig = in.getInt();
+ if (sig != CENSIG) {
+ throw new ZipException("Central Directory Entry not found");
+ }
+
+ in.position(8);
+ int gpbf = in.getShort() & 0xffff;
+
+ if ((gpbf & GPBF_UNSUPPORTED_MASK) != 0) {
+ throw new ZipException("Invalid General Purpose Bit Flag: " + gpbf);
+ }
+
+ int compressionMethod = in.getShort() & 0xffff;
+ int time = in.getShort() & 0xffff;
+ int modDate = in.getShort() & 0xffff;
+
+ // These are 32-bit values in the file, but 64-bit fields in this object.
+ long crc = ((long) in.getInt()) & 0xffffffffL;
+ long compressedSize = ((long) in.getInt()) & 0xffffffffL;
+ long size = ((long) in.getInt()) & 0xffffffffL;
+
+ int nameLength = in.getShort() & 0xffff;
+ int extraLength = in.getShort() & 0xffff;
+ int commentByteCount = in.getShort() & 0xffff;
+
+ // This is a 32-bit value in the file, but a 64-bit field in this object.
+ in.position(42);
+ long localHeaderRelOffset = ((long) in.getInt()) & 0xffffffffL;
+
+ byte[] nameBytes = new byte[nameLength];
+ in.get(nameBytes, 0, nameBytes.length);
+ String name = new String(nameBytes, 0, nameBytes.length, UTF_8);
+
+ ZipEntry entry = new ZipEntry(name);
+ entry.setMethod(compressionMethod);
+ entry.setTime(getTime(time, modDate));
+
+ entry.setCrc(crc);
+ entry.setCompressedSize(compressedSize);
+ entry.setSize(size);
+
+ // The RI has always assumed UTF-8. (If GPBF_UTF8_FLAG isn't set, the encoding is
+ // actually IBM-437.)
+ if (commentByteCount > 0) {
+ byte[] commentBytes = new byte[commentByteCount];
+ in.get(commentBytes, 0, commentByteCount);
+ entry.setComment(new String(commentBytes, 0, commentBytes.length, UTF_8));
+ }
+
+ if (extraLength > 0) {
+ byte[] extra = new byte[extraLength];
+ in.get(extra, 0, extraLength);
+ entry.setExtra(extra);
+ }
+
+ return entry;
+
+ }
+
+ private static long getTime(int time, int modDate) {
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.set(1980 + ((modDate >> 9) & 0x7f), ((modDate >> 5) & 0xf) - 1,
+ modDate & 0x1f, (time >> 11) & 0x1f, (time >> 5) & 0x3f,
+ (time & 0x1f) << 1);
+ return cal.getTime().getTime();
+ }
+
+}
diff --git a/frameworks/multidex/library/test/src/androidx/multidex/ZipUtilTest.java b/frameworks/multidex/library/test/src/androidx/multidex/ZipUtilTest.java
new file mode 100644
index 0000000000..39a59ac578
--- /dev/null
+++ b/frameworks/multidex/library/test/src/androidx/multidex/ZipUtilTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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 androidx.multidex.ZipUtil.CentralDirectory;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+/**
+ * Tests of ZipUtil class.
+ *
+ * The test assumes that ANDROID_BUILD_TOP environment variable is defined and point to the top of a
+ * built android tree. This is the case when the console used for running the tests is setup for
+ * android tree compilation.
+ */
+public class ZipUtilTest {
+ private static final File zipFile = new File(System.getenv("ANDROID_BUILD_TOP"),
+ "out/target/common/obj/JAVA_LIBRARIES/android-support-multidex_intermediates/javalib.jar");
+ @BeforeClass
+ public static void setupClass() throws ZipException, IOException {
+ // just verify the zip is valid
+ new ZipFile(zipFile).close();
+ }
+
+ @Test
+ public void testCrcDoNotCrash() throws IOException {
+
+ long crc =
+ ZipUtil.getZipCrc(zipFile);
+ System.out.println("crc is " + crc);
+
+ }
+
+ @Test
+ public void testCrcRange() throws IOException {
+ RandomAccessFile raf = new RandomAccessFile(zipFile, "r");
+ CentralDirectory dir = ZipUtil.findCentralDirectory(raf);
+ byte[] dirData = new byte[(int) dir.size];
+ int length = dirData.length;
+ int off = 0;
+ raf.seek(dir.offset);
+ while (length > 0) {
+ int read = raf.read(dirData, off, length);
+ if (length == -1) {
+ throw new EOFException();
+ }
+ length -= read;
+ off += read;
+ }
+ raf.close();
+ ByteBuffer buffer = ByteBuffer.wrap(dirData);
+ Map toCheck = new HashMap();
+ while (buffer.hasRemaining()) {
+ buffer = buffer.slice();
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ ZipEntry entry = ZipEntryReader.readEntry(buffer);
+ toCheck.put(entry.getName(), entry);
+ }
+
+ ZipFile zip = new ZipFile(zipFile);
+ Assert.assertEquals(zip.size(), toCheck.size());
+ Enumeration extends ZipEntry> ref = zip.entries();
+ while (ref.hasMoreElements()) {
+ ZipEntry refEntry = ref.nextElement();
+ ZipEntry checkEntry = toCheck.get(refEntry.getName());
+ Assert.assertNotNull(checkEntry);
+ Assert.assertEquals(refEntry.getName(), checkEntry.getName());
+ Assert.assertEquals(refEntry.getComment(), checkEntry.getComment());
+ Assert.assertEquals(refEntry.getTime(), checkEntry.getTime());
+ Assert.assertEquals(refEntry.getCrc(), checkEntry.getCrc());
+ Assert.assertEquals(refEntry.getCompressedSize(), checkEntry.getCompressedSize());
+ Assert.assertEquals(refEntry.getSize(), checkEntry.getSize());
+ Assert.assertEquals(refEntry.getMethod(), checkEntry.getMethod());
+ Assert.assertArrayEquals(refEntry.getExtra(), checkEntry.getExtra());
+ }
+ zip.close();
+ }
+
+ @Test
+ public void testCrcValue() throws IOException {
+ ZipFile zip = new ZipFile(zipFile);
+ Enumeration extends ZipEntry> ref = zip.entries();
+ byte[] buffer = new byte[0x2000];
+ while (ref.hasMoreElements()) {
+ ZipEntry refEntry = ref.nextElement();
+ if (refEntry.getSize() > 0) {
+ File tmp = File.createTempFile("ZipUtilTest", ".fakezip");
+ InputStream in = zip.getInputStream(refEntry);
+ OutputStream out = new FileOutputStream(tmp);
+ int read = in.read(buffer);
+ while (read != -1) {
+ out.write(buffer, 0, read);
+ read = in.read(buffer);
+ }
+ in.close();
+ out.close();
+ RandomAccessFile raf = new RandomAccessFile(tmp, "r");
+ CentralDirectory dir = new CentralDirectory();
+ dir.offset = 0;
+ dir.size = raf.length();
+ long crc = ZipUtil.computeCrcOfCentralDir(raf, dir);
+ Assert.assertEquals(refEntry.getCrc(), crc);
+ raf.close();
+ tmp.delete();
+ }
+ }
+ zip.close();
+ }
+ @Test
+ public void testInvalidCrcValue() throws IOException {
+ ZipFile zip = new ZipFile(zipFile);
+ Enumeration extends ZipEntry> ref = zip.entries();
+ byte[] buffer = new byte[0x2000];
+ while (ref.hasMoreElements()) {
+ ZipEntry refEntry = ref.nextElement();
+ if (refEntry.getSize() > 0) {
+ File tmp = File.createTempFile("ZipUtilTest", ".fakezip");
+ InputStream in = zip.getInputStream(refEntry);
+ OutputStream out = new FileOutputStream(tmp);
+ int read = in.read(buffer);
+ while (read != -1) {
+ out.write(buffer, 0, read);
+ read = in.read(buffer);
+ }
+ in.close();
+ out.close();
+ RandomAccessFile raf = new RandomAccessFile(tmp, "r");
+ CentralDirectory dir = new CentralDirectory();
+ dir.offset = 0;
+ dir.size = raf.length() - 1;
+ long crc = ZipUtil.computeCrcOfCentralDir(raf, dir);
+ Assert.assertNotEquals(refEntry.getCrc(), crc);
+ raf.close();
+ tmp.delete();
+ }
+ }
+ zip.close();
+ }
+
+}