MultiDexExtractor.java revision f6d1f23926672c8dd61da515f8d1bcb37ef4292d
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.FileInputStream;
26import java.io.FileNotFoundException;
27import java.io.FileOutputStream;
28import java.io.IOException;
29import java.io.InputStream;
30import java.security.MessageDigest;
31import java.security.NoSuchAlgorithmException;
32import java.util.ArrayList;
33import java.util.List;
34import java.util.zip.ZipEntry;
35import java.util.zip.ZipException;
36import java.util.zip.ZipFile;
37import java.util.zip.ZipOutputStream;
38
39/**
40 * Exposes application secondary dex files as files in the application data
41 * directory.
42 */
43final class MultiDexExtractor {
44
45    private static final String TAG = MultiDex.TAG;
46
47    /**
48     * We look for additional dex files named {@code classes2.dex},
49     * {@code classes3.dex}, etc.
50     */
51    private static final String DEX_PREFIX = "classes";
52    private static final String DEX_SUFFIX = ".dex";
53
54    private static final String EXTRACTED_NAME_EXT = ".classes";
55    private static final String EXTRACTED_SUFFIX = ".zip";
56    private static final int MAX_EXTRACT_ATTEMPTS = 3;
57    private static final int MAX_ATTEMPTS_NO_SUCH_ALGORITHM = 2;
58
59
60    private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
61        'A', 'B', 'C', 'D', 'E', 'F' };
62
63    private static final int BUFFER_SIZE = 0x4000;
64
65    /**
66     * Extracts application secondary dexes into files in the application data
67     * directory.
68     *
69     * @return a list of files that were created. The list may be empty if there
70     *         are no secondary dex files.
71     * @throws IOException if encounters a problem while reading or writing
72     *         secondary dex files
73     */
74    static List<File> load(ApplicationInfo applicationInfo, File dexDir)
75            throws IOException {
76
77        File sourceApk = new File(applicationInfo.sourceDir);
78        long lastModified = sourceApk.lastModified();
79        String extractedFilePrefix = sourceApk.getName()
80                + EXTRACTED_NAME_EXT;
81
82        prepareDexDir(dexDir, extractedFilePrefix, lastModified);
83
84        final List<File> files = new ArrayList<File>();
85        ZipFile apk = new ZipFile(applicationInfo.sourceDir);
86        try {
87
88            int secondaryNumber = 2;
89
90            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
91            while (dexFile != null) {
92                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
93                File extractedFile = new File(dexDir, fileName);
94                files.add(extractedFile);
95
96                if (!extractedFile.isFile()) {
97                    int numAttempts = 0;
98                    boolean isExtractionSuccessful = false;
99                    while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
100                        numAttempts++;
101
102                        // Create a zip file (extractedFile) containing only the secondary dex file
103                        // (dexFile) from the apk.
104                        extract(apk, dexFile, extractedFile, extractedFilePrefix,
105                                lastModified);
106
107                        // Verify that the extracted file is indeed a zip file.
108                        isExtractionSuccessful = verifyZipFile(extractedFile);
109
110                        // Log the sha1 of the extracted zip file
111                        Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
112                                " - SHA1 of " + extractedFile.getAbsolutePath() + ": " +
113                                computeSha1Digest(extractedFile));
114                        if (!isExtractionSuccessful) {
115                            // Delete the extracted file
116                            extractedFile.delete();
117                        }
118                    }
119                    if (!isExtractionSuccessful) {
120                        throw new IOException("Could not create zip file " +
121                                extractedFile.getAbsolutePath() + " for secondary dex (" +
122                                secondaryNumber + ")");
123                    }
124                }
125                secondaryNumber++;
126                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
127            }
128        } finally {
129            try {
130                apk.close();
131            } catch (IOException e) {
132                Log.w(TAG, "Failed to close resource", e);
133            }
134        }
135
136        return files;
137    }
138
139    private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
140            final long sourceLastModified) throws IOException {
141        dexDir.mkdir();
142        if (!dexDir.isDirectory()) {
143            throw new IOException("Failed to create dex directory " + dexDir.getPath());
144        }
145
146        // Clean possible old files
147        FileFilter filter = new FileFilter() {
148
149            @Override
150            public boolean accept(File pathname) {
151                return (!pathname.getName().startsWith(extractedFilePrefix))
152                    || (pathname.lastModified() < sourceLastModified);
153            }
154        };
155        File[] files = dexDir.listFiles(filter);
156        if (files == null) {
157            Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
158            return;
159        }
160        for (File oldFile : files) {
161            if (!oldFile.delete()) {
162                Log.w(TAG, "Failed to delete 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 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            } finally {
191                closeQuietly(out);
192            }
193            if (!tmp.setLastModified(sourceLastModified)) {
194                Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." +
195                        " This may cause problems with later updates of the apk.");
196            }
197            Log.i(TAG, "Renaming to " + extractTo.getPath());
198            if (!tmp.renameTo(extractTo)) {
199                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" +
200                        extractTo.getAbsolutePath() + "\"");
201            }
202        } finally {
203            closeQuietly(in);
204            tmp.delete(); // return status ignored
205        }
206    }
207
208    /**
209     * Returns whether the file is a valid zip file.
210     */
211    private static boolean verifyZipFile(File file) {
212        try {
213            ZipFile zipFile = new ZipFile(file);
214            try {
215                zipFile.close();
216            } catch (IOException e) {
217                Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
218            }
219            return true;
220        } catch (ZipException ex) {
221            Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
222        } catch (IOException ex) {
223            Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
224        }
225        return false;
226    }
227
228    /**
229     * Closes the given {@code Closeable}. Suppresses any IO exceptions.
230     */
231    private static void closeQuietly(Closeable closeable) {
232        try {
233            closeable.close();
234        } catch (IOException e) {
235            Log.w(TAG, "Failed to close resource", e);
236        }
237    }
238
239    private static synchronized String computeSha1Digest(File file) {
240        MessageDigest messageDigest = getMessageDigest("SHA1");
241        if (messageDigest == null) {
242            return "";
243        }
244        FileInputStream in = null;
245        try {
246            in = new FileInputStream(file);
247            byte[] bytes = new byte[8192];
248            int byteCount;
249            while ((byteCount = in.read(bytes)) != -1) {
250                messageDigest.update(bytes, 0, byteCount);
251            }
252            return toHex(messageDigest.digest(), false /* zeroTerminated */)
253                    .toLowerCase();
254        } catch (IOException e) {
255            return "";
256        } finally {
257            if (in != null) {
258                closeQuietly(in);
259            }
260        }
261    }
262
263    /**
264     * Encodes a byte array as a hexadecimal representation of bytes.
265     */
266    private static String toHex(byte[] in, boolean zeroTerminated) {
267        int length = in.length;
268        StringBuilder out = new StringBuilder(length * 2);
269        for (int i = 0; i < length; i++) {
270            if (zeroTerminated && i == length - 1 && (in[i] & 0xff) == 0) {
271                break;
272            }
273            out.append(HEX_DIGITS[(in[i] & 0xf0) >>> 4]);
274            out.append(HEX_DIGITS[in[i] & 0x0f]);
275        }
276        return out.toString();
277    }
278
279    /**
280     * Retrieves the message digest instance for a given hash algorithm. Makes
281     * {@link #MAX_ATTEMPTS_NO_SUCH_ALGORITHM} to successfully retrieve the
282     * MessageDigest or will return null.
283     */
284    private static MessageDigest getMessageDigest(String hashAlgorithm) {
285        for (int i = 0; i < MAX_ATTEMPTS_NO_SUCH_ALGORITHM; i++) {
286            try {
287                MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm);
288                if (messageDigest != null) {
289                    return messageDigest;
290                }
291            } catch (NoSuchAlgorithmException e) {
292                // try again - this is needed due to a bug in MessageDigest that can have corrupted
293                // internal state.
294                continue;
295            }
296        }
297        return null;
298    }
299}
300