blob: fd70deedaa707fba5562fd52f0c9b308c5537a2e [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;
Maurice Chu7e267a32014-01-15 19:02:18 -080033import java.lang.reflect.InvocationTargetException;
34import java.lang.reflect.Method;
Maurice Chu667f9a82013-10-16 13:12:22 -070035import java.util.ArrayList;
36import java.util.List;
37import java.util.zip.ZipEntry;
Maurice Chuf6d1f232013-11-27 12:56:14 -080038import java.util.zip.ZipException;
Maurice Chu667f9a82013-10-16 13:12:22 -070039import java.util.zip.ZipFile;
40import java.util.zip.ZipOutputStream;
41
42/**
43 * Exposes application secondary dex files as files in the application data
44 * directory.
45 */
46final class MultiDexExtractor {
47
48 private static final String TAG = MultiDex.TAG;
49
50 /**
51 * We look for additional dex files named {@code classes2.dex},
52 * {@code classes3.dex}, etc.
53 */
54 private static final String DEX_PREFIX = "classes";
55 private static final String DEX_SUFFIX = ".dex";
56
57 private static final String EXTRACTED_NAME_EXT = ".classes";
58 private static final String EXTRACTED_SUFFIX = ".zip";
Maurice Chuf6d1f232013-11-27 12:56:14 -080059 private static final int MAX_EXTRACT_ATTEMPTS = 3;
Maurice Chu667f9a82013-10-16 13:12:22 -070060
Maurice Chu7e267a32014-01-15 19:02:18 -080061 private static final String PREFS_FILE = "multidex.version";
Yohann Roussel602c6ca2014-03-28 17:35:02 +010062 private static final String KEY_TIME_STAMP = "timestamp";
63 private static final String KEY_CRC = "crc";
64 private static final String KEY_DEX_NUMBER = "dex.number";
65
66 /**
67 * Size of reading buffers.
68 */
69 private static final int BUFFER_SIZE = 0x4000;
70 /* Keep value away from 0 because it is a too probable time stamp value */
71 private static final long NO_VALUE = -1L;
Maurice Chu7e267a32014-01-15 19:02:18 -080072
Maurice Chu667f9a82013-10-16 13:12:22 -070073 /**
74 * Extracts application secondary dexes into files in the application data
75 * directory.
76 *
Maurice Chu667f9a82013-10-16 13:12:22 -070077 * @return a list of files that were created. The list may be empty if there
78 * are no secondary dex files.
79 * @throws IOException if encounters a problem while reading or writing
80 * secondary dex files
81 */
Maurice Chu7e267a32014-01-15 19:02:18 -080082 static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
83 boolean forceReload) throws IOException {
Yohann Roussel602c6ca2014-03-28 17:35:02 +010084 Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
Maurice Chu7e267a32014-01-15 19:02:18 -080085 final File sourceApk = new File(applicationInfo.sourceDir);
Yohann Roussel602c6ca2014-03-28 17:35:02 +010086
87 File archive = new File(applicationInfo.sourceDir);
88 long currentCrc = getZipCrc(archive);
89
90 List<File> files;
91 if (!forceReload && !isModified(context, archive, currentCrc)) {
92 try {
93 files = loadExistingExtractions(context, sourceApk, dexDir);
94 } catch (IOException ioe) {
95 Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
96 + " falling back to fresh extraction", ioe);
97 files = performExtractions(sourceApk, dexDir);
98 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
99
100 }
101 } else {
102 Log.i(TAG, "Detected that extraction must be performed.");
103 files = performExtractions(sourceApk, dexDir);
104 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
105 }
106
107 Log.i(TAG, "load found " + files.size() + " secondary dex files");
108 return files;
109 }
110
111 private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
112 throws IOException {
113 Log.i(TAG, "loading existing secondary dex files");
114
115 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
116 int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
117 final List<File> files = new ArrayList<File>(totalDexNumber);
118
119 for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
120 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
121 File extractedFile = new File(dexDir, fileName);
122 if (extractedFile.isFile()) {
123 files.add(extractedFile);
124 if (!verifyZipFile(extractedFile)) {
125 Log.i(TAG, "Invalid zip file: " + extractedFile);
126 throw new IOException("Invalid ZIP file.");
127 }
128 } else {
129 throw new IOException("Missing extracted secondary dex file '" +
130 extractedFile.getPath() + "'");
131 }
132 }
133
134 return files;
135 }
136
137 private static boolean isModified(Context context, File archive, long currentCrc) {
138 SharedPreferences prefs = getMultiDexPreferences(context);
139 return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
140 || (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
141 }
142
143 private static long getTimeStamp(File archive) {
144 long timeStamp = archive.lastModified();
145 if (timeStamp == NO_VALUE) {
146 // never return NO_VALUE
147 timeStamp--;
148 }
149 return timeStamp;
150 }
151
152
153 private static long getZipCrc(File archive) throws IOException {
154 long computedValue = ZipUtil.getZipCrc(archive);
155 if (computedValue == NO_VALUE) {
156 // never return NO_VALUE
157 computedValue--;
158 }
159 return computedValue;
160 }
161
162 private static List<File> performExtractions(File sourceApk, File dexDir)
163 throws IOException {
164
Maurice Chu7e267a32014-01-15 19:02:18 -0800165 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
Maurice Chu667f9a82013-10-16 13:12:22 -0700166
Maurice Chu994fa842014-01-17 10:42:54 -0800167 // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
168 // contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
169 // multi-process race conditions can cause a crash loop where one process deletes the zip
170 // while another had created it.
171 prepareDexDir(dexDir, extractedFilePrefix);
Maurice Chu667f9a82013-10-16 13:12:22 -0700172
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100173 List<File> files = new ArrayList<File>();
Maurice Chu7e267a32014-01-15 19:02:18 -0800174
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100175 final ZipFile apk = new ZipFile(sourceApk);
Maurice Chu667f9a82013-10-16 13:12:22 -0700176 try {
177
178 int secondaryNumber = 2;
179
180 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
181 while (dexFile != null) {
182 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
183 File extractedFile = new File(dexDir, fileName);
184 files.add(extractedFile);
185
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100186 Log.i(TAG, "Extraction is needed for file " + extractedFile);
187 int numAttempts = 0;
188 boolean isExtractionSuccessful = false;
189 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
190 numAttempts++;
Maurice Chuf6d1f232013-11-27 12:56:14 -0800191
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100192 // Create a zip file (extractedFile) containing only the secondary dex file
193 // (dexFile) from the apk.
194 extract(apk, dexFile, extractedFile, extractedFilePrefix);
Maurice Chuf6d1f232013-11-27 12:56:14 -0800195
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100196 // Verify that the extracted file is indeed a zip file.
197 isExtractionSuccessful = verifyZipFile(extractedFile);
Maurice Chuf6d1f232013-11-27 12:56:14 -0800198
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100199 // Log the sha1 of the extracted zip file
200 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
201 " - length " + extractedFile.getAbsolutePath() + ": " +
202 extractedFile.length());
203 if (!isExtractionSuccessful) {
204 // Delete the extracted file
205 extractedFile.delete();
206 if (extractedFile.exists()) {
207 Log.w(TAG, "Failed to delete corrupted secondary dex '" +
208 extractedFile.getPath() + "'");
Maurice Chuf6d1f232013-11-27 12:56:14 -0800209 }
210 }
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100211 }
212 if (!isExtractionSuccessful) {
213 throw new IOException("Could not create zip file " +
214 extractedFile.getAbsolutePath() + " for secondary dex (" +
215 secondaryNumber + ")");
Maurice Chu667f9a82013-10-16 13:12:22 -0700216 }
217 secondaryNumber++;
218 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
219 }
220 } finally {
221 try {
222 apk.close();
223 } catch (IOException e) {
224 Log.w(TAG, "Failed to close resource", e);
225 }
226 }
227
228 return files;
229 }
230
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100231 private static void putStoredApkInfo(Context context, long timeStamp, long crc,
232 int totalDexNumber) {
Maurice Chu994fa842014-01-17 10:42:54 -0800233 SharedPreferences prefs = getMultiDexPreferences(context);
Maurice Chu7e267a32014-01-15 19:02:18 -0800234 SharedPreferences.Editor edit = prefs.edit();
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100235 edit.putLong(KEY_TIME_STAMP, timeStamp);
236 edit.putLong(KEY_CRC, crc);
237 /* SharedPreferences.Editor doc says that apply() and commit() "atomically performs the
238 * requested modifications" it should be OK to rely on saving the dex files number (getting
239 * old number value would go along with old crc and time stamp).
240 */
241 edit.putInt(KEY_DEX_NUMBER, totalDexNumber);
Maurice Chu7e267a32014-01-15 19:02:18 -0800242 apply(edit);
243 }
244
Maurice Chu994fa842014-01-17 10:42:54 -0800245 private static SharedPreferences getMultiDexPreferences(Context context) {
246 return context.getSharedPreferences(PREFS_FILE,
247 Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
248 ? Context.MODE_PRIVATE
249 : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
250 }
251
Maurice Chu7e267a32014-01-15 19:02:18 -0800252 /**
Maurice Chu994fa842014-01-17 10:42:54 -0800253 * This removes any files that do not have the correct prefix.
Maurice Chucc63eda2013-12-02 15:39:59 -0800254 */
Maurice Chu994fa842014-01-17 10:42:54 -0800255 private static void prepareDexDir(File dexDir, final String extractedFilePrefix)
256 throws IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700257 dexDir.mkdir();
258 if (!dexDir.isDirectory()) {
259 throw new IOException("Failed to create dex directory " + dexDir.getPath());
260 }
261
262 // Clean possible old files
Yohann Rousseld9eda552013-11-12 18:13:55 +0100263 FileFilter filter = new FileFilter() {
264
Maurice Chu667f9a82013-10-16 13:12:22 -0700265 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100266 public boolean accept(File pathname) {
Maurice Chu994fa842014-01-17 10:42:54 -0800267 return !pathname.getName().startsWith(extractedFilePrefix);
Maurice Chu667f9a82013-10-16 13:12:22 -0700268 }
269 };
270 File[] files = dexDir.listFiles(filter);
271 if (files == null) {
272 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
273 return;
274 }
275 for (File oldFile : files) {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200276 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
Maurice Chu7e267a32014-01-15 19:02:18 -0800277 oldFile.length());
Maurice Chu667f9a82013-10-16 13:12:22 -0700278 if (!oldFile.delete()) {
279 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
Yohann Roussel88117c32013-11-28 23:22:11 +0100280 } else {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200281 Log.i(TAG, "Deleted old file " + oldFile.getPath());
Maurice Chu667f9a82013-10-16 13:12:22 -0700282 }
283 }
284 }
285
Yohann Roussel52eafa02013-11-21 11:46:53 +0100286 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
Maurice Chu7e267a32014-01-15 19:02:18 -0800287 String extractedFilePrefix) throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700288
289 InputStream in = apk.getInputStream(dexFile);
290 ZipOutputStream out = null;
291 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
292 extractTo.getParentFile());
293 Log.i(TAG, "Extracting " + tmp.getPath());
294 try {
Maurice Chua159fd52013-11-28 12:59:37 -0800295 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
Maurice Chu667f9a82013-10-16 13:12:22 -0700296 try {
297 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100298 // keep zip entry time since it is the criteria used by Dalvik
299 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700300 out.putNextEntry(classesDex);
301
302 byte[] buffer = new byte[BUFFER_SIZE];
303 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800304 while (length != -1) {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100305 out.write(buffer, 0, length);
Maurice Chu667f9a82013-10-16 13:12:22 -0700306 length = in.read(buffer);
307 }
Maurice Chua159fd52013-11-28 12:59:37 -0800308 out.closeEntry();
Maurice Chu667f9a82013-10-16 13:12:22 -0700309 } finally {
Maurice Chua0c1a852013-11-27 19:01:53 -0800310 out.close();
Maurice Chu667f9a82013-10-16 13:12:22 -0700311 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700312 Log.i(TAG, "Renaming to " + extractTo.getPath());
Maurice Chua159fd52013-11-28 12:59:37 -0800313 if (!tmp.renameTo(extractTo)) {
314 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
315 "\" to \"" + extractTo.getAbsolutePath() + "\"");
Maurice Chu667f9a82013-10-16 13:12:22 -0700316 }
317 } finally {
318 closeQuietly(in);
319 tmp.delete(); // return status ignored
320 }
321 }
322
323 /**
Maurice Chuf6d1f232013-11-27 12:56:14 -0800324 * Returns whether the file is a valid zip file.
325 */
Maurice Chucc63eda2013-12-02 15:39:59 -0800326 static boolean verifyZipFile(File file) {
Maurice Chuf6d1f232013-11-27 12:56:14 -0800327 try {
328 ZipFile zipFile = new ZipFile(file);
329 try {
330 zipFile.close();
Maurice Chua0c1a852013-11-27 19:01:53 -0800331 return true;
Maurice Chuf6d1f232013-11-27 12:56:14 -0800332 } catch (IOException e) {
333 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
334 }
Maurice Chuf6d1f232013-11-27 12:56:14 -0800335 } catch (ZipException ex) {
336 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
337 } catch (IOException ex) {
338 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
339 }
340 return false;
341 }
342
343 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700344 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
345 */
346 private static void closeQuietly(Closeable closeable) {
347 try {
348 closeable.close();
349 } catch (IOException e) {
350 Log.w(TAG, "Failed to close resource", e);
351 }
352 }
Maurice Chu7e267a32014-01-15 19:02:18 -0800353
354 // The following is taken from SharedPreferencesCompat to avoid having a dependency of the
355 // multidex support library on another support library.
356 private static Method sApplyMethod; // final
357 static {
358 try {
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100359 Class<?> cls = SharedPreferences.Editor.class;
Maurice Chu7e267a32014-01-15 19:02:18 -0800360 sApplyMethod = cls.getMethod("apply");
361 } catch (NoSuchMethodException unused) {
362 sApplyMethod = null;
363 }
364 }
365
366 private static void apply(SharedPreferences.Editor editor) {
367 if (sApplyMethod != null) {
368 try {
369 sApplyMethod.invoke(editor);
370 return;
371 } catch (InvocationTargetException unused) {
372 // fall through
373 } catch (IllegalAccessException unused) {
374 // fall through
375 }
376 }
377 editor.commit();
378 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700379}