summaryrefslogtreecommitdiff
path: root/library/src/androidx/multidex/MultiDexExtractor.java
diff options
context:
space:
mode:
Diffstat (limited to 'library/src/androidx/multidex/MultiDexExtractor.java')
-rw-r--r--library/src/androidx/multidex/MultiDexExtractor.java427
1 files changed, 427 insertions, 0 deletions
diff --git a/library/src/androidx/multidex/MultiDexExtractor.java b/library/src/androidx/multidex/MultiDexExtractor.java
new file mode 100644
index 0000000..2b96113
--- /dev/null
+++ b/library/src/androidx/multidex/MultiDexExtractor.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.multidex;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.util.Log;
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Exposes application secondary dex files as files in the application data
+ * directory.
+ * {@link MultiDexExtractor} is taking the file lock in the dex dir on creation and release it
+ * during close.
+ */
+final class MultiDexExtractor implements Closeable {
+
+ /**
+ * Zip file containing one secondary dex file.
+ */
+ private static class ExtractedDex extends File {
+ public long crc = NO_VALUE;
+
+ public ExtractedDex(File dexDir, String fileName) {
+ super(dexDir, fileName);
+ }
+ }
+
+ private static final String TAG = MultiDex.TAG;
+
+ /**
+ * We look for additional dex files named {@code classes2.dex},
+ * {@code classes3.dex}, etc.
+ */
+ private static final String DEX_PREFIX = "classes";
+ static final String DEX_SUFFIX = ".dex";
+
+ private static final String EXTRACTED_NAME_EXT = ".classes";
+ static final String EXTRACTED_SUFFIX = ".zip";
+ private static final int MAX_EXTRACT_ATTEMPTS = 3;
+
+ private static final String PREFS_FILE = "multidex.version";
+ private static final String KEY_TIME_STAMP = "timestamp";
+ private static final String KEY_CRC = "crc";
+ private static final String KEY_DEX_NUMBER = "dex.number";
+ private static final String KEY_DEX_CRC = "dex.crc.";
+ private static final String KEY_DEX_TIME = "dex.time.";
+
+ /**
+ * Size of reading buffers.
+ */
+ private static final int BUFFER_SIZE = 0x4000;
+ /* Keep value away from 0 because it is a too probable time stamp value */
+ private static final long NO_VALUE = -1L;
+
+ private static final String LOCK_FILENAME = "MultiDex.lock";
+ private final File sourceApk;
+ private final long sourceCrc;
+ private final File dexDir;
+ private final RandomAccessFile lockRaf;
+ private final FileChannel lockChannel;
+ private final FileLock cacheLock;
+
+ MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
+ Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")");
+ this.sourceApk = sourceApk;
+ this.dexDir = dexDir;
+ sourceCrc = getZipCrc(sourceApk);
+ File lockFile = new File(dexDir, LOCK_FILENAME);
+ lockRaf = new RandomAccessFile(lockFile, "rw");
+ try {
+ lockChannel = lockRaf.getChannel();
+ try {
+ Log.i(TAG, "Blocking on lock " + lockFile.getPath());
+ cacheLock = lockChannel.lock();
+ } catch (IOException | RuntimeException | Error e) {
+ closeQuietly(lockChannel);
+ throw e;
+ }
+ Log.i(TAG, lockFile.getPath() + " locked");
+ } catch (IOException | RuntimeException | Error e) {
+ closeQuietly(lockRaf);
+ throw e;
+ }
+ }
+
+ /**
+ * Extracts application secondary dexes into files in the application data
+ * directory.
+ *
+ * @return a list of files that were created. The list may be empty if there
+ * are no secondary dex files. Never return null.
+ * @throws IOException if encounters a problem while reading or writing
+ * secondary dex files
+ */
+ List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
+ throws IOException {
+ Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +
+ prefsKeyPrefix + ")");
+
+ if (!cacheLock.isValid()) {
+ throw new IllegalStateException("MultiDexExtractor was closed");
+ }
+
+ List<ExtractedDex> files;
+ if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
+ try {
+ files = loadExistingExtractions(context, prefsKeyPrefix);
+ } catch (IOException ioe) {
+ Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ + " falling back to fresh extraction", ioe);
+ files = performExtractions();
+ putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
+ files);
+ }
+ } else {
+ if (forceReload) {
+ Log.i(TAG, "Forced extraction must be performed.");
+ } else {
+ Log.i(TAG, "Detected that extraction must be performed.");
+ }
+ files = performExtractions();
+ putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
+ files);
+ }
+
+ Log.i(TAG, "load found " + files.size() + " secondary dex files");
+ return files;
+ }
+
+ @Override
+ public void close() throws IOException {
+ cacheLock.release();
+ lockChannel.close();
+ lockRaf.close();
+ }
+
+ /**
+ * Load previously extracted secondary dex files. Should be called only while owning the lock on
+ * {@link #LOCK_FILENAME}.
+ */
+ private List<ExtractedDex> loadExistingExtractions(
+ Context context,
+ String prefsKeyPrefix)
+ throws IOException {
+ Log.i(TAG, "loading existing secondary dex files");
+
+ final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
+ SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
+ int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 1);
+ final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1);
+
+ for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
+ String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
+ ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
+ if (extractedFile.isFile()) {
+ extractedFile.crc = getZipCrc(extractedFile);
+ long expectedCrc = multiDexPreferences.getLong(
+ prefsKeyPrefix + KEY_DEX_CRC + secondaryNumber, NO_VALUE);
+ long expectedModTime = multiDexPreferences.getLong(
+ prefsKeyPrefix + KEY_DEX_TIME + secondaryNumber, NO_VALUE);
+ long lastModified = extractedFile.lastModified();
+ if ((expectedModTime != lastModified)
+ || (expectedCrc != extractedFile.crc)) {
+ throw new IOException("Invalid extracted dex: " + extractedFile +
+ " (key \"" + prefsKeyPrefix + "\"), expected modification time: "
+ + expectedModTime + ", modification time: "
+ + lastModified + ", expected crc: "
+ + expectedCrc + ", file crc: " + extractedFile.crc);
+ }
+ files.add(extractedFile);
+ } else {
+ throw new IOException("Missing extracted secondary dex file '" +
+ extractedFile.getPath() + "'");
+ }
+ }
+
+ return files;
+ }
+
+
+ /**
+ * Compare current archive and crc with values stored in {@link SharedPreferences}. Should be
+ * called only while owning the lock on {@link #LOCK_FILENAME}.
+ */
+ private static boolean isModified(Context context, File archive, long currentCrc,
+ String prefsKeyPrefix) {
+ SharedPreferences prefs = getMultiDexPreferences(context);
+ return (prefs.getLong(prefsKeyPrefix + KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
+ || (prefs.getLong(prefsKeyPrefix + KEY_CRC, NO_VALUE) != currentCrc);
+ }
+
+ private static long getTimeStamp(File archive) {
+ long timeStamp = archive.lastModified();
+ if (timeStamp == NO_VALUE) {
+ // never return NO_VALUE
+ timeStamp--;
+ }
+ return timeStamp;
+ }
+
+
+ private static long getZipCrc(File archive) throws IOException {
+ long computedValue = ZipUtil.getZipCrc(archive);
+ if (computedValue == NO_VALUE) {
+ // never return NO_VALUE
+ computedValue--;
+ }
+ return computedValue;
+ }
+
+ private List<ExtractedDex> performExtractions() throws IOException {
+
+ final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
+
+ // It is safe to fully clear the dex dir because we own the file lock so no other process is
+ // extracting or running optimizing dexopt. It may cause crash of already running
+ // applications if for whatever reason we end up extracting again over a valid extraction.
+ clearDexDir();
+
+ List<ExtractedDex> files = new ArrayList<ExtractedDex>();
+
+ final ZipFile apk = new ZipFile(sourceApk);
+ try {
+
+ int secondaryNumber = 2;
+
+ ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
+ while (dexFile != null) {
+ String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
+ ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
+ files.add(extractedFile);
+
+ Log.i(TAG, "Extraction is needed for file " + extractedFile);
+ int numAttempts = 0;
+ boolean isExtractionSuccessful = false;
+ while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
+ numAttempts++;
+
+ // Create a zip file (extractedFile) containing only the secondary dex file
+ // (dexFile) from the apk.
+ extract(apk, dexFile, extractedFile, extractedFilePrefix);
+
+ // Read zip crc of extracted dex
+ try {
+ extractedFile.crc = getZipCrc(extractedFile);
+ isExtractionSuccessful = true;
+ } catch (IOException e) {
+ isExtractionSuccessful = false;
+ Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e);
+ }
+
+ // Log size and crc of the extracted zip file
+ Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
+ + " '" + extractedFile.getAbsolutePath() + "': length "
+ + extractedFile.length() + " - crc: " + extractedFile.crc);
+ if (!isExtractionSuccessful) {
+ // Delete the extracted file
+ extractedFile.delete();
+ if (extractedFile.exists()) {
+ Log.w(TAG, "Failed to delete corrupted secondary dex '" +
+ extractedFile.getPath() + "'");
+ }
+ }
+ }
+ if (!isExtractionSuccessful) {
+ throw new IOException("Could not create zip file " +
+ extractedFile.getAbsolutePath() + " for secondary dex (" +
+ secondaryNumber + ")");
+ }
+ secondaryNumber++;
+ dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
+ }
+ } finally {
+ try {
+ apk.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close resource", e);
+ }
+ }
+
+ return files;
+ }
+
+ /**
+ * Save {@link SharedPreferences}. Should be called only while owning the lock on
+ * {@link #LOCK_FILENAME}.
+ */
+ private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp,
+ long crc, List<ExtractedDex> extractedDexes) {
+ SharedPreferences prefs = getMultiDexPreferences(context);
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putLong(keyPrefix + KEY_TIME_STAMP, timeStamp);
+ edit.putLong(keyPrefix + KEY_CRC, crc);
+ edit.putInt(keyPrefix + KEY_DEX_NUMBER, extractedDexes.size() + 1);
+
+ int extractedDexId = 2;
+ for (ExtractedDex dex : extractedDexes) {
+ edit.putLong(keyPrefix + KEY_DEX_CRC + extractedDexId, dex.crc);
+ edit.putLong(keyPrefix + KEY_DEX_TIME + extractedDexId, dex.lastModified());
+ extractedDexId++;
+ }
+ /* Use commit() and not apply() as advised by the doc because we need synchronous writing of
+ * the editor content and apply is doing an "asynchronous commit to disk".
+ */
+ edit.commit();
+ }
+
+ /**
+ * Get the MuliDex {@link SharedPreferences} for the current application. Should be called only
+ * while owning the lock on {@link #LOCK_FILENAME}.
+ */
+ private static SharedPreferences getMultiDexPreferences(Context context) {
+ return context.getSharedPreferences(PREFS_FILE,
+ Build.VERSION.SDK_INT < 11 /* Build.VERSION_CODES.HONEYCOMB */
+ ? Context.MODE_PRIVATE
+ : Context.MODE_PRIVATE | 0x0004 /* Context.MODE_MULTI_PROCESS */);
+ }
+
+ /**
+ * Clear the dex dir from all files but the lock.
+ */
+ private void clearDexDir() {
+ File[] files = dexDir.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return !pathname.getName().equals(LOCK_FILENAME);
+ }
+ });
+ if (files == null) {
+ Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
+ return;
+ }
+ for (File oldFile : files) {
+ Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
+ oldFile.length());
+ if (!oldFile.delete()) {
+ Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
+ } else {
+ Log.i(TAG, "Deleted old file " + oldFile.getPath());
+ }
+ }
+ }
+
+ private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
+ String extractedFilePrefix) throws IOException, FileNotFoundException {
+
+ InputStream in = apk.getInputStream(dexFile);
+ ZipOutputStream out = null;
+ // Temp files must not start with extractedFilePrefix to get cleaned up in prepareDexDir()
+ File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX,
+ extractTo.getParentFile());
+ Log.i(TAG, "Extracting " + tmp.getPath());
+ try {
+ out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
+ try {
+ ZipEntry classesDex = new ZipEntry("classes.dex");
+ // keep zip entry time since it is the criteria used by Dalvik
+ classesDex.setTime(dexFile.getTime());
+ out.putNextEntry(classesDex);
+
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int length = in.read(buffer);
+ while (length != -1) {
+ out.write(buffer, 0, length);
+ length = in.read(buffer);
+ }
+ out.closeEntry();
+ } finally {
+ out.close();
+ }
+ if (!tmp.setReadOnly()) {
+ throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() +
+ "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
+ }
+ Log.i(TAG, "Renaming to " + extractTo.getPath());
+ if (!tmp.renameTo(extractTo)) {
+ throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
+ "\" to \"" + extractTo.getAbsolutePath() + "\"");
+ }
+ } finally {
+ closeQuietly(in);
+ tmp.delete(); // return status ignored
+ }
+ }
+
+ /**
+ * Closes the given {@code Closeable}. Suppresses any IO exceptions.
+ */
+ private static void closeQuietly(Closeable closeable) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close resource", e);
+ }
+ }
+}