blob: c4547dda87248f1a3b28304cbe1113a94337fbaf [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 Chu667f9a82013-10-16 13:12:22 -070019import java.io.Closeable;
20import java.io.File;
Yohann Rousseld9eda552013-11-12 18:13:55 +010021import java.io.FileFilter;
Maurice Chu667f9a82013-10-16 13:12:22 -070022import java.io.FileNotFoundException;
23import java.io.FileOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070024import java.io.IOException;
25import java.io.InputStream;
26import java.util.ArrayList;
27import java.util.List;
28import java.util.zip.ZipEntry;
29import java.util.zip.ZipFile;
30import java.util.zip.ZipOutputStream;
31
Yohann Rousseld9eda552013-11-12 18:13:55 +010032import android.content.Context;
33import android.content.pm.ApplicationInfo;
34import android.util.Log;
35
Maurice Chu667f9a82013-10-16 13:12:22 -070036/**
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");
149 out.putNextEntry(classesDex);
150
151 byte[] buffer = new byte[BUFFER_SIZE];
152 int length = in.read(buffer);
153 while (length > 0) {
154 out.write(buffer, 0, length);
155 length = in.read(buffer);
156 }
157 } finally {
158 closeQuietly(out);
159 }
Yohann Rousseld9eda552013-11-12 18:13:55 +0100160 if (!tmp.setLastModified(sourceLastModified)) {
161 Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." +
162 " This may cause problems with later updates of the apk.");
163 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700164 Log.i(TAG, "Renaming to " + extractTo.getPath());
165 if (!tmp.renameTo(extractTo)) {
166 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" +
167 extractTo.getAbsolutePath() + "\"");
168 }
169 } finally {
170 closeQuietly(in);
171 tmp.delete(); // return status ignored
172 }
173 }
174
175 /**
176 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
177 */
178 private static void closeQuietly(Closeable closeable) {
179 try {
180 closeable.close();
181 } catch (IOException e) {
182 Log.w(TAG, "Failed to close resource", e);
183 }
184 }
185}