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