MultiDexExtractor.java revision 7e267a38525afac2a571da186e770a2b86a01846
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.Context;
20import android.content.SharedPreferences;
21import android.content.pm.ApplicationInfo;
22import android.util.Log;
23
24import java.io.BufferedOutputStream;
25import java.io.Closeable;
26import java.io.File;
27import java.io.FileFilter;
28import java.io.FileNotFoundException;
29import java.io.FileOutputStream;
30import java.io.IOException;
31import java.io.InputStream;
32import java.lang.reflect.InvocationTargetException;
33import java.lang.reflect.Method;
34import java.util.ArrayList;
35import java.util.List;
36import java.util.zip.ZipEntry;
37import java.util.zip.ZipException;
38import java.util.zip.ZipFile;
39import java.util.zip.ZipOutputStream;
40
41/**
42 * Exposes application secondary dex files as files in the application data
43 * directory.
44 */
45final class MultiDexExtractor {
46
47    private static final String TAG = MultiDex.TAG;
48
49    /**
50     * We look for additional dex files named {@code classes2.dex},
51     * {@code classes3.dex}, etc.
52     */
53    private static final String DEX_PREFIX = "classes";
54    private static final String DEX_SUFFIX = ".dex";
55
56    private static final String EXTRACTED_NAME_EXT = ".classes";
57    private static final String EXTRACTED_SUFFIX = ".zip";
58    private static final int MAX_EXTRACT_ATTEMPTS = 3;
59
60    private static final int BUFFER_SIZE = 0x4000;
61
62    private static final String PREFS_FILE = "multidex.version";
63    private static final String KEY_APK_SIZE = "apk_size";
64    private static final String KEY_NUM_DEX_FILES = "num_dex";
65    private static final String KEY_PREFIX_DEX_CRC = "crc";
66
67    /**
68     * Extracts application secondary dexes into files in the application data
69     * directory.
70     *
71     * @return a list of files that were created. The list may be empty if there
72     *         are no secondary dex files.
73     * @throws IOException if encounters a problem while reading or writing
74     *         secondary dex files
75     */
76    static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
77            boolean forceReload) throws IOException {
78        Log.i(TAG, "load(" + applicationInfo.sourceDir + ", forceReload=" + forceReload + ")");
79        final File sourceApk = new File(applicationInfo.sourceDir);
80        final long currentApkSize = sourceApk.length();
81        final boolean isNewApk = getStoredApkSize(context) != currentApkSize;
82        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
83
84        prepareDexDir(dexDir, extractedFilePrefix, isNewApk);
85
86        final List<File> files = new ArrayList<File>();
87        final ZipFile apk = new ZipFile(applicationInfo.sourceDir);
88
89        // If the CRC of any of the dex files is different than what we have stored or the number of
90        // dex files are different, then force reload everything.
91        ArrayList<Long> dexCrcs = getAllDexCrcs(apk);
92        if (isAnyDexCrcDifferent(context, dexCrcs)) {
93            forceReload = true;
94        }
95        try {
96
97            int secondaryNumber = 2;
98
99            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
100            while (dexFile != null) {
101                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
102                File extractedFile = new File(dexDir, fileName);
103                files.add(extractedFile);
104
105                Log.i(TAG, "Need extracted file " + extractedFile);
106                if (forceReload || !extractedFile.isFile()) {
107                    Log.i(TAG, "Extraction is needed for file " + extractedFile);
108                    int numAttempts = 0;
109                    boolean isExtractionSuccessful = false;
110                    while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
111                        numAttempts++;
112
113                        // Create a zip file (extractedFile) containing only the secondary dex file
114                        // (dexFile) from the apk.
115                        extract(apk, dexFile, extractedFile, extractedFilePrefix);
116
117                        // Verify that the extracted file is indeed a zip file.
118                        isExtractionSuccessful = verifyZipFile(extractedFile);
119
120                        // Log the sha1 of the extracted zip file
121                        Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
122                                " - length " + extractedFile.getAbsolutePath() + ": " +
123                                extractedFile.length());
124                        if (!isExtractionSuccessful) {
125                            // Delete the extracted file
126                            extractedFile.delete();
127                        }
128                    }
129                    if (isExtractionSuccessful) {
130                        // Write the current apk size and the dex crc's into the shared preferences
131                        putStoredApkSize(context, currentApkSize);
132                        putStoredDexCrcs(context, dexCrcs);
133                    } else {
134                        throw new IOException("Could not create zip file " +
135                                extractedFile.getAbsolutePath() + " for secondary dex (" +
136                                secondaryNumber + ")");
137                    }
138                } else {
139                    Log.i(TAG, "No extraction needed for " + extractedFile + " of size " +
140                            extractedFile.length());
141                }
142                secondaryNumber++;
143                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
144            }
145        } finally {
146            try {
147                apk.close();
148            } catch (IOException e) {
149                Log.w(TAG, "Failed to close resource", e);
150            }
151        }
152
153        return files;
154    }
155
156    /**
157     * Iterate through the expected dex files, classes.dex, classes2.dex, classes3.dex, etc. and
158     * return the CRC of each zip entry in a list.
159     */
160    private static ArrayList<Long> getAllDexCrcs(ZipFile apk) {
161        ArrayList<Long> dexCrcs = new ArrayList<Long>();
162
163        // Add the first one
164        dexCrcs.add(apk.getEntry(DEX_PREFIX + DEX_SUFFIX).getCrc());
165
166        // Get the number of dex files in the apk.
167        int secondaryNumber = 2;
168        while (true) {
169            ZipEntry dexEntry = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
170            if (dexEntry == null) {
171                break;
172            }
173
174            dexCrcs.add(dexEntry.getCrc());
175            secondaryNumber++;
176        }
177        return dexCrcs;
178    }
179
180    /**
181     * Returns true if the number of dex files is different than what is stored in the shared
182     * preferences file or if any dex CRC value is different.
183     */
184    private static boolean isAnyDexCrcDifferent(Context context, ArrayList<Long> dexCrcs) {
185        final ArrayList<Long> storedDexCrcs = getStoredDexCrcs(context);
186
187        if (dexCrcs.size() != storedDexCrcs.size()) {
188            return true;
189        }
190
191        // We know the length of storedDexCrcs and dexCrcs are the same.
192        for (int i = 0; i < storedDexCrcs.size(); i++) {
193            if (storedDexCrcs.get(i) != dexCrcs.get(i)) {
194                return true;
195            }
196        }
197
198        // All the same
199        return false;
200    }
201
202    private static long getStoredApkSize(Context context) {
203        SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0);
204        return prefs.getLong(KEY_APK_SIZE, -1L);
205    }
206
207    private static void putStoredApkSize(Context context, long apkSize) {
208        SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0);
209        SharedPreferences.Editor edit = prefs.edit();
210        edit.putLong(KEY_APK_SIZE, apkSize);
211        apply(edit);
212    }
213
214    private static ArrayList<Long> getStoredDexCrcs(Context context) {
215        SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0);
216        int numDexFiles = prefs.getInt(KEY_NUM_DEX_FILES, 0);
217        ArrayList<Long> dexCrcs = new ArrayList<Long>(numDexFiles);
218        for (int i = 0; i < numDexFiles; i++) {
219            dexCrcs.add(prefs.getLong(makeDexCrcKey(i), 0));
220        }
221        return dexCrcs;
222    }
223
224    private static void putStoredDexCrcs(Context context, ArrayList<Long> dexCrcs) {
225        SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0);
226        SharedPreferences.Editor edit = prefs.edit();
227        edit.putInt(KEY_NUM_DEX_FILES, dexCrcs.size());
228        for (int i = 0; i < dexCrcs.size(); i++) {
229            edit.putLong(makeDexCrcKey(i), dexCrcs.get(i));
230        }
231        apply(edit);
232    }
233
234    private static String makeDexCrcKey(int i) {
235        return KEY_PREFIX_DEX_CRC + Integer.toString(i);
236    }
237
238    /**
239     * This always removes extraneous files.  If {@code removeAll} is true, then any existing
240     * zip and dex files are removed from the secondary-dexes directory.
241     */
242    private static void prepareDexDir(File dexDir, final String extractedFilePrefix,
243            final boolean removeAll) throws IOException {
244        dexDir.mkdir();
245        if (!dexDir.isDirectory()) {
246            throw new IOException("Failed to create dex directory " + dexDir.getPath());
247        }
248
249        // Clean possible old files
250        FileFilter filter = new FileFilter() {
251
252            @Override
253            public boolean accept(File pathname) {
254                return removeAll || !pathname.getName().startsWith(extractedFilePrefix);
255            }
256        };
257        File[] files = dexDir.listFiles(filter);
258        if (files == null) {
259            Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
260            return;
261        }
262        for (File oldFile : files) {
263            Log.w(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
264                    oldFile.length());
265            if (!oldFile.delete()) {
266                Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
267            } else {
268                Log.w(TAG, "Deleted old file " + oldFile.getPath());
269            }
270        }
271    }
272
273    private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
274            String extractedFilePrefix) throws IOException, FileNotFoundException {
275
276        InputStream in = apk.getInputStream(dexFile);
277        ZipOutputStream out = null;
278        File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
279                extractTo.getParentFile());
280        Log.i(TAG, "Extracting " + tmp.getPath());
281        try {
282            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
283            try {
284                ZipEntry classesDex = new ZipEntry("classes.dex");
285                // keep zip entry time since it is the criteria used by Dalvik
286                classesDex.setTime(dexFile.getTime());
287                out.putNextEntry(classesDex);
288
289                byte[] buffer = new byte[BUFFER_SIZE];
290                int length = in.read(buffer);
291                while (length != -1) {
292                    out.write(buffer, 0, length);
293                    length = in.read(buffer);
294                }
295                out.closeEntry();
296            } finally {
297                out.close();
298            }
299            Log.i(TAG, "Renaming to " + extractTo.getPath());
300            if (!tmp.renameTo(extractTo)) {
301                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
302                        "\" to \"" + extractTo.getAbsolutePath() + "\"");
303            }
304        } finally {
305            closeQuietly(in);
306            tmp.delete(); // return status ignored
307        }
308    }
309
310    /**
311     * Returns whether the file is a valid zip file.
312     */
313    static boolean verifyZipFile(File file) {
314        try {
315            ZipFile zipFile = new ZipFile(file);
316            try {
317                zipFile.close();
318                return true;
319            } catch (IOException e) {
320                Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
321            }
322        } catch (ZipException ex) {
323            Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
324        } catch (IOException ex) {
325            Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
326        }
327        return false;
328    }
329
330    /**
331     * Closes the given {@code Closeable}. Suppresses any IO exceptions.
332     */
333    private static void closeQuietly(Closeable closeable) {
334        try {
335            closeable.close();
336        } catch (IOException e) {
337            Log.w(TAG, "Failed to close resource", e);
338        }
339    }
340
341    // The following is taken from SharedPreferencesCompat to avoid having a dependency of the
342    // multidex support library on another support library.
343    private static Method sApplyMethod;  // final
344    static {
345        try {
346            Class cls = SharedPreferences.Editor.class;
347            sApplyMethod = cls.getMethod("apply");
348        } catch (NoSuchMethodException unused) {
349            sApplyMethod = null;
350        }
351    }
352
353    private static void apply(SharedPreferences.Editor editor) {
354        if (sApplyMethod != null) {
355            try {
356                sApplyMethod.invoke(editor);
357                return;
358            } catch (InvocationTargetException unused) {
359                // fall through
360            } catch (IllegalAccessException unused) {
361                // fall through
362            }
363        }
364        editor.commit();
365    }
366}
367