MultiDexExtractor.java revision 88117c37e2c3f8601f295b74a3e804877afb78ee
1/*
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.pm.ApplicationInfo;
20import android.util.Log;
21
22import java.io.BufferedOutputStream;
23import java.io.Closeable;
24import java.io.File;
25import java.io.FileFilter;
26import java.io.FileNotFoundException;
27import java.io.FileOutputStream;
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.ZipException;
34import 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";
54    private static final int MAX_EXTRACT_ATTEMPTS = 3;
55
56    private static final int BUFFER_SIZE = 0x4000;
57
58    /**
59     * Extracts application secondary dexes into files in the application data
60     * directory.
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(ApplicationInfo applicationInfo, File dexDir)
68            throws IOException {
69        Log.i(TAG, "load(" + applicationInfo.sourceDir + ")");
70        File sourceApk = new File(applicationInfo.sourceDir);
71        long lastModified = sourceApk.lastModified();
72        String extractedFilePrefix = sourceApk.getName()
73                + EXTRACTED_NAME_EXT;
74
75        prepareDexDir(dexDir, extractedFilePrefix, lastModified);
76
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                Log.i(TAG, "Need extracted file " + extractedFile);
90                if (!extractedFile.isFile()) {
91                    Log.i(TAG, "Extraction is needed for file " + extractedFile);
92                    int numAttempts = 0;
93                    boolean isExtractionSuccessful = false;
94                    while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
95                        numAttempts++;
96
97                        // Create a zip file (extractedFile) containing only the secondary dex file
98                        // (dexFile) from the apk.
99                        extract(apk, dexFile, extractedFile, extractedFilePrefix,
100                                lastModified);
101
102                        // Verify that the extracted file is indeed a zip file.
103                        isExtractionSuccessful = verifyZipFile(extractedFile);
104
105                        // Log the sha1 of the extracted zip file
106                        Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
107                                " - length " + extractedFile.getAbsolutePath() + ": " +
108                                extractedFile.length());
109                        if (!isExtractionSuccessful) {
110                            // Delete the extracted file
111                            extractedFile.delete();
112                        }
113                    }
114                    if (!isExtractionSuccessful) {
115                        throw new IOException("Could not create zip file " +
116                                extractedFile.getAbsolutePath() + " for secondary dex (" +
117                                secondaryNumber + ")");
118                    }
119                } else {
120                    Log.i(TAG, "No extraction needed for " + extractedFile + " of size " + extractedFile.length());
121                }
122                secondaryNumber++;
123                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
124            }
125        } finally {
126            try {
127                apk.close();
128            } catch (IOException e) {
129                Log.w(TAG, "Failed to close resource", e);
130            }
131        }
132
133        return files;
134    }
135
136    private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
137            final long sourceLastModified) throws IOException {
138        dexDir.mkdir();
139        if (!dexDir.isDirectory()) {
140            throw new IOException("Failed to create dex directory " + dexDir.getPath());
141        }
142
143        // Clean possible old files
144        FileFilter filter = new FileFilter() {
145
146            @Override
147            public boolean accept(File pathname) {
148                return (!pathname.getName().startsWith(extractedFilePrefix))
149                    || (pathname.lastModified() < sourceLastModified);
150            }
151        };
152        File[] files = dexDir.listFiles(filter);
153        if (files == null) {
154            Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
155            return;
156        }
157        for (File oldFile : files) {
158            Log.w(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
159            if (!oldFile.delete()) {
160                Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
161            } else {
162                Log.w(TAG, "Deleted old file " + oldFile.getPath());
163            }
164        }
165    }
166
167    private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
168            String extractedFilePrefix, long sourceLastModified)
169                    throws IOException, FileNotFoundException {
170
171        InputStream in = apk.getInputStream(dexFile);
172        ZipOutputStream out = null;
173        File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
174                extractTo.getParentFile());
175        Log.i(TAG, "Extracting " + tmp.getPath());
176        try {
177            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
178            try {
179                ZipEntry classesDex = new ZipEntry("classes.dex");
180                // keep zip entry time since it is the criteria used by Dalvik
181                classesDex.setTime(dexFile.getTime());
182                out.putNextEntry(classesDex);
183
184                byte[] buffer = new byte[BUFFER_SIZE];
185                int length = in.read(buffer);
186                while (length != -1) {
187                    out.write(buffer, 0, length);
188                    length = in.read(buffer);
189                }
190                out.closeEntry();
191            } finally {
192                out.close();
193            }
194            if (!tmp.setLastModified(sourceLastModified)) {
195                Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." +
196                        " This may cause problems with later updates of the apk.");
197            }
198            Log.i(TAG, "Renaming to " + extractTo.getPath());
199            if (!tmp.renameTo(extractTo)) {
200                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
201                        "\" to \"" + extractTo.getAbsolutePath() + "\"");
202            }
203        } finally {
204            closeQuietly(in);
205            tmp.delete(); // return status ignored
206        }
207    }
208
209    /**
210     * Returns whether the file is a valid zip file.
211     */
212    private static boolean verifyZipFile(File file) {
213        try {
214            ZipFile zipFile = new ZipFile(file);
215            try {
216                zipFile.close();
217                return true;
218            } catch (IOException e) {
219                Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
220            }
221        } catch (ZipException ex) {
222            Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
223        } catch (IOException ex) {
224            Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
225        }
226        return false;
227    }
228
229    /**
230     * Closes the given {@code Closeable}. Suppresses any IO exceptions.
231     */
232    private static void closeQuietly(Closeable closeable) {
233        try {
234            closeable.close();
235        } catch (IOException e) {
236            Log.w(TAG, "Failed to close resource", e);
237        }
238    }
239}
240