blob: b6988a480a285256f0edb76cfe61859fec7c190d [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.Context;
20import android.content.pm.ApplicationInfo;
21import android.util.Log;
22
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;
33import java.util.zip.ZipFile;
34import java.util.zip.ZipOutputStream;
35
36/**
37 * Exposes application secondary dex files as files in the application data
38 * directory.
39 */
40final class MultiDexExtractor {
41
42 private static final String TAG = MultiDex.TAG;
43
44 /**
45 * We look for additional dex files named {@code classes2.dex},
46 * {@code classes3.dex}, etc.
47 */
48 private static final String DEX_PREFIX = "classes";
49 private static final String DEX_SUFFIX = ".dex";
50
51 private static final String EXTRACTED_NAME_EXT = ".classes";
52 private static final String EXTRACTED_SUFFIX = ".zip";
53
54 private static final int BUFFER_SIZE = 0x4000;
55
56 /**
57 * Extracts application secondary dexes into files in the application data
58 * directory.
59 *
60 * @param dexDir
61 *
62 * @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 */
67 static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir)
68 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()) {
Yohann Rousseld9eda552013-11-12 18:13:55 +010090 extract(context, apk, dexFile, extractedFile, extractedFilePrefix,
91 lastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -070092 }
93 secondaryNumber++;
94 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
95 }
96 } finally {
97 try {
98 apk.close();
99 } catch (IOException e) {
100 Log.w(TAG, "Failed to close resource", e);
101 }
102 }
103
104 return files;
105 }
106
Yohann Rousseld9eda552013-11-12 18:13:55 +0100107 private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
108 final long sourceLastModified) throws IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700109 dexDir.mkdir();
110 if (!dexDir.isDirectory()) {
111 throw new IOException("Failed to create dex directory " + dexDir.getPath());
112 }
113
114 // Clean possible old files
Yohann Rousseld9eda552013-11-12 18:13:55 +0100115 FileFilter filter = new FileFilter() {
116
Maurice Chu667f9a82013-10-16 13:12:22 -0700117 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100118 public boolean accept(File pathname) {
119 return (!pathname.getName().startsWith(extractedFilePrefix))
120 || (pathname.lastModified() < sourceLastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -0700121 }
122 };
123 File[] files = dexDir.listFiles(filter);
124 if (files == null) {
125 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
126 return;
127 }
128 for (File oldFile : files) {
129 if (!oldFile.delete()) {
130 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
131 }
132 }
133 }
134
135 private static void extract(
136 Context context, ZipFile apk, ZipEntry dexFile, File extractTo,
Yohann Rousseld9eda552013-11-12 18:13:55 +0100137 String extractedFilePrefix, long sourceLastModified)
138 throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700139
140 InputStream in = apk.getInputStream(dexFile);
141 ZipOutputStream out = null;
142 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
143 extractTo.getParentFile());
144 Log.i(TAG, "Extracting " + tmp.getPath());
145 try {
146 out = new ZipOutputStream(new FileOutputStream(tmp));
147 try {
148 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100149 // keep zip entry time since it is the criteria used by Dalvik
150 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700151 out.putNextEntry(classesDex);
152
153 byte[] buffer = new byte[BUFFER_SIZE];
154 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800155 while (length != -1) {
156 if (length > 0) {
157 out.write(buffer, 0, length);
158 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700159 length = in.read(buffer);
160 }
161 } finally {
162 closeQuietly(out);
163 }
Yohann Rousseld9eda552013-11-12 18:13:55 +0100164 if (!tmp.setLastModified(sourceLastModified)) {
165 Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." +
166 " This may cause problems with later updates of the apk.");
167 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700168 Log.i(TAG, "Renaming to " + extractTo.getPath());
169 if (!tmp.renameTo(extractTo)) {
170 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" +
171 extractTo.getAbsolutePath() + "\"");
172 }
173 } finally {
174 closeQuietly(in);
175 tmp.delete(); // return status ignored
176 }
177 }
178
179 /**
180 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
181 */
182 private static void closeQuietly(Closeable closeable) {
183 try {
184 closeable.close();
185 } catch (IOException e) {
186 Log.w(TAG, "Failed to close resource", e);
187 }
188 }
189}