blob: cde27bffc8c7972230d8c6b7473a5a927aca031d [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 Chua159fd52013-11-28 12:59:37 -080022import java.io.BufferedOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070023import java.io.Closeable;
24import java.io.File;
Yohann Rousseld9eda552013-11-12 18:13:55 +010025import java.io.FileFilter;
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;
30import java.util.ArrayList;
31import java.util.List;
32import java.util.zip.ZipEntry;
Maurice Chuf6d1f232013-11-27 12:56:14 -080033import java.util.zip.ZipException;
Maurice Chu667f9a82013-10-16 13:12:22 -070034import java.util.zip.ZipFile;
35import java.util.zip.ZipOutputStream;
36
37/**
38 * Exposes application secondary dex files as files in the application data
39 * directory.
40 */
41final class MultiDexExtractor {
42
43 private static final String TAG = MultiDex.TAG;
44
45 /**
46 * We look for additional dex files named {@code classes2.dex},
47 * {@code classes3.dex}, etc.
48 */
49 private static final String DEX_PREFIX = "classes";
50 private static final String DEX_SUFFIX = ".dex";
51
52 private static final String EXTRACTED_NAME_EXT = ".classes";
53 private static final String EXTRACTED_SUFFIX = ".zip";
Maurice Chuf6d1f232013-11-27 12:56:14 -080054 private static final int MAX_EXTRACT_ATTEMPTS = 3;
Maurice Chu667f9a82013-10-16 13:12:22 -070055
56 private static final int BUFFER_SIZE = 0x4000;
57
58 /**
59 * Extracts application secondary dexes into files in the application data
60 * directory.
61 *
Maurice Chu667f9a82013-10-16 13:12:22 -070062 * @return a list of files that were created. The list may be empty if there
63 * are no secondary dex files.
64 * @throws IOException if encounters a problem while reading or writing
65 * secondary dex files
66 */
Yohann Roussel52eafa02013-11-21 11:46:53 +010067 static List<File> load(ApplicationInfo applicationInfo, File dexDir)
Maurice Chu667f9a82013-10-16 13:12:22 -070068 throws IOException {
69
Yohann Rousseld9eda552013-11-12 18:13:55 +010070 File sourceApk = new File(applicationInfo.sourceDir);
71 long lastModified = sourceApk.lastModified();
72 String extractedFilePrefix = sourceApk.getName()
Maurice Chu667f9a82013-10-16 13:12:22 -070073 + EXTRACTED_NAME_EXT;
74
Yohann Rousseld9eda552013-11-12 18:13:55 +010075 prepareDexDir(dexDir, extractedFilePrefix, lastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -070076
77 final List<File> files = new ArrayList<File>();
78 ZipFile apk = new ZipFile(applicationInfo.sourceDir);
79 try {
80
81 int secondaryNumber = 2;
82
83 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
84 while (dexFile != null) {
85 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
86 File extractedFile = new File(dexDir, fileName);
87 files.add(extractedFile);
88
89 if (!extractedFile.isFile()) {
Maurice Chuf6d1f232013-11-27 12:56:14 -080090 int numAttempts = 0;
91 boolean isExtractionSuccessful = false;
92 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
93 numAttempts++;
94
95 // Create a zip file (extractedFile) containing only the secondary dex file
96 // (dexFile) from the apk.
97 extract(apk, dexFile, extractedFile, extractedFilePrefix,
98 lastModified);
99
100 // Verify that the extracted file is indeed a zip file.
101 isExtractionSuccessful = verifyZipFile(extractedFile);
102
103 // Log the sha1 of the extracted zip file
104 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
Maurice Chu48cd0402013-11-27 15:59:39 -0800105 " - length " + extractedFile.getAbsolutePath() + ": " +
106 extractedFile.length());
Maurice Chuf6d1f232013-11-27 12:56:14 -0800107 if (!isExtractionSuccessful) {
108 // Delete the extracted file
109 extractedFile.delete();
110 }
111 }
112 if (!isExtractionSuccessful) {
113 throw new IOException("Could not create zip file " +
114 extractedFile.getAbsolutePath() + " for secondary dex (" +
115 secondaryNumber + ")");
116 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700117 }
118 secondaryNumber++;
119 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
120 }
121 } finally {
122 try {
123 apk.close();
124 } catch (IOException e) {
125 Log.w(TAG, "Failed to close resource", e);
126 }
127 }
128
129 return files;
130 }
131
Yohann Rousseld9eda552013-11-12 18:13:55 +0100132 private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
133 final long sourceLastModified) throws IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700134 dexDir.mkdir();
135 if (!dexDir.isDirectory()) {
136 throw new IOException("Failed to create dex directory " + dexDir.getPath());
137 }
138
139 // Clean possible old files
Yohann Rousseld9eda552013-11-12 18:13:55 +0100140 FileFilter filter = new FileFilter() {
141
Maurice Chu667f9a82013-10-16 13:12:22 -0700142 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100143 public boolean accept(File pathname) {
144 return (!pathname.getName().startsWith(extractedFilePrefix))
145 || (pathname.lastModified() < sourceLastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -0700146 }
147 };
148 File[] files = dexDir.listFiles(filter);
149 if (files == null) {
150 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
151 return;
152 }
153 for (File oldFile : files) {
154 if (!oldFile.delete()) {
155 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
156 }
157 }
158 }
159
Yohann Roussel52eafa02013-11-21 11:46:53 +0100160 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
Yohann Rousseld9eda552013-11-12 18:13:55 +0100161 String extractedFilePrefix, long sourceLastModified)
162 throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700163
164 InputStream in = apk.getInputStream(dexFile);
165 ZipOutputStream out = null;
166 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
167 extractTo.getParentFile());
168 Log.i(TAG, "Extracting " + tmp.getPath());
169 try {
Maurice Chua159fd52013-11-28 12:59:37 -0800170 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
Maurice Chu667f9a82013-10-16 13:12:22 -0700171 try {
172 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100173 // keep zip entry time since it is the criteria used by Dalvik
174 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700175 out.putNextEntry(classesDex);
176
177 byte[] buffer = new byte[BUFFER_SIZE];
178 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800179 while (length != -1) {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100180 out.write(buffer, 0, length);
Maurice Chu667f9a82013-10-16 13:12:22 -0700181 length = in.read(buffer);
182 }
Maurice Chua159fd52013-11-28 12:59:37 -0800183 out.closeEntry();
Maurice Chu667f9a82013-10-16 13:12:22 -0700184 } finally {
Maurice Chua0c1a852013-11-27 19:01:53 -0800185 out.close();
Maurice Chu667f9a82013-10-16 13:12:22 -0700186 }
Yohann Rousseld9eda552013-11-12 18:13:55 +0100187 if (!tmp.setLastModified(sourceLastModified)) {
188 Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." +
189 " This may cause problems with later updates of the apk.");
190 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700191 Log.i(TAG, "Renaming to " + extractTo.getPath());
Maurice Chua159fd52013-11-28 12:59:37 -0800192 if (!tmp.renameTo(extractTo)) {
193 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
194 "\" to \"" + extractTo.getAbsolutePath() + "\"");
Maurice Chu667f9a82013-10-16 13:12:22 -0700195 }
196 } finally {
197 closeQuietly(in);
198 tmp.delete(); // return status ignored
199 }
200 }
201
202 /**
Maurice Chuf6d1f232013-11-27 12:56:14 -0800203 * Returns whether the file is a valid zip file.
204 */
205 private static boolean verifyZipFile(File file) {
206 try {
207 ZipFile zipFile = new ZipFile(file);
208 try {
209 zipFile.close();
Maurice Chua0c1a852013-11-27 19:01:53 -0800210 return true;
Maurice Chuf6d1f232013-11-27 12:56:14 -0800211 } catch (IOException e) {
212 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
213 }
Maurice Chuf6d1f232013-11-27 12:56:14 -0800214 } catch (ZipException ex) {
215 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
216 } catch (IOException ex) {
217 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
218 }
219 return false;
220 }
221
222 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700223 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
224 */
225 private static void closeQuietly(Closeable closeable) {
226 try {
227 closeable.close();
228 } catch (IOException e) {
229 Log.w(TAG, "Failed to close resource", e);
230 }
231 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700232}