blob: ce779d93d5bfc1173faad7372c1d30d2d0ae544f [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
61 private static final int BUFFER_SIZE = 0x4000;
62
Maurice Chu7e267a32014-01-15 19:02:18 -080063 private static final String PREFS_FILE = "multidex.version";
Maurice Chu7e267a32014-01-15 19:02:18 -080064 private static final String KEY_NUM_DEX_FILES = "num_dex";
65 private static final String KEY_PREFIX_DEX_CRC = "crc";
66
Maurice Chu667f9a82013-10-16 13:12:22 -070067 /**
68 * Extracts application secondary dexes into files in the application data
69 * directory.
70 *
Maurice Chu667f9a82013-10-16 13:12:22 -070071 * @return a list of files that were created. The list may be empty if there
72 * are no secondary dex files.
73 * @throws IOException if encounters a problem while reading or writing
74 * secondary dex files
75 */
Maurice Chu7e267a32014-01-15 19:02:18 -080076 static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
77 boolean forceReload) throws IOException {
Maurice Chucc63eda2013-12-02 15:39:59 -080078 Log.i(TAG, "load(" + applicationInfo.sourceDir + ", forceReload=" + forceReload + ")");
Maurice Chu7e267a32014-01-15 19:02:18 -080079 final File sourceApk = new File(applicationInfo.sourceDir);
Maurice Chu7e267a32014-01-15 19:02:18 -080080 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
Maurice Chu667f9a82013-10-16 13:12:22 -070081
Maurice Chu994fa842014-01-17 10:42:54 -080082 // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
83 // contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
84 // multi-process race conditions can cause a crash loop where one process deletes the zip
85 // while another had created it.
86 prepareDexDir(dexDir, extractedFilePrefix);
Maurice Chu667f9a82013-10-16 13:12:22 -070087
88 final List<File> files = new ArrayList<File>();
Maurice Chu7e267a32014-01-15 19:02:18 -080089 final ZipFile apk = new ZipFile(applicationInfo.sourceDir);
90
91 // If the CRC of any of the dex files is different than what we have stored or the number of
92 // dex files are different, then force reload everything.
93 ArrayList<Long> dexCrcs = getAllDexCrcs(apk);
94 if (isAnyDexCrcDifferent(context, dexCrcs)) {
95 forceReload = true;
96 }
Maurice Chu667f9a82013-10-16 13:12:22 -070097 try {
98
99 int secondaryNumber = 2;
100
101 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
102 while (dexFile != null) {
103 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
104 File extractedFile = new File(dexDir, fileName);
105 files.add(extractedFile);
106
Yohann Roussel88117c32013-11-28 23:22:11 +0100107 Log.i(TAG, "Need extracted file " + extractedFile);
Maurice Chucc63eda2013-12-02 15:39:59 -0800108 if (forceReload || !extractedFile.isFile()) {
Yohann Roussel88117c32013-11-28 23:22:11 +0100109 Log.i(TAG, "Extraction is needed for file " + extractedFile);
Maurice Chuf6d1f232013-11-27 12:56:14 -0800110 int numAttempts = 0;
111 boolean isExtractionSuccessful = false;
112 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
113 numAttempts++;
114
115 // Create a zip file (extractedFile) containing only the secondary dex file
116 // (dexFile) from the apk.
Maurice Chu7e267a32014-01-15 19:02:18 -0800117 extract(apk, dexFile, extractedFile, extractedFilePrefix);
Maurice Chuf6d1f232013-11-27 12:56:14 -0800118
119 // Verify that the extracted file is indeed a zip file.
120 isExtractionSuccessful = verifyZipFile(extractedFile);
121
122 // Log the sha1 of the extracted zip file
123 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
Maurice Chu48cd0402013-11-27 15:59:39 -0800124 " - length " + extractedFile.getAbsolutePath() + ": " +
125 extractedFile.length());
Maurice Chuf6d1f232013-11-27 12:56:14 -0800126 if (!isExtractionSuccessful) {
127 // Delete the extracted file
128 extractedFile.delete();
129 }
130 }
Maurice Chu7e267a32014-01-15 19:02:18 -0800131 if (isExtractionSuccessful) {
Maurice Chu994fa842014-01-17 10:42:54 -0800132 // Write the dex crc's into the shared preferences
Maurice Chu7e267a32014-01-15 19:02:18 -0800133 putStoredDexCrcs(context, dexCrcs);
134 } else {
Maurice Chuf6d1f232013-11-27 12:56:14 -0800135 throw new IOException("Could not create zip file " +
136 extractedFile.getAbsolutePath() + " for secondary dex (" +
137 secondaryNumber + ")");
138 }
Yohann Roussel88117c32013-11-28 23:22:11 +0100139 } else {
Maurice Chu7e267a32014-01-15 19:02:18 -0800140 Log.i(TAG, "No extraction needed for " + extractedFile + " of size " +
141 extractedFile.length());
Maurice Chu667f9a82013-10-16 13:12:22 -0700142 }
143 secondaryNumber++;
144 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
145 }
146 } finally {
147 try {
148 apk.close();
149 } catch (IOException e) {
150 Log.w(TAG, "Failed to close resource", e);
151 }
152 }
153
154 return files;
155 }
156
Maurice Chucc63eda2013-12-02 15:39:59 -0800157 /**
Maurice Chu7e267a32014-01-15 19:02:18 -0800158 * Iterate through the expected dex files, classes.dex, classes2.dex, classes3.dex, etc. and
159 * return the CRC of each zip entry in a list.
160 */
161 private static ArrayList<Long> getAllDexCrcs(ZipFile apk) {
162 ArrayList<Long> dexCrcs = new ArrayList<Long>();
163
164 // Add the first one
165 dexCrcs.add(apk.getEntry(DEX_PREFIX + DEX_SUFFIX).getCrc());
166
167 // Get the number of dex files in the apk.
168 int secondaryNumber = 2;
169 while (true) {
170 ZipEntry dexEntry = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
171 if (dexEntry == null) {
172 break;
173 }
174
175 dexCrcs.add(dexEntry.getCrc());
176 secondaryNumber++;
177 }
178 return dexCrcs;
179 }
180
181 /**
182 * Returns true if the number of dex files is different than what is stored in the shared
183 * preferences file or if any dex CRC value is different.
184 */
185 private static boolean isAnyDexCrcDifferent(Context context, ArrayList<Long> dexCrcs) {
186 final ArrayList<Long> storedDexCrcs = getStoredDexCrcs(context);
187
188 if (dexCrcs.size() != storedDexCrcs.size()) {
189 return true;
190 }
191
192 // We know the length of storedDexCrcs and dexCrcs are the same.
193 for (int i = 0; i < storedDexCrcs.size(); i++) {
194 if (storedDexCrcs.get(i) != dexCrcs.get(i)) {
195 return true;
196 }
197 }
198
199 // All the same
200 return false;
201 }
202
Maurice Chu7e267a32014-01-15 19:02:18 -0800203 private static ArrayList<Long> getStoredDexCrcs(Context context) {
Maurice Chu994fa842014-01-17 10:42:54 -0800204 SharedPreferences prefs = getMultiDexPreferences(context);
Maurice Chu7e267a32014-01-15 19:02:18 -0800205 int numDexFiles = prefs.getInt(KEY_NUM_DEX_FILES, 0);
206 ArrayList<Long> dexCrcs = new ArrayList<Long>(numDexFiles);
207 for (int i = 0; i < numDexFiles; i++) {
208 dexCrcs.add(prefs.getLong(makeDexCrcKey(i), 0));
209 }
210 return dexCrcs;
211 }
212
213 private static void putStoredDexCrcs(Context context, ArrayList<Long> dexCrcs) {
Maurice Chu994fa842014-01-17 10:42:54 -0800214 SharedPreferences prefs = getMultiDexPreferences(context);
Maurice Chu7e267a32014-01-15 19:02:18 -0800215 SharedPreferences.Editor edit = prefs.edit();
216 edit.putInt(KEY_NUM_DEX_FILES, dexCrcs.size());
217 for (int i = 0; i < dexCrcs.size(); i++) {
218 edit.putLong(makeDexCrcKey(i), dexCrcs.get(i));
219 }
220 apply(edit);
221 }
222
Maurice Chu994fa842014-01-17 10:42:54 -0800223 private static SharedPreferences getMultiDexPreferences(Context context) {
224 return context.getSharedPreferences(PREFS_FILE,
225 Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
226 ? Context.MODE_PRIVATE
227 : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
228 }
229
Maurice Chu7e267a32014-01-15 19:02:18 -0800230 private static String makeDexCrcKey(int i) {
231 return KEY_PREFIX_DEX_CRC + Integer.toString(i);
232 }
233
234 /**
Maurice Chu994fa842014-01-17 10:42:54 -0800235 * This removes any files that do not have the correct prefix.
Maurice Chucc63eda2013-12-02 15:39:59 -0800236 */
Maurice Chu994fa842014-01-17 10:42:54 -0800237 private static void prepareDexDir(File dexDir, final String extractedFilePrefix)
238 throws IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700239 dexDir.mkdir();
240 if (!dexDir.isDirectory()) {
241 throw new IOException("Failed to create dex directory " + dexDir.getPath());
242 }
243
244 // Clean possible old files
Yohann Rousseld9eda552013-11-12 18:13:55 +0100245 FileFilter filter = new FileFilter() {
246
Maurice Chu667f9a82013-10-16 13:12:22 -0700247 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100248 public boolean accept(File pathname) {
Maurice Chu994fa842014-01-17 10:42:54 -0800249 return !pathname.getName().startsWith(extractedFilePrefix);
Maurice Chu667f9a82013-10-16 13:12:22 -0700250 }
251 };
252 File[] files = dexDir.listFiles(filter);
253 if (files == null) {
254 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
255 return;
256 }
257 for (File oldFile : files) {
Maurice Chu7e267a32014-01-15 19:02:18 -0800258 Log.w(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
259 oldFile.length());
Maurice Chu667f9a82013-10-16 13:12:22 -0700260 if (!oldFile.delete()) {
261 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
Yohann Roussel88117c32013-11-28 23:22:11 +0100262 } else {
263 Log.w(TAG, "Deleted old file " + oldFile.getPath());
Maurice Chu667f9a82013-10-16 13:12:22 -0700264 }
265 }
266 }
267
Yohann Roussel52eafa02013-11-21 11:46:53 +0100268 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
Maurice Chu7e267a32014-01-15 19:02:18 -0800269 String extractedFilePrefix) throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700270
271 InputStream in = apk.getInputStream(dexFile);
272 ZipOutputStream out = null;
273 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
274 extractTo.getParentFile());
275 Log.i(TAG, "Extracting " + tmp.getPath());
276 try {
Maurice Chua159fd52013-11-28 12:59:37 -0800277 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
Maurice Chu667f9a82013-10-16 13:12:22 -0700278 try {
279 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100280 // keep zip entry time since it is the criteria used by Dalvik
281 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700282 out.putNextEntry(classesDex);
283
284 byte[] buffer = new byte[BUFFER_SIZE];
285 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800286 while (length != -1) {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100287 out.write(buffer, 0, length);
Maurice Chu667f9a82013-10-16 13:12:22 -0700288 length = in.read(buffer);
289 }
Maurice Chua159fd52013-11-28 12:59:37 -0800290 out.closeEntry();
Maurice Chu667f9a82013-10-16 13:12:22 -0700291 } finally {
Maurice Chua0c1a852013-11-27 19:01:53 -0800292 out.close();
Maurice Chu667f9a82013-10-16 13:12:22 -0700293 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700294 Log.i(TAG, "Renaming to " + extractTo.getPath());
Maurice Chua159fd52013-11-28 12:59:37 -0800295 if (!tmp.renameTo(extractTo)) {
296 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
297 "\" to \"" + extractTo.getAbsolutePath() + "\"");
Maurice Chu667f9a82013-10-16 13:12:22 -0700298 }
299 } finally {
300 closeQuietly(in);
301 tmp.delete(); // return status ignored
302 }
303 }
304
305 /**
Maurice Chuf6d1f232013-11-27 12:56:14 -0800306 * Returns whether the file is a valid zip file.
307 */
Maurice Chucc63eda2013-12-02 15:39:59 -0800308 static boolean verifyZipFile(File file) {
Maurice Chuf6d1f232013-11-27 12:56:14 -0800309 try {
310 ZipFile zipFile = new ZipFile(file);
311 try {
312 zipFile.close();
Maurice Chua0c1a852013-11-27 19:01:53 -0800313 return true;
Maurice Chuf6d1f232013-11-27 12:56:14 -0800314 } catch (IOException e) {
315 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
316 }
Maurice Chuf6d1f232013-11-27 12:56:14 -0800317 } catch (ZipException ex) {
318 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
319 } catch (IOException ex) {
320 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
321 }
322 return false;
323 }
324
325 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700326 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
327 */
328 private static void closeQuietly(Closeable closeable) {
329 try {
330 closeable.close();
331 } catch (IOException e) {
332 Log.w(TAG, "Failed to close resource", e);
333 }
334 }
Maurice Chu7e267a32014-01-15 19:02:18 -0800335
336 // The following is taken from SharedPreferencesCompat to avoid having a dependency of the
337 // multidex support library on another support library.
338 private static Method sApplyMethod; // final
339 static {
340 try {
341 Class cls = SharedPreferences.Editor.class;
342 sApplyMethod = cls.getMethod("apply");
343 } catch (NoSuchMethodException unused) {
344 sApplyMethod = null;
345 }
346 }
347
348 private static void apply(SharedPreferences.Editor editor) {
349 if (sApplyMethod != null) {
350 try {
351 sApplyMethod.invoke(editor);
352 return;
353 } catch (InvocationTargetException unused) {
354 // fall through
355 } catch (IllegalAccessException unused) {
356 // fall through
357 }
358 }
359 editor.commit();
360 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700361}