MultiDexExtractor.java revision cc63eda4e6defe6b0dd5dd3c8fa608cf6ff26011
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, boolean forceReload)
68            throws IOException {
69        Log.i(TAG, "load(" + applicationInfo.sourceDir + ", forceReload=" + forceReload + ")");
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 (forceReload || !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    /**
137     * This removes any old zip and dex files or extraneous files from the secondary-dexes
138     * directory.
139     */
140    private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
141            final long sourceLastModified) throws IOException {
142        dexDir.mkdir();
143        if (!dexDir.isDirectory()) {
144            throw new IOException("Failed to create dex directory " + dexDir.getPath());
145        }
146
147        // Clean possible old files
148        FileFilter filter = new FileFilter() {
149
150            @Override
151            public boolean accept(File pathname) {
152                return (!pathname.getName().startsWith(extractedFilePrefix))
153                        || (pathname.lastModified() < sourceLastModified);
154            }
155        };
156        File[] files = dexDir.listFiles(filter);
157        if (files == null) {
158            Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
159            return;
160        }
161        for (File oldFile : files) {
162            Log.w(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
163            if (!oldFile.delete()) {
164                Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
165            } else {
166                Log.w(TAG, "Deleted old file " + oldFile.getPath());
167            }
168        }
169    }
170
171    private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
172            String extractedFilePrefix, long sourceLastModified)
173                    throws IOException, FileNotFoundException {
174
175        InputStream in = apk.getInputStream(dexFile);
176        ZipOutputStream out = null;
177        File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
178                extractTo.getParentFile());
179        Log.i(TAG, "Extracting " + tmp.getPath());
180        try {
181            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
182            try {
183                ZipEntry classesDex = new ZipEntry("classes.dex");
184                // keep zip entry time since it is the criteria used by Dalvik
185                classesDex.setTime(dexFile.getTime());
186                out.putNextEntry(classesDex);
187
188                byte[] buffer = new byte[BUFFER_SIZE];
189                int length = in.read(buffer);
190                while (length != -1) {
191                    out.write(buffer, 0, length);
192                    length = in.read(buffer);
193                }
194                out.closeEntry();
195            } finally {
196                out.close();
197            }
198            if (!tmp.setLastModified(sourceLastModified)) {
199                Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." +
200                        " This may cause problems with later updates of the apk.");
201            }
202            Log.i(TAG, "Renaming to " + extractTo.getPath());
203            if (!tmp.renameTo(extractTo)) {
204                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
205                        "\" to \"" + extractTo.getAbsolutePath() + "\"");
206            }
207        } finally {
208            closeQuietly(in);
209            tmp.delete(); // return status ignored
210        }
211    }
212
213    /**
214     * Returns whether the file is a valid zip file.
215     */
216    static boolean verifyZipFile(File file) {
217        try {
218            ZipFile zipFile = new ZipFile(file);
219            try {
220                zipFile.close();
221                return true;
222            } catch (IOException e) {
223                Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
224            }
225        } catch (ZipException ex) {
226            Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
227        } catch (IOException ex) {
228            Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
229        }
230        return false;
231    }
232
233    /**
234     * Closes the given {@code Closeable}. Suppresses any IO exceptions.
235     */
236    private static void closeQuietly(Closeable closeable) {
237        try {
238            closeable.close();
239        } catch (IOException e) {
240            Log.w(TAG, "Failed to close resource", e);
241        }
242    }
243}
244