blob: bcd9c4de9e47bf6ee5879d281824ce67b26c83bb [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
19import android.content.Context;
20import android.content.pm.ApplicationInfo;
21import android.util.Log;
22
23import java.io.Closeable;
24import java.io.File;
25import java.io.FileNotFoundException;
26import java.io.FileOutputStream;
27import java.io.FilenameFilter;
28import 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
70 String extractedFilePrefix = new File(applicationInfo.sourceDir).getName()
71 + EXTRACTED_NAME_EXT;
72
73 prepareDexDir(dexDir, extractedFilePrefix);
74
75 final List<File> files = new ArrayList<File>();
76 ZipFile apk = new ZipFile(applicationInfo.sourceDir);
77 try {
78
79 int secondaryNumber = 2;
80
81 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
82 while (dexFile != null) {
83 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
84 File extractedFile = new File(dexDir, fileName);
85 files.add(extractedFile);
86
87 if (!extractedFile.isFile()) {
88 extract(context, apk, dexFile, extractedFile, extractedFilePrefix);
89 }
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
104 private static void prepareDexDir(File dexDir, final String extractedFilePrefix)
105 throws IOException {
106 dexDir.mkdir();
107 if (!dexDir.isDirectory()) {
108 throw new IOException("Failed to create dex directory " + dexDir.getPath());
109 }
110
111 // Clean possible old files
112 FilenameFilter filter = new FilenameFilter() {
113 @Override
114 public boolean accept(File dir, String name) {
115 return !name.startsWith(extractedFilePrefix);
116 }
117 };
118 File[] files = dexDir.listFiles(filter);
119 if (files == null) {
120 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
121 return;
122 }
123 for (File oldFile : files) {
124 if (!oldFile.delete()) {
125 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
126 }
127 }
128 }
129
130 private static void extract(
131 Context context, ZipFile apk, ZipEntry dexFile, File extractTo,
132 String extractedFilePrefix) throws IOException, FileNotFoundException {
133
134 InputStream in = apk.getInputStream(dexFile);
135 ZipOutputStream out = null;
136 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
137 extractTo.getParentFile());
138 Log.i(TAG, "Extracting " + tmp.getPath());
139 try {
140 out = new ZipOutputStream(new FileOutputStream(tmp));
141 try {
142 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100143 // keep zip entry time since it is the criteria used by Dalvik
144 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700145 out.putNextEntry(classesDex);
146
147 byte[] buffer = new byte[BUFFER_SIZE];
148 int length = in.read(buffer);
149 while (length > 0) {
150 out.write(buffer, 0, length);
151 length = in.read(buffer);
152 }
153 } finally {
154 closeQuietly(out);
155 }
156 Log.i(TAG, "Renaming to " + extractTo.getPath());
157 if (!tmp.renameTo(extractTo)) {
158 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" +
159 extractTo.getAbsolutePath() + "\"");
160 }
161 } finally {
162 closeQuietly(in);
163 tmp.delete(); // return status ignored
164 }
165 }
166
167 /**
168 * Closes the given {@code Closeable}. Suppresses any IO exceptions.
169 */
170 private static void closeQuietly(Closeable closeable) {
171 try {
172 closeable.close();
173 } catch (IOException e) {
174 Log.w(TAG, "Failed to close resource", e);
175 }
176 }
177}