blob: dfbe8f14ca439a743881682662c0d328fc472383 [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;
22import android.util.Log;
23
Maurice Chua159fd52013-11-28 12:59:37 -080024import java.io.BufferedOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070025import java.io.Closeable;
26import java.io.File;
Yohann Rousseld9eda552013-11-12 18:13:55 +010027import java.io.FileFilter;
Maurice Chu667f9a82013-10-16 13:12:22 -070028import java.io.FileNotFoundException;
29import java.io.FileOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070030import java.io.IOException;
31import java.io.InputStream;
Maurice Chu7e267a32014-01-15 19:02:18 -080032import java.lang.reflect.InvocationTargetException;
33import java.lang.reflect.Method;
Maurice Chu667f9a82013-10-16 13:12:22 -070034import java.util.ArrayList;
35import java.util.List;
36import java.util.zip.ZipEntry;
Maurice Chuf6d1f232013-11-27 12:56:14 -080037import java.util.zip.ZipException;
Maurice Chu667f9a82013-10-16 13:12:22 -070038import java.util.zip.ZipFile;
39import java.util.zip.ZipOutputStream;
40
41/**
42 * Exposes application secondary dex files as files in the application data
43 * directory.
44 */
45final class MultiDexExtractor {
46
47 private static final String TAG = MultiDex.TAG;
48
49 /**
50 * We look for additional dex files named {@code classes2.dex},
51 * {@code classes3.dex}, etc.
52 */
53 private static final String DEX_PREFIX = "classes";
54 private static final String DEX_SUFFIX = ".dex";
55
56 private static final String EXTRACTED_NAME_EXT = ".classes";
57 private static final String EXTRACTED_SUFFIX = ".zip";
Maurice Chuf6d1f232013-11-27 12:56:14 -080058 private static final int MAX_EXTRACT_ATTEMPTS = 3;
Maurice Chu667f9a82013-10-16 13:12:22 -070059
60 private static final int BUFFER_SIZE = 0x4000;
61
Maurice Chu7e267a32014-01-15 19:02:18 -080062 private static final String PREFS_FILE = "multidex.version";
63 private static final String KEY_APK_SIZE = "apk_size";
64 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);
80 final long currentApkSize = sourceApk.length();
81 final boolean isNewApk = getStoredApkSize(context) != currentApkSize;
82 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
Maurice Chu667f9a82013-10-16 13:12:22 -070083
Maurice Chu7e267a32014-01-15 19:02:18 -080084 prepareDexDir(dexDir, extractedFilePrefix, isNewApk);
Maurice Chu667f9a82013-10-16 13:12:22 -070085
86 final List<File> files = new ArrayList<File>();
Maurice Chu7e267a32014-01-15 19:02:18 -080087 final ZipFile apk = new ZipFile(applicationInfo.sourceDir);
88
89 // If the CRC of any of the dex files is different than what we have stored or the number of
90 // dex files are different, then force reload everything.
91 ArrayList<Long> dexCrcs = getAllDexCrcs(apk);
92 if (isAnyDexCrcDifferent(context, dexCrcs)) {
93 forceReload = true;
94 }
Maurice Chu667f9a82013-10-16 13:12:22 -070095 try {
96
97 int secondaryNumber = 2;
98
99 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
100 while (dexFile != null) {
101 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
102 File extractedFile = new File(dexDir, fileName);
103 files.add(extractedFile);
104
Yohann Roussel88117c32013-11-28 23:22:11 +0100105 Log.i(TAG, "Need extracted file " + extractedFile);
Maurice Chucc63eda2013-12-02 15:39:59 -0800106 if (forceReload || !extractedFile.isFile()) {
Yohann Roussel88117c32013-11-28 23:22:11 +0100107 Log.i(TAG, "Extraction is needed for file " + extractedFile);
Maurice Chuf6d1f232013-11-27 12:56:14 -0800108 int numAttempts = 0;
109 boolean isExtractionSuccessful = false;
110 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
111 numAttempts++;
112
113 // Create a zip file (extractedFile) containing only the secondary dex file
114 // (dexFile) from the apk.
Maurice Chu7e267a32014-01-15 19:02:18 -0800115 extract(apk, dexFile, extractedFile, extractedFilePrefix);
Maurice Chuf6d1f232013-11-27 12:56:14 -0800116
117 // Verify that the extracted file is indeed a zip file.
118 isExtractionSuccessful = verifyZipFile(extractedFile);
119
120 // Log the sha1 of the extracted zip file
121 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
Maurice Chu48cd0402013-11-27 15:59:39 -0800122 " - length " + extractedFile.getAbsolutePath() + ": " +
123 extractedFile.length());
Maurice Chuf6d1f232013-11-27 12:56:14 -0800124 if (!isExtractionSuccessful) {
125 // Delete the extracted file
126 extractedFile.delete();
127 }
128 }
Maurice Chu7e267a32014-01-15 19:02:18 -0800129 if (isExtractionSuccessful) {
130 // Write the current apk size and the dex crc's into the shared preferences
131 putStoredApkSize(context, currentApkSize);
132 putStoredDexCrcs(context, dexCrcs);
133 } else {
Maurice Chuf6d1f232013-11-27 12:56:14 -0800134 throw new IOException("Could not create zip file " +
135 extractedFile.getAbsolutePath() + " for secondary dex (" +
136 secondaryNumber + ")");
137 }
Yohann Roussel88117c32013-11-28 23:22:11 +0100138 } else {
Maurice Chu7e267a32014-01-15 19:02:18 -0800139 Log.i(TAG, "No extraction needed for " + extractedFile + " of size " +
140 extractedFile.length());
Maurice Chu667f9a82013-10-16 13:12:22 -0700141 }
142 secondaryNumber++;
143 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
144 }
145 } finally {
146 try {
147 apk.close();
148 } catch (IOException e) {
149 Log.w(TAG, "Failed to close resource", e);
150 }
151 }
152
153 return files;
154 }
155
Maurice Chucc63eda2013-12-02 15:39:59 -0800156 /**
Maurice Chu7e267a32014-01-15 19:02:18 -0800157 * Iterate through the expected dex files, classes.dex, classes2.dex, classes3.dex, etc. and
158 * return the CRC of each zip entry in a list.
159 */
160 private static ArrayList<Long> getAllDexCrcs(ZipFile apk) {
161 ArrayList<Long> dexCrcs = new ArrayList<Long>();
162
163 // Add the first one
164 dexCrcs.add(apk.getEntry(DEX_PREFIX + DEX_SUFFIX).getCrc());
165
166 // Get the number of dex files in the apk.
167 int secondaryNumber = 2;
168 while (true) {
169 ZipEntry dexEntry = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
170 if (dexEntry == null) {
171 break;
172 }
173
174 dexCrcs.add(dexEntry.getCrc());
175 secondaryNumber++;
176 }
177 return dexCrcs;
178 }
179
180 /**
181 * Returns true if the number of dex files is different than what is stored in the shared
182 * preferences file or if any dex CRC value is different.
183 */
184 private static boolean isAnyDexCrcDifferent(Context context, ArrayList<Long> dexCrcs) {
185 final ArrayList<Long> storedDexCrcs = getStoredDexCrcs(context);
186
187 if (dexCrcs.size() != storedDexCrcs.size()) {
188 return true;
189 }
190
191 // We know the length of storedDexCrcs and dexCrcs are the same.
192 for (int i = 0; i < storedDexCrcs.size(); i++) {
193 if (storedDexCrcs.get(i) != dexCrcs.get(i)) {
194 return true;
195 }
196 }
197
198 // All the same
199 return false;
200 }
201
202 private static long getStoredApkSize(Context context) {
203 SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0);
204 return prefs.getLong(KEY_APK_SIZE, -1L);
205 }
206
207 private static void putStoredApkSize(Context context, long apkSize) {
208 SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0);
209 SharedPreferences.Editor edit = prefs.edit();
210 edit.putLong(KEY_APK_SIZE, apkSize);
211 apply(edit);
212 }
213
214 private static ArrayList<Long> getStoredDexCrcs(Context context) {
215 SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0);
216 int numDexFiles = prefs.getInt(KEY_NUM_DEX_FILES, 0);
217 ArrayList<Long> dexCrcs = new ArrayList<Long>(numDexFiles);
218 for (int i = 0; i < numDexFiles; i++) {
219 dexCrcs.add(prefs.getLong(makeDexCrcKey(i), 0));
220 }
221 return dexCrcs;
222 }
223
224 private static void putStoredDexCrcs(Context context, ArrayList<Long> dexCrcs) {
225 SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0);
226 SharedPreferences.Editor edit = prefs.edit();
227 edit.putInt(KEY_NUM_DEX_FILES, dexCrcs.size());
228 for (int i = 0; i < dexCrcs.size(); i++) {
229 edit.putLong(makeDexCrcKey(i), dexCrcs.get(i));
230 }
231 apply(edit);
232 }
233
234 private static String makeDexCrcKey(int i) {
235 return KEY_PREFIX_DEX_CRC + Integer.toString(i);
236 }
237
238 /**
239 * This always removes extraneous files. If {@code removeAll} is true, then any existing
240 * zip and dex files are removed from the secondary-dexes directory.
Maurice Chucc63eda2013-12-02 15:39:59 -0800241 */
Yohann Rousseld9eda552013-11-12 18:13:55 +0100242 private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
Maurice Chu7e267a32014-01-15 19:02:18 -0800243 final boolean removeAll) throws IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700244 dexDir.mkdir();
245 if (!dexDir.isDirectory()) {
246 throw new IOException("Failed to create dex directory " + dexDir.getPath());
247 }
248
249 // Clean possible old files
Yohann Rousseld9eda552013-11-12 18:13:55 +0100250 FileFilter filter = new FileFilter() {
251
Maurice Chu667f9a82013-10-16 13:12:22 -0700252 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100253 public boolean accept(File pathname) {
Maurice Chu7e267a32014-01-15 19:02:18 -0800254 return removeAll || !pathname.getName().startsWith(extractedFilePrefix);
Maurice Chu667f9a82013-10-16 13:12:22 -0700255 }
256 };
257 File[] files = dexDir.listFiles(filter);
258 if (files == null) {
259 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
260 return;
261 }
262 for (File oldFile : files) {
Maurice Chu7e267a32014-01-15 19:02:18 -0800263 Log.w(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
264 oldFile.length());
Maurice Chu667f9a82013-10-16 13:12:22 -0700265 if (!oldFile.delete()) {
266 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
Yohann Roussel88117c32013-11-28 23:22:11 +0100267 } else {
268 Log.w(TAG, "Deleted old file " + oldFile.getPath());
Maurice Chu667f9a82013-10-16 13:12:22 -0700269 }
270 }
271 }
272
Yohann Roussel52eafa02013-11-21 11:46:53 +0100273 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
Maurice Chu7e267a32014-01-15 19:02:18 -0800274 String extractedFilePrefix) throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700275
276 InputStream in = apk.getInputStream(dexFile);
277 ZipOutputStream out = null;
278 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
279 extractTo.getParentFile());
280 Log.i(TAG, "Extracting " + tmp.getPath());
281 try {
Maurice Chua159fd52013-11-28 12:59:37 -0800282 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
Maurice Chu667f9a82013-10-16 13:12:22 -0700283 try {
284 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100285 // keep zip entry time since it is the criteria used by Dalvik
286 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700287 out.putNextEntry(classesDex);
288
289 byte[] buffer = new byte[BUFFER_SIZE];
290 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800291 while (length != -1) {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100292 out.write(buffer, 0, length);
Maurice Chu667f9a82013-10-16 13:12:22 -0700293 length = in.read(buffer);
294 }
Maurice Chua159fd52013-11-28 12:59:37 -0800295 out.closeEntry();
Maurice Chu667f9a82013-10-16 13:12:22 -0700296 } finally {
Maurice Chua0c1a852013-11-27 19:01:53 -0800297 out.close();
Maurice Chu667f9a82013-10-16 13:12:22 -0700298 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700299 Log.i(TAG, "Renaming to " + extractTo.getPath());
Maurice Chua159fd52013-11-28 12:59:37 -0800300 if (!tmp.renameTo(extractTo)) {
301 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
302 "\" to \"" + extractTo.getAbsolutePath() + "\"");
Maurice Chu667f9a82013-10-16 13:12:22 -0700303 }
304 } finally {
305 closeQuietly(in);
306 tmp.delete(); // return status ignored
307 }
308 }
309
310 /**
Maurice Chuf6d1f232013-11-27 12:56:14 -0800311 * Returns whether the file is a valid zip file.
312 */
Maurice Chucc63eda2013-12-02 15:39:59 -0800313 static boolean verifyZipFile(File file) {
Maurice Chuf6d1f232013-11-27 12:56:14 -0800314 try {
315 ZipFile zipFile = new ZipFile(file);
316 try {
317 zipFile.close();
Maurice Chua0c1a852013-11-27 19:01:53 -0800318 return true;
Maurice Chuf6d1f232013-11-27 12:56:14 -0800319 } catch (IOException e) {
320 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
321 }
Maurice Chuf6d1f232013-11-27 12:56:14 -0800322 } catch (ZipException ex) {
323 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
324 } catch (IOException ex) {
325 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
326 }
327 return false;
328 }
329
330 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700331 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
332 */
333 private static void closeQuietly(Closeable closeable) {
334 try {
335 closeable.close();
336 } catch (IOException e) {
337 Log.w(TAG, "Failed to close resource", e);
338 }
339 }
Maurice Chu7e267a32014-01-15 19:02:18 -0800340
341 // The following is taken from SharedPreferencesCompat to avoid having a dependency of the
342 // multidex support library on another support library.
343 private static Method sApplyMethod; // final
344 static {
345 try {
346 Class cls = SharedPreferences.Editor.class;
347 sApplyMethod = cls.getMethod("apply");
348 } catch (NoSuchMethodException unused) {
349 sApplyMethod = null;
350 }
351 }
352
353 private static void apply(SharedPreferences.Editor editor) {
354 if (sApplyMethod != null) {
355 try {
356 sApplyMethod.invoke(editor);
357 return;
358 } catch (InvocationTargetException unused) {
359 // fall through
360 } catch (IllegalAccessException unused) {
361 // fall through
362 }
363 }
364 editor.commit();
365 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700366}