| /* |
| * 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.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); |
| } |
| } |
| } |