blob: 2d7402adee1d57dbd8fb8326d1f5b5345b770bcc [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.multidex;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
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.
*/
final class MultiDexExtractor {
/**
* 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";
private static final String DEX_SUFFIX = ".dex";
private static final String EXTRACTED_NAME_EXT = ".classes";
private static final String EXTRACTED_SUFFIX = ".zip";
private static final int 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";
/**
* 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
*/
static List<? extends File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
boolean forceReload) throws IOException {
Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
final File sourceApk = new File(applicationInfo.sourceDir);
long currentCrc = getZipCrc(sourceApk);
// Validity check and extraction must be done only while the lock file has been taken.
File lockFile = new File(dexDir, LOCK_FILENAME);
RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
FileChannel lockChannel = null;
FileLock cacheLock = null;
List<ExtractedDex> files;
IOException releaseLockException = null;
try {
lockChannel = lockRaf.getChannel();
Log.i(TAG, "Blocking on lock " + lockFile.getPath());
cacheLock = lockChannel.lock();
Log.i(TAG, lockFile.getPath() + " locked");
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException ioe) {
Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ " falling back to fresh extraction", ioe);
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files);
}
} else {
Log.i(TAG, "Detected that extraction must be performed.");
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files);
}
} finally {
if (cacheLock != null) {
try {
cacheLock.release();
} catch (IOException e) {
Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
// Exception while releasing the lock is bad, we want to report it, but not at
// the price of overriding any already pending exception.
releaseLockException = e;
}
}
if (lockChannel != null) {
closeQuietly(lockChannel);
}
closeQuietly(lockRaf);
}
if (releaseLockException != null) {
throw releaseLockException;
}
Log.i(TAG, "load found " + files.size() + " secondary dex files");
return files;
}
/**
* Load previously extracted secondary dex files. Should be called only while owning the lock on
* {@link #LOCK_FILENAME}.
*/
private static List<ExtractedDex> loadExistingExtractions(
Context context, File sourceApk, File dexDir)
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(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(KEY_DEX_CRC + secondaryNumber, NO_VALUE);
long expectedModTime =
multiDexPreferences.getLong(KEY_DEX_TIME + secondaryNumber, NO_VALUE);
long lastModified = extractedFile.lastModified();
if ((expectedModTime != lastModified)
|| (expectedCrc != extractedFile.crc)) {
throw new IOException("Invalid extracted dex: " + extractedFile +
", 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) {
SharedPreferences prefs = getMultiDexPreferences(context);
return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
|| (prefs.getLong(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 static List<ExtractedDex> performExtractions(File sourceApk, File dexDir)
throws IOException {
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
// contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
// multi-process race conditions can cause a crash loop where one process deletes the zip
// while another had created it.
prepareDexDir(dexDir, extractedFilePrefix);
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") +
" - length " + extractedFile.getAbsolutePath() + ": " +
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, long timeStamp, long crc,
List<ExtractedDex> extractedDexes) {
SharedPreferences prefs = getMultiDexPreferences(context);
SharedPreferences.Editor edit = prefs.edit();
edit.putLong(KEY_TIME_STAMP, timeStamp);
edit.putLong(KEY_CRC, crc);
edit.putInt(KEY_DEX_NUMBER, extractedDexes.size() + 1);
int extractedDexId = 2;
for (ExtractedDex dex : extractedDexes) {
edit.putLong(KEY_DEX_CRC + extractedDexId, dex.crc);
edit.putLong(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 */);
}
/**
* This removes old files.
*/
private static void prepareDexDir(File dexDir, final String extractedFilePrefix) {
FileFilter filter = new FileFilter() {
@Override
public boolean accept(File pathname) {
String name = pathname.getName();
return !(name.startsWith(extractedFilePrefix)
|| name.equals(LOCK_FILENAME));
}
};
File[] files = dexDir.listFiles(filter);
if (files == null) {
Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
return;
}
for (File oldFile : files) {
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);
}
}
}