blob: 32d7ee98f24179e79a20b9b2e771c00724741602 [file] [log] [blame]
Maurice Chu667f9a82013-10-16 13:12:22 -07001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.multidex;
18
Maurice Chu7e267a32014-01-15 19:02:18 -080019import android.content.Context;
20import android.content.SharedPreferences;
Maurice Chu1f8c3492013-11-20 17:14:31 -080021import android.content.pm.ApplicationInfo;
Maurice Chu994fa842014-01-17 10:42:54 -080022import android.os.Build;
Maurice Chu1f8c3492013-11-20 17:14:31 -080023import android.util.Log;
24
Maurice Chua159fd52013-11-28 12:59:37 -080025import java.io.BufferedOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070026import java.io.Closeable;
27import java.io.File;
Yohann Rousseld9eda552013-11-12 18:13:55 +010028import java.io.FileFilter;
Maurice Chu667f9a82013-10-16 13:12:22 -070029import java.io.FileNotFoundException;
30import java.io.FileOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070031import java.io.IOException;
32import java.io.InputStream;
Andrew Johnson048fbf72016-03-09 10:26:37 -050033import java.io.RandomAccessFile;
34import java.nio.channels.FileChannel;
35import java.nio.channels.FileLock;
Maurice Chu667f9a82013-10-16 13:12:22 -070036import java.util.ArrayList;
37import java.util.List;
38import java.util.zip.ZipEntry;
Maurice Chuf6d1f232013-11-27 12:56:14 -080039import java.util.zip.ZipException;
Maurice Chu667f9a82013-10-16 13:12:22 -070040import java.util.zip.ZipFile;
41import java.util.zip.ZipOutputStream;
42
43/**
44 * Exposes application secondary dex files as files in the application data
45 * directory.
46 */
47final class MultiDexExtractor {
48
49 private static final String TAG = MultiDex.TAG;
50
51 /**
52 * We look for additional dex files named {@code classes2.dex},
53 * {@code classes3.dex}, etc.
54 */
55 private static final String DEX_PREFIX = "classes";
56 private static final String DEX_SUFFIX = ".dex";
57
58 private static final String EXTRACTED_NAME_EXT = ".classes";
59 private static final String EXTRACTED_SUFFIX = ".zip";
Maurice Chuf6d1f232013-11-27 12:56:14 -080060 private static final int MAX_EXTRACT_ATTEMPTS = 3;
Maurice Chu667f9a82013-10-16 13:12:22 -070061
Maurice Chu7e267a32014-01-15 19:02:18 -080062 private static final String PREFS_FILE = "multidex.version";
Yohann Roussel602c6ca2014-03-28 17:35:02 +010063 private static final String KEY_TIME_STAMP = "timestamp";
64 private static final String KEY_CRC = "crc";
65 private static final String KEY_DEX_NUMBER = "dex.number";
66
67 /**
68 * Size of reading buffers.
69 */
70 private static final int BUFFER_SIZE = 0x4000;
71 /* Keep value away from 0 because it is a too probable time stamp value */
72 private static final long NO_VALUE = -1L;
Maurice Chu7e267a32014-01-15 19:02:18 -080073
Andrew Johnson048fbf72016-03-09 10:26:37 -050074 private static final String LOCK_FILENAME = "MultiDex.lock";
75
Maurice Chu667f9a82013-10-16 13:12:22 -070076 /**
77 * Extracts application secondary dexes into files in the application data
78 * directory.
79 *
Maurice Chu667f9a82013-10-16 13:12:22 -070080 * @return a list of files that were created. The list may be empty if there
Andrew Johnson048fbf72016-03-09 10:26:37 -050081 * are no secondary dex files. Never return null.
Maurice Chu667f9a82013-10-16 13:12:22 -070082 * @throws IOException if encounters a problem while reading or writing
83 * secondary dex files
84 */
Maurice Chu7e267a32014-01-15 19:02:18 -080085 static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
86 boolean forceReload) throws IOException {
Yohann Roussel602c6ca2014-03-28 17:35:02 +010087 Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
Maurice Chu7e267a32014-01-15 19:02:18 -080088 final File sourceApk = new File(applicationInfo.sourceDir);
Yohann Roussel602c6ca2014-03-28 17:35:02 +010089
Yohann Roussel590a07e2014-07-21 17:47:26 +020090 long currentCrc = getZipCrc(sourceApk);
Yohann Roussel602c6ca2014-03-28 17:35:02 +010091
Andrew Johnson048fbf72016-03-09 10:26:37 -050092 // Validity check and extraction must be done only while the lock file has been taken.
93 File lockFile = new File(dexDir, LOCK_FILENAME);
94 RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
95 FileChannel lockChannel = null;
96 FileLock cacheLock = null;
Yohann Roussel602c6ca2014-03-28 17:35:02 +010097 List<File> files;
Andrew Johnson048fbf72016-03-09 10:26:37 -050098 IOException releaseLockException = null;
99 try {
100 lockChannel = lockRaf.getChannel();
101 Log.i(TAG, "Blocking on lock " + lockFile.getPath());
102 cacheLock = lockChannel.lock();
103 Log.i(TAG, lockFile.getPath() + " locked");
104
105 if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
106 try {
107 files = loadExistingExtractions(context, sourceApk, dexDir);
108 } catch (IOException ioe) {
109 Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
110 + " falling back to fresh extraction", ioe);
111 files = performExtractions(sourceApk, dexDir);
112 putStoredApkInfo(context,
113 getTimeStamp(sourceApk), currentCrc, files.size() + 1);
114
115 }
116 } else {
117 Log.i(TAG, "Detected that extraction must be performed.");
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100118 files = performExtractions(sourceApk, dexDir);
119 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100120 }
Andrew Johnson048fbf72016-03-09 10:26:37 -0500121 } finally {
122 if (cacheLock != null) {
123 try {
124 cacheLock.release();
125 } catch (IOException e) {
126 Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
127 // Exception while releasing the lock is bad, we want to report it, but not at
128 // the price of overriding any already pending exception.
129 releaseLockException = e;
130 }
131 }
132 if (lockChannel != null) {
133 closeQuietly(lockChannel);
134 }
135 closeQuietly(lockRaf);
136 }
137
138 if (releaseLockException != null) {
139 throw releaseLockException;
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100140 }
141
142 Log.i(TAG, "load found " + files.size() + " secondary dex files");
143 return files;
144 }
145
Andrew Johnson048fbf72016-03-09 10:26:37 -0500146 /**
147 * Load previously extracted secondary dex files. Should be called only while owning the lock on
148 * {@link #LOCK_FILENAME}.
149 */
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100150 private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
151 throws IOException {
152 Log.i(TAG, "loading existing secondary dex files");
153
154 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
155 int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
156 final List<File> files = new ArrayList<File>(totalDexNumber);
157
158 for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
159 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
160 File extractedFile = new File(dexDir, fileName);
161 if (extractedFile.isFile()) {
162 files.add(extractedFile);
163 if (!verifyZipFile(extractedFile)) {
164 Log.i(TAG, "Invalid zip file: " + extractedFile);
165 throw new IOException("Invalid ZIP file.");
166 }
167 } else {
168 throw new IOException("Missing extracted secondary dex file '" +
169 extractedFile.getPath() + "'");
170 }
171 }
172
173 return files;
174 }
175
Andrew Johnson048fbf72016-03-09 10:26:37 -0500176
177 /**
178 * Compare current archive and crc with values stored in {@link SharedPreferences}. Should be
179 * called only while owning the lock on {@link #LOCK_FILENAME}.
180 */
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100181 private static boolean isModified(Context context, File archive, long currentCrc) {
182 SharedPreferences prefs = getMultiDexPreferences(context);
183 return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
184 || (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
185 }
186
187 private static long getTimeStamp(File archive) {
188 long timeStamp = archive.lastModified();
189 if (timeStamp == NO_VALUE) {
190 // never return NO_VALUE
191 timeStamp--;
192 }
193 return timeStamp;
194 }
195
196
197 private static long getZipCrc(File archive) throws IOException {
198 long computedValue = ZipUtil.getZipCrc(archive);
199 if (computedValue == NO_VALUE) {
200 // never return NO_VALUE
201 computedValue--;
202 }
203 return computedValue;
204 }
205
206 private static List<File> performExtractions(File sourceApk, File dexDir)
207 throws IOException {
208
Maurice Chu7e267a32014-01-15 19:02:18 -0800209 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
Maurice Chu667f9a82013-10-16 13:12:22 -0700210
Maurice Chu994fa842014-01-17 10:42:54 -0800211 // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
212 // contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
213 // multi-process race conditions can cause a crash loop where one process deletes the zip
214 // while another had created it.
215 prepareDexDir(dexDir, extractedFilePrefix);
Maurice Chu667f9a82013-10-16 13:12:22 -0700216
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100217 List<File> files = new ArrayList<File>();
Maurice Chu7e267a32014-01-15 19:02:18 -0800218
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100219 final ZipFile apk = new ZipFile(sourceApk);
Maurice Chu667f9a82013-10-16 13:12:22 -0700220 try {
221
222 int secondaryNumber = 2;
223
224 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
225 while (dexFile != null) {
226 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
227 File extractedFile = new File(dexDir, fileName);
228 files.add(extractedFile);
229
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100230 Log.i(TAG, "Extraction is needed for file " + extractedFile);
231 int numAttempts = 0;
232 boolean isExtractionSuccessful = false;
233 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
234 numAttempts++;
Maurice Chuf6d1f232013-11-27 12:56:14 -0800235
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100236 // Create a zip file (extractedFile) containing only the secondary dex file
237 // (dexFile) from the apk.
238 extract(apk, dexFile, extractedFile, extractedFilePrefix);
Maurice Chuf6d1f232013-11-27 12:56:14 -0800239
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100240 // Verify that the extracted file is indeed a zip file.
241 isExtractionSuccessful = verifyZipFile(extractedFile);
Maurice Chuf6d1f232013-11-27 12:56:14 -0800242
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100243 // Log the sha1 of the extracted zip file
244 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
245 " - length " + extractedFile.getAbsolutePath() + ": " +
246 extractedFile.length());
247 if (!isExtractionSuccessful) {
248 // Delete the extracted file
249 extractedFile.delete();
250 if (extractedFile.exists()) {
251 Log.w(TAG, "Failed to delete corrupted secondary dex '" +
252 extractedFile.getPath() + "'");
Maurice Chuf6d1f232013-11-27 12:56:14 -0800253 }
254 }
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100255 }
256 if (!isExtractionSuccessful) {
257 throw new IOException("Could not create zip file " +
258 extractedFile.getAbsolutePath() + " for secondary dex (" +
259 secondaryNumber + ")");
Maurice Chu667f9a82013-10-16 13:12:22 -0700260 }
261 secondaryNumber++;
262 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
263 }
264 } finally {
265 try {
266 apk.close();
267 } catch (IOException e) {
268 Log.w(TAG, "Failed to close resource", e);
269 }
270 }
271
272 return files;
273 }
274
Andrew Johnson048fbf72016-03-09 10:26:37 -0500275 /**
276 * Save {@link SharedPreferences}. Should be called only while owning the lock on
277 * {@link #LOCK_FILENAME}.
278 */
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100279 private static void putStoredApkInfo(Context context, long timeStamp, long crc,
280 int totalDexNumber) {
Maurice Chu994fa842014-01-17 10:42:54 -0800281 SharedPreferences prefs = getMultiDexPreferences(context);
Maurice Chu7e267a32014-01-15 19:02:18 -0800282 SharedPreferences.Editor edit = prefs.edit();
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100283 edit.putLong(KEY_TIME_STAMP, timeStamp);
284 edit.putLong(KEY_CRC, crc);
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100285 edit.putInt(KEY_DEX_NUMBER, totalDexNumber);
Andrew Johnson048fbf72016-03-09 10:26:37 -0500286 /* Use commit() and not apply() as advised by the doc because we need synchronous writing of
287 * the editor content and apply is doing an "asynchronous commit to disk".
288 */
289 edit.commit();
Maurice Chu7e267a32014-01-15 19:02:18 -0800290 }
291
Andrew Johnson048fbf72016-03-09 10:26:37 -0500292 /**
293 * Get the MuliDex {@link SharedPreferences} for the current application. Should be called only
294 * while owning the lock on {@link #LOCK_FILENAME}.
295 */
Maurice Chu994fa842014-01-17 10:42:54 -0800296 private static SharedPreferences getMultiDexPreferences(Context context) {
297 return context.getSharedPreferences(PREFS_FILE,
Yohann Roussele99daea2014-09-17 16:49:21 +0200298 Build.VERSION.SDK_INT < 11 /* Build.VERSION_CODES.HONEYCOMB */
Maurice Chu994fa842014-01-17 10:42:54 -0800299 ? Context.MODE_PRIVATE
Yohann Roussele99daea2014-09-17 16:49:21 +0200300 : Context.MODE_PRIVATE | 0x0004 /* Context.MODE_MULTI_PROCESS */);
Maurice Chu994fa842014-01-17 10:42:54 -0800301 }
302
Maurice Chu7e267a32014-01-15 19:02:18 -0800303 /**
Andrew Johnson048fbf72016-03-09 10:26:37 -0500304 * This removes old files.
Maurice Chucc63eda2013-12-02 15:39:59 -0800305 */
Yohann Roussel606af942015-05-12 17:40:52 +0200306 private static void prepareDexDir(File dexDir, final String extractedFilePrefix) {
Yohann Rousseld9eda552013-11-12 18:13:55 +0100307 FileFilter filter = new FileFilter() {
308
Maurice Chu667f9a82013-10-16 13:12:22 -0700309 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100310 public boolean accept(File pathname) {
Andrew Johnson048fbf72016-03-09 10:26:37 -0500311 String name = pathname.getName();
312 return !(name.startsWith(extractedFilePrefix)
313 || name.equals(LOCK_FILENAME));
Maurice Chu667f9a82013-10-16 13:12:22 -0700314 }
315 };
316 File[] files = dexDir.listFiles(filter);
317 if (files == null) {
318 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
319 return;
320 }
321 for (File oldFile : files) {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200322 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
Maurice Chu7e267a32014-01-15 19:02:18 -0800323 oldFile.length());
Maurice Chu667f9a82013-10-16 13:12:22 -0700324 if (!oldFile.delete()) {
325 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
Yohann Roussel88117c32013-11-28 23:22:11 +0100326 } else {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200327 Log.i(TAG, "Deleted old file " + oldFile.getPath());
Maurice Chu667f9a82013-10-16 13:12:22 -0700328 }
329 }
330 }
331
Yohann Roussel52eafa02013-11-21 11:46:53 +0100332 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
Maurice Chu7e267a32014-01-15 19:02:18 -0800333 String extractedFilePrefix) throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700334
335 InputStream in = apk.getInputStream(dexFile);
336 ZipOutputStream out = null;
337 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
338 extractTo.getParentFile());
339 Log.i(TAG, "Extracting " + tmp.getPath());
340 try {
Maurice Chua159fd52013-11-28 12:59:37 -0800341 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
Maurice Chu667f9a82013-10-16 13:12:22 -0700342 try {
343 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100344 // keep zip entry time since it is the criteria used by Dalvik
345 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700346 out.putNextEntry(classesDex);
347
348 byte[] buffer = new byte[BUFFER_SIZE];
349 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800350 while (length != -1) {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100351 out.write(buffer, 0, length);
Maurice Chu667f9a82013-10-16 13:12:22 -0700352 length = in.read(buffer);
353 }
Maurice Chua159fd52013-11-28 12:59:37 -0800354 out.closeEntry();
Maurice Chu667f9a82013-10-16 13:12:22 -0700355 } finally {
Maurice Chua0c1a852013-11-27 19:01:53 -0800356 out.close();
Maurice Chu667f9a82013-10-16 13:12:22 -0700357 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700358 Log.i(TAG, "Renaming to " + extractTo.getPath());
Maurice Chua159fd52013-11-28 12:59:37 -0800359 if (!tmp.renameTo(extractTo)) {
360 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
361 "\" to \"" + extractTo.getAbsolutePath() + "\"");
Maurice Chu667f9a82013-10-16 13:12:22 -0700362 }
363 } finally {
364 closeQuietly(in);
365 tmp.delete(); // return status ignored
366 }
367 }
368
369 /**
Maurice Chuf6d1f232013-11-27 12:56:14 -0800370 * Returns whether the file is a valid zip file.
371 */
Maurice Chucc63eda2013-12-02 15:39:59 -0800372 static boolean verifyZipFile(File file) {
Maurice Chuf6d1f232013-11-27 12:56:14 -0800373 try {
374 ZipFile zipFile = new ZipFile(file);
375 try {
376 zipFile.close();
Maurice Chua0c1a852013-11-27 19:01:53 -0800377 return true;
Maurice Chuf6d1f232013-11-27 12:56:14 -0800378 } catch (IOException e) {
379 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
380 }
Maurice Chuf6d1f232013-11-27 12:56:14 -0800381 } catch (ZipException ex) {
382 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
383 } catch (IOException ex) {
384 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
385 }
386 return false;
387 }
388
389 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700390 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
391 */
392 private static void closeQuietly(Closeable closeable) {
393 try {
394 closeable.close();
395 } catch (IOException e) {
396 Log.w(TAG, "Failed to close resource", e);
397 }
398 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700399}