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