blob: 633b22bfd48736ced04ad65a6987382396926f31 [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 Chu667f9a82013-10-16 13:12:22 -070025import java.io.FileNotFoundException;
26import java.io.FileOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070027import java.io.IOException;
28import java.io.InputStream;
Maurice Chua0c1a852013-11-27 19:01:53 -080029import java.nio.channels.FileLock;
Maurice Chu667f9a82013-10-16 13:12:22 -070030import 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 Chua0c1a852013-11-27 19:01:53 -080055 private static final String LOCK_FILENAME = "renamelock";
Maurice Chu667f9a82013-10-16 13:12:22 -070056
57 private static final int BUFFER_SIZE = 0x4000;
58
59 /**
60 * Extracts application secondary dexes into files in the application data
61 * directory.
62 *
Maurice Chu667f9a82013-10-16 13:12:22 -070063 * @return a list of files that were created. The list may be empty if there
64 * are no secondary dex files.
65 * @throws IOException if encounters a problem while reading or writing
66 * secondary dex files
67 */
Yohann Roussel52eafa02013-11-21 11:46:53 +010068 static List<File> load(ApplicationInfo applicationInfo, File dexDir)
Maurice Chu667f9a82013-10-16 13:12:22 -070069 throws IOException {
70
Yohann Rousseld9eda552013-11-12 18:13:55 +010071 File sourceApk = new File(applicationInfo.sourceDir);
72 long lastModified = sourceApk.lastModified();
73 String extractedFilePrefix = sourceApk.getName()
Maurice Chu667f9a82013-10-16 13:12:22 -070074 + EXTRACTED_NAME_EXT;
75
Yohann Rousseld9eda552013-11-12 18:13:55 +010076 prepareDexDir(dexDir, extractedFilePrefix, lastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -070077
78 final List<File> files = new ArrayList<File>();
79 ZipFile apk = new ZipFile(applicationInfo.sourceDir);
80 try {
81
82 int secondaryNumber = 2;
83
84 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
85 while (dexFile != null) {
86 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
87 File extractedFile = new File(dexDir, fileName);
88 files.add(extractedFile);
89
90 if (!extractedFile.isFile()) {
Maurice Chuf6d1f232013-11-27 12:56:14 -080091 int numAttempts = 0;
92 boolean isExtractionSuccessful = false;
93 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
94 numAttempts++;
95
96 // Create a zip file (extractedFile) containing only the secondary dex file
97 // (dexFile) from the apk.
98 extract(apk, dexFile, extractedFile, extractedFilePrefix,
99 lastModified);
100
101 // Verify that the extracted file is indeed a zip file.
102 isExtractionSuccessful = verifyZipFile(extractedFile);
103
104 // Log the sha1 of the extracted zip file
105 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
Maurice Chu48cd0402013-11-27 15:59:39 -0800106 " - length " + extractedFile.getAbsolutePath() + ": " +
107 extractedFile.length());
Maurice Chuf6d1f232013-11-27 12:56:14 -0800108 if (!isExtractionSuccessful) {
109 // Delete the extracted file
110 extractedFile.delete();
111 }
112 }
113 if (!isExtractionSuccessful) {
114 throw new IOException("Could not create zip file " +
115 extractedFile.getAbsolutePath() + " for secondary dex (" +
116 secondaryNumber + ")");
117 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700118 }
119 secondaryNumber++;
120 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
121 }
122 } finally {
123 try {
124 apk.close();
125 } catch (IOException e) {
126 Log.w(TAG, "Failed to close resource", e);
127 }
128 }
129
130 return files;
131 }
132
Yohann Rousseld9eda552013-11-12 18:13:55 +0100133 private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
134 final long sourceLastModified) throws IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700135 dexDir.mkdir();
136 if (!dexDir.isDirectory()) {
137 throw new IOException("Failed to create dex directory " + dexDir.getPath());
138 }
139
140 // Clean possible old files
Yohann Rousseld9eda552013-11-12 18:13:55 +0100141 FileFilter filter = new FileFilter() {
142
Maurice Chu667f9a82013-10-16 13:12:22 -0700143 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100144 public boolean accept(File pathname) {
145 return (!pathname.getName().startsWith(extractedFilePrefix))
146 || (pathname.lastModified() < sourceLastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -0700147 }
148 };
149 File[] files = dexDir.listFiles(filter);
150 if (files == null) {
151 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
152 return;
153 }
154 for (File oldFile : files) {
155 if (!oldFile.delete()) {
156 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
157 }
158 }
159 }
160
Yohann Roussel52eafa02013-11-21 11:46:53 +0100161 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
Yohann Rousseld9eda552013-11-12 18:13:55 +0100162 String extractedFilePrefix, long sourceLastModified)
163 throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700164
165 InputStream in = apk.getInputStream(dexFile);
166 ZipOutputStream out = null;
167 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
168 extractTo.getParentFile());
169 Log.i(TAG, "Extracting " + tmp.getPath());
170 try {
171 out = new ZipOutputStream(new FileOutputStream(tmp));
172 try {
173 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100174 // keep zip entry time since it is the criteria used by Dalvik
175 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700176 out.putNextEntry(classesDex);
177
178 byte[] buffer = new byte[BUFFER_SIZE];
179 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800180 while (length != -1) {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100181 out.write(buffer, 0, length);
Maurice Chu667f9a82013-10-16 13:12:22 -0700182 length = in.read(buffer);
183 }
184 } 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 Chua0c1a852013-11-27 19:01:53 -0800192 File lockFile = new File(extractTo.getParentFile(), LOCK_FILENAME);
193 // Grab the file lock.
194 FileOutputStream lockFileOutputStream = new FileOutputStream(lockFile);
195 FileLock lockFileLock = lockFileOutputStream.getChannel().lock();
196 try {
197 if (!extractTo.exists()) {
198 if (!tmp.renameTo(extractTo)) {
199 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
200 "\" to \"" + extractTo.getAbsolutePath() + "\"");
201 }
202 }
203 } finally {
204 // Release the lock file.
205 lockFileLock.release();
206 lockFileOutputStream.close();
Maurice Chu667f9a82013-10-16 13:12:22 -0700207 }
208 } finally {
209 closeQuietly(in);
210 tmp.delete(); // return status ignored
211 }
212 }
213
214 /**
Maurice Chuf6d1f232013-11-27 12:56:14 -0800215 * Returns whether the file is a valid zip file.
216 */
217 private static boolean verifyZipFile(File file) {
218 try {
219 ZipFile zipFile = new ZipFile(file);
220 try {
221 zipFile.close();
Maurice Chua0c1a852013-11-27 19:01:53 -0800222 return true;
Maurice Chuf6d1f232013-11-27 12:56:14 -0800223 } catch (IOException e) {
224 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
225 }
Maurice Chuf6d1f232013-11-27 12:56:14 -0800226 } catch (ZipException ex) {
227 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
228 } catch (IOException ex) {
229 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
230 }
231 return false;
232 }
233
234 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700235 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
236 */
237 private static void closeQuietly(Closeable closeable) {
238 try {
239 closeable.close();
240 } catch (IOException e) {
241 Log.w(TAG, "Failed to close resource", e);
242 }
243 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700244}