blob: df82e0810ab15c7921d3cba919e7111f02198cbe [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 Chu1f8c3492013-11-20 17:14:31 -080019import android.content.pm.ApplicationInfo;
20import android.util.Log;
21
Maurice Chu667f9a82013-10-16 13:12:22 -070022import java.io.Closeable;
23import java.io.File;
Yohann Rousseld9eda552013-11-12 18:13:55 +010024import java.io.FileFilter;
Maurice Chuf6d1f232013-11-27 12:56:14 -080025import java.io.FileInputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070026import java.io.FileNotFoundException;
27import java.io.FileOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070028import java.io.IOException;
29import java.io.InputStream;
Maurice Chuf6d1f232013-11-27 12:56:14 -080030import java.security.MessageDigest;
31import java.security.NoSuchAlgorithmException;
Maurice Chu667f9a82013-10-16 13:12:22 -070032import java.util.ArrayList;
33import java.util.List;
34import java.util.zip.ZipEntry;
Maurice Chuf6d1f232013-11-27 12:56:14 -080035import java.util.zip.ZipException;
Maurice Chu667f9a82013-10-16 13:12:22 -070036import java.util.zip.ZipFile;
37import java.util.zip.ZipOutputStream;
38
39/**
40 * Exposes application secondary dex files as files in the application data
41 * directory.
42 */
43final class MultiDexExtractor {
44
45 private static final String TAG = MultiDex.TAG;
46
47 /**
48 * We look for additional dex files named {@code classes2.dex},
49 * {@code classes3.dex}, etc.
50 */
51 private static final String DEX_PREFIX = "classes";
52 private static final String DEX_SUFFIX = ".dex";
53
54 private static final String EXTRACTED_NAME_EXT = ".classes";
55 private static final String EXTRACTED_SUFFIX = ".zip";
Maurice Chuf6d1f232013-11-27 12:56:14 -080056 private static final int MAX_EXTRACT_ATTEMPTS = 3;
57 private static final int MAX_ATTEMPTS_NO_SUCH_ALGORITHM = 2;
58
59
60 private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
61 'A', 'B', 'C', 'D', 'E', 'F' };
Maurice Chu667f9a82013-10-16 13:12:22 -070062
63 private static final int BUFFER_SIZE = 0x4000;
64
65 /**
66 * Extracts application secondary dexes into files in the application data
67 * directory.
68 *
Maurice Chu667f9a82013-10-16 13:12:22 -070069 * @return a list of files that were created. The list may be empty if there
70 * are no secondary dex files.
71 * @throws IOException if encounters a problem while reading or writing
72 * secondary dex files
73 */
Yohann Roussel52eafa02013-11-21 11:46:53 +010074 static List<File> load(ApplicationInfo applicationInfo, File dexDir)
Maurice Chu667f9a82013-10-16 13:12:22 -070075 throws IOException {
76
Yohann Rousseld9eda552013-11-12 18:13:55 +010077 File sourceApk = new File(applicationInfo.sourceDir);
78 long lastModified = sourceApk.lastModified();
79 String extractedFilePrefix = sourceApk.getName()
Maurice Chu667f9a82013-10-16 13:12:22 -070080 + EXTRACTED_NAME_EXT;
81
Yohann Rousseld9eda552013-11-12 18:13:55 +010082 prepareDexDir(dexDir, extractedFilePrefix, lastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -070083
84 final List<File> files = new ArrayList<File>();
85 ZipFile apk = new ZipFile(applicationInfo.sourceDir);
86 try {
87
88 int secondaryNumber = 2;
89
90 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
91 while (dexFile != null) {
92 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
93 File extractedFile = new File(dexDir, fileName);
94 files.add(extractedFile);
95
96 if (!extractedFile.isFile()) {
Maurice Chuf6d1f232013-11-27 12:56:14 -080097 int numAttempts = 0;
98 boolean isExtractionSuccessful = false;
99 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
100 numAttempts++;
101
102 // Create a zip file (extractedFile) containing only the secondary dex file
103 // (dexFile) from the apk.
104 extract(apk, dexFile, extractedFile, extractedFilePrefix,
105 lastModified);
106
107 // Verify that the extracted file is indeed a zip file.
108 isExtractionSuccessful = verifyZipFile(extractedFile);
109
110 // Log the sha1 of the extracted zip file
111 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
112 " - SHA1 of " + extractedFile.getAbsolutePath() + ": " +
113 computeSha1Digest(extractedFile));
114 if (!isExtractionSuccessful) {
115 // Delete the extracted file
116 extractedFile.delete();
117 }
118 }
119 if (!isExtractionSuccessful) {
120 throw new IOException("Could not create zip file " +
121 extractedFile.getAbsolutePath() + " for secondary dex (" +
122 secondaryNumber + ")");
123 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700124 }
125 secondaryNumber++;
126 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
127 }
128 } finally {
129 try {
130 apk.close();
131 } catch (IOException e) {
132 Log.w(TAG, "Failed to close resource", e);
133 }
134 }
135
136 return files;
137 }
138
Yohann Rousseld9eda552013-11-12 18:13:55 +0100139 private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
140 final long sourceLastModified) throws IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700141 dexDir.mkdir();
142 if (!dexDir.isDirectory()) {
143 throw new IOException("Failed to create dex directory " + dexDir.getPath());
144 }
145
146 // Clean possible old files
Yohann Rousseld9eda552013-11-12 18:13:55 +0100147 FileFilter filter = new FileFilter() {
148
Maurice Chu667f9a82013-10-16 13:12:22 -0700149 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100150 public boolean accept(File pathname) {
151 return (!pathname.getName().startsWith(extractedFilePrefix))
152 || (pathname.lastModified() < sourceLastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -0700153 }
154 };
155 File[] files = dexDir.listFiles(filter);
156 if (files == null) {
157 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
158 return;
159 }
160 for (File oldFile : files) {
161 if (!oldFile.delete()) {
162 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
163 }
164 }
165 }
166
Yohann Roussel52eafa02013-11-21 11:46:53 +0100167 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
Yohann Rousseld9eda552013-11-12 18:13:55 +0100168 String extractedFilePrefix, long sourceLastModified)
169 throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700170
171 InputStream in = apk.getInputStream(dexFile);
172 ZipOutputStream out = null;
173 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
174 extractTo.getParentFile());
175 Log.i(TAG, "Extracting " + tmp.getPath());
176 try {
177 out = new ZipOutputStream(new FileOutputStream(tmp));
178 try {
179 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100180 // keep zip entry time since it is the criteria used by Dalvik
181 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700182 out.putNextEntry(classesDex);
183
184 byte[] buffer = new byte[BUFFER_SIZE];
185 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800186 while (length != -1) {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100187 out.write(buffer, 0, length);
Maurice Chu667f9a82013-10-16 13:12:22 -0700188 length = in.read(buffer);
189 }
190 } finally {
191 closeQuietly(out);
192 }
Yohann Rousseld9eda552013-11-12 18:13:55 +0100193 if (!tmp.setLastModified(sourceLastModified)) {
194 Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." +
195 " This may cause problems with later updates of the apk.");
196 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700197 Log.i(TAG, "Renaming to " + extractTo.getPath());
198 if (!tmp.renameTo(extractTo)) {
199 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" +
200 extractTo.getAbsolutePath() + "\"");
201 }
202 } finally {
203 closeQuietly(in);
204 tmp.delete(); // return status ignored
205 }
206 }
207
208 /**
Maurice Chuf6d1f232013-11-27 12:56:14 -0800209 * Returns whether the file is a valid zip file.
210 */
211 private static boolean verifyZipFile(File file) {
212 try {
213 ZipFile zipFile = new ZipFile(file);
214 try {
215 zipFile.close();
216 } catch (IOException e) {
217 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
218 }
219 return true;
220 } catch (ZipException ex) {
221 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
222 } catch (IOException ex) {
223 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
224 }
225 return false;
226 }
227
228 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700229 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
230 */
231 private static void closeQuietly(Closeable closeable) {
232 try {
233 closeable.close();
234 } catch (IOException e) {
235 Log.w(TAG, "Failed to close resource", e);
236 }
237 }
Maurice Chuf6d1f232013-11-27 12:56:14 -0800238
239 private static synchronized String computeSha1Digest(File file) {
240 MessageDigest messageDigest = getMessageDigest("SHA1");
241 if (messageDigest == null) {
242 return "";
243 }
244 FileInputStream in = null;
245 try {
246 in = new FileInputStream(file);
247 byte[] bytes = new byte[8192];
248 int byteCount;
249 while ((byteCount = in.read(bytes)) != -1) {
250 messageDigest.update(bytes, 0, byteCount);
251 }
252 return toHex(messageDigest.digest(), false /* zeroTerminated */)
253 .toLowerCase();
254 } catch (IOException e) {
255 return "";
256 } finally {
257 if (in != null) {
258 closeQuietly(in);
259 }
260 }
261 }
262
263 /**
264 * Encodes a byte array as a hexadecimal representation of bytes.
265 */
266 private static String toHex(byte[] in, boolean zeroTerminated) {
267 int length = in.length;
268 StringBuilder out = new StringBuilder(length * 2);
269 for (int i = 0; i < length; i++) {
270 if (zeroTerminated && i == length - 1 && (in[i] & 0xff) == 0) {
271 break;
272 }
273 out.append(HEX_DIGITS[(in[i] & 0xf0) >>> 4]);
274 out.append(HEX_DIGITS[in[i] & 0x0f]);
275 }
276 return out.toString();
277 }
278
279 /**
280 * Retrieves the message digest instance for a given hash algorithm. Makes
281 * {@link #MAX_ATTEMPTS_NO_SUCH_ALGORITHM} to successfully retrieve the
282 * MessageDigest or will return null.
283 */
284 private static MessageDigest getMessageDigest(String hashAlgorithm) {
285 for (int i = 0; i < MAX_ATTEMPTS_NO_SUCH_ALGORITHM; i++) {
286 try {
287 MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm);
288 if (messageDigest != null) {
289 return messageDigest;
290 }
291 } catch (NoSuchAlgorithmException e) {
292 // try again - this is needed due to a bug in MessageDigest that can have corrupted
293 // internal state.
294 continue;
295 }
296 }
297 return null;
298 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700299}