blob: 05d237a5636ae2efda27096da00086827c0fa5ec [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;
29import java.util.ArrayList;
30import java.util.List;
31import java.util.zip.ZipEntry;
32import java.util.zip.ZipFile;
33import java.util.zip.ZipOutputStream;
34
35/**
36 * Exposes application secondary dex files as files in the application data
37 * directory.
38 */
39final class MultiDexExtractor {
40
41 private static final String TAG = MultiDex.TAG;
42
43 /**
44 * We look for additional dex files named {@code classes2.dex},
45 * {@code classes3.dex}, etc.
46 */
47 private static final String DEX_PREFIX = "classes";
48 private static final String DEX_SUFFIX = ".dex";
49
50 private static final String EXTRACTED_NAME_EXT = ".classes";
51 private static final String EXTRACTED_SUFFIX = ".zip";
52
53 private static final int BUFFER_SIZE = 0x4000;
54
55 /**
56 * Extracts application secondary dexes into files in the application data
57 * directory.
58 *
Maurice Chu667f9a82013-10-16 13:12:22 -070059 * @return a list of files that were created. The list may be empty if there
60 * are no secondary dex files.
61 * @throws IOException if encounters a problem while reading or writing
62 * secondary dex files
63 */
Yohann Roussel52eafa02013-11-21 11:46:53 +010064 static List<File> load(ApplicationInfo applicationInfo, File dexDir)
Maurice Chu667f9a82013-10-16 13:12:22 -070065 throws IOException {
66
Yohann Rousseld9eda552013-11-12 18:13:55 +010067 File sourceApk = new File(applicationInfo.sourceDir);
68 long lastModified = sourceApk.lastModified();
69 String extractedFilePrefix = sourceApk.getName()
Maurice Chu667f9a82013-10-16 13:12:22 -070070 + EXTRACTED_NAME_EXT;
71
Yohann Rousseld9eda552013-11-12 18:13:55 +010072 prepareDexDir(dexDir, extractedFilePrefix, lastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -070073
74 final List<File> files = new ArrayList<File>();
75 ZipFile apk = new ZipFile(applicationInfo.sourceDir);
76 try {
77
78 int secondaryNumber = 2;
79
80 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
81 while (dexFile != null) {
82 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
83 File extractedFile = new File(dexDir, fileName);
84 files.add(extractedFile);
85
86 if (!extractedFile.isFile()) {
Yohann Roussel52eafa02013-11-21 11:46:53 +010087 extract(apk, dexFile, extractedFile, extractedFilePrefix,
Yohann Rousseld9eda552013-11-12 18:13:55 +010088 lastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -070089 }
90 secondaryNumber++;
91 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
92 }
93 } finally {
94 try {
95 apk.close();
96 } catch (IOException e) {
97 Log.w(TAG, "Failed to close resource", e);
98 }
99 }
100
101 return files;
102 }
103
Yohann Rousseld9eda552013-11-12 18:13:55 +0100104 private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
105 final long sourceLastModified) throws IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700106 dexDir.mkdir();
107 if (!dexDir.isDirectory()) {
108 throw new IOException("Failed to create dex directory " + dexDir.getPath());
109 }
110
111 // Clean possible old files
Yohann Rousseld9eda552013-11-12 18:13:55 +0100112 FileFilter filter = new FileFilter() {
113
Maurice Chu667f9a82013-10-16 13:12:22 -0700114 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100115 public boolean accept(File pathname) {
116 return (!pathname.getName().startsWith(extractedFilePrefix))
117 || (pathname.lastModified() < sourceLastModified);
Maurice Chu667f9a82013-10-16 13:12:22 -0700118 }
119 };
120 File[] files = dexDir.listFiles(filter);
121 if (files == null) {
122 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
123 return;
124 }
125 for (File oldFile : files) {
126 if (!oldFile.delete()) {
127 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
128 }
129 }
130 }
131
Yohann Roussel52eafa02013-11-21 11:46:53 +0100132 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
Yohann Rousseld9eda552013-11-12 18:13:55 +0100133 String extractedFilePrefix, long sourceLastModified)
134 throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700135
136 InputStream in = apk.getInputStream(dexFile);
137 ZipOutputStream out = null;
138 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
139 extractTo.getParentFile());
140 Log.i(TAG, "Extracting " + tmp.getPath());
141 try {
142 out = new ZipOutputStream(new FileOutputStream(tmp));
143 try {
144 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100145 // keep zip entry time since it is the criteria used by Dalvik
146 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700147 out.putNextEntry(classesDex);
148
149 byte[] buffer = new byte[BUFFER_SIZE];
150 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800151 while (length != -1) {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100152 out.write(buffer, 0, length);
Maurice Chu667f9a82013-10-16 13:12:22 -0700153 length = in.read(buffer);
154 }
155 } finally {
156 closeQuietly(out);
157 }
Yohann Rousseld9eda552013-11-12 18:13:55 +0100158 if (!tmp.setLastModified(sourceLastModified)) {
159 Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." +
160 " This may cause problems with later updates of the apk.");
161 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700162 Log.i(TAG, "Renaming to " + extractTo.getPath());
163 if (!tmp.renameTo(extractTo)) {
164 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" +
165 extractTo.getAbsolutePath() + "\"");
166 }
167 } finally {
168 closeQuietly(in);
169 tmp.delete(); // return status ignored
170 }
171 }
172
173 /**
174 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
175 */
176 private static void closeQuietly(Closeable closeable) {
177 try {
178 closeable.close();
179 } catch (IOException e) {
180 Log.w(TAG, "Failed to close resource", e);
181 }
182 }
183}