1667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu/*
2667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * Copyright (C) 2013 The Android Open Source Project
3667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu *
4667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * Licensed under the Apache License, Version 2.0 (the "License");
5667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * you may not use this file except in compliance with the License.
6667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * You may obtain a copy of the License at
7667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu *
8667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu *      http://www.apache.org/licenses/LICENSE-2.0
9667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu *
10667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * Unless required by applicable law or agreed to in writing, software
11667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * distributed under the License is distributed on an "AS IS" BASIS,
12667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * See the License for the specific language governing permissions and
14667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * limitations under the License.
15667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu */
16667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
17667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chupackage android.support.multidex;
18667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
197e267a38525afac2a571da186e770a2b86a01846Maurice Chuimport android.content.Context;
207e267a38525afac2a571da186e770a2b86a01846Maurice Chuimport android.content.SharedPreferences;
211f8c349b6524aa39a10a570115ce0afb039bd06fMaurice Chuimport android.content.pm.ApplicationInfo;
22994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chuimport android.os.Build;
231f8c349b6524aa39a10a570115ce0afb039bd06fMaurice Chuimport android.util.Log;
24a159fd5dbd60171c9bb602f316ad8272419afe40Maurice Chuimport java.io.BufferedOutputStream;
25667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.io.Closeable;
26667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.io.File;
27d9eda5550540306f2037e2db2aba2919fda90057Yohann Rousselimport java.io.FileFilter;
28667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.io.FileNotFoundException;
29667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.io.FileOutputStream;
30667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.io.IOException;
31667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.io.InputStream;
32048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnsonimport java.io.RandomAccessFile;
33048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnsonimport java.nio.channels.FileChannel;
34048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnsonimport java.nio.channels.FileLock;
35667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.util.ArrayList;
36667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.util.List;
37667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.util.zip.ZipEntry;
38667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.util.zip.ZipFile;
39667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chuimport java.util.zip.ZipOutputStream;
40667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
41667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu/**
42667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * Exposes application secondary dex files as files in the application data
43667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu * directory.
44667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu */
45667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chufinal class MultiDexExtractor {
46667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
479958145a97586375966bc133c900ead0e1550f2aYohann Roussel    /**
489958145a97586375966bc133c900ead0e1550f2aYohann Roussel     * Zip file containing one secondary dex file.
499958145a97586375966bc133c900ead0e1550f2aYohann Roussel     */
509958145a97586375966bc133c900ead0e1550f2aYohann Roussel    private static class ExtractedDex extends File {
519958145a97586375966bc133c900ead0e1550f2aYohann Roussel        public long crc = NO_VALUE;
529958145a97586375966bc133c900ead0e1550f2aYohann Roussel
539958145a97586375966bc133c900ead0e1550f2aYohann Roussel        public ExtractedDex(File dexDir, String fileName) {
549958145a97586375966bc133c900ead0e1550f2aYohann Roussel            super(dexDir, fileName);
559958145a97586375966bc133c900ead0e1550f2aYohann Roussel        }
569958145a97586375966bc133c900ead0e1550f2aYohann Roussel    }
579958145a97586375966bc133c900ead0e1550f2aYohann Roussel
58667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    private static final String TAG = MultiDex.TAG;
59667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
60667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    /**
61667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     * We look for additional dex files named {@code classes2.dex},
62667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     * {@code classes3.dex}, etc.
63667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     */
64667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    private static final String DEX_PREFIX = "classes";
65667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    private static final String DEX_SUFFIX = ".dex";
66667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
67667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    private static final String EXTRACTED_NAME_EXT = ".classes";
68667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    private static final String EXTRACTED_SUFFIX = ".zip";
69f6d1f23926672c8dd61da515f8d1bcb37ef4292dMaurice Chu    private static final int MAX_EXTRACT_ATTEMPTS = 3;
70667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
717e267a38525afac2a571da186e770a2b86a01846Maurice Chu    private static final String PREFS_FILE = "multidex.version";
72602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    private static final String KEY_TIME_STAMP = "timestamp";
73602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    private static final String KEY_CRC = "crc";
74602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    private static final String KEY_DEX_NUMBER = "dex.number";
759958145a97586375966bc133c900ead0e1550f2aYohann Roussel    private static final String KEY_DEX_CRC = "dex.crc.";
769958145a97586375966bc133c900ead0e1550f2aYohann Roussel    private static final String KEY_DEX_TIME = "dex.time.";
77602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
78602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    /**
79602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel     * Size of reading buffers.
80602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel     */
81602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    private static final int BUFFER_SIZE = 0x4000;
82602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    /* Keep value away from 0 because it is a too probable time stamp value */
83602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    private static final long NO_VALUE = -1L;
847e267a38525afac2a571da186e770a2b86a01846Maurice Chu
85048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson    private static final String LOCK_FILENAME = "MultiDex.lock";
86048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson
87667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    /**
88667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     * Extracts application secondary dexes into files in the application data
89667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     * directory.
90667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     *
91667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     * @return a list of files that were created. The list may be empty if there
92048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     *         are no secondary dex files. Never return null.
93667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     * @throws IOException if encounters a problem while reading or writing
94667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     *         secondary dex files
95667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     */
969958145a97586375966bc133c900ead0e1550f2aYohann Roussel    static List<? extends File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
977e267a38525afac2a571da186e770a2b86a01846Maurice Chu            boolean forceReload) throws IOException {
98602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
997e267a38525afac2a571da186e770a2b86a01846Maurice Chu        final File sourceApk = new File(applicationInfo.sourceDir);
100602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
101590a07e63868f0a1da311ff22b4a9f35eb48a865Yohann Roussel        long currentCrc = getZipCrc(sourceApk);
102602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
103048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        // Validity check and extraction must be done only while the lock file has been taken.
104048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        File lockFile = new File(dexDir, LOCK_FILENAME);
105048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
106048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        FileChannel lockChannel = null;
107048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        FileLock cacheLock = null;
1089958145a97586375966bc133c900ead0e1550f2aYohann Roussel        List<ExtractedDex> files;
109048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        IOException releaseLockException = null;
110048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        try {
111048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            lockChannel = lockRaf.getChannel();
112048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            Log.i(TAG, "Blocking on lock " + lockFile.getPath());
113048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            cacheLock = lockChannel.lock();
114048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            Log.i(TAG, lockFile.getPath() + " locked");
115048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson
116048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
117048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                try {
118048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                    files = loadExistingExtractions(context, sourceApk, dexDir);
119048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                } catch (IOException ioe) {
120048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                    Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
121048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                            + " falling back to fresh extraction", ioe);
122048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                    files = performExtractions(sourceApk, dexDir);
1239958145a97586375966bc133c900ead0e1550f2aYohann Roussel                    putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files);
124048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                }
125048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            } else {
126048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                Log.i(TAG, "Detected that extraction must be performed.");
127602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                files = performExtractions(sourceApk, dexDir);
1289958145a97586375966bc133c900ead0e1550f2aYohann Roussel                putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files);
129602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            }
130048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        } finally {
131048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            if (cacheLock != null) {
132048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                try {
133048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                    cacheLock.release();
134048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                } catch (IOException e) {
135048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                    Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
136048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                    // Exception while releasing the lock is bad, we want to report it, but not at
137048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                    // the price of overriding any already pending exception.
138048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                    releaseLockException = e;
139048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                }
140048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            }
141048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            if (lockChannel != null) {
142048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                closeQuietly(lockChannel);
143048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            }
144048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            closeQuietly(lockRaf);
145048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        }
146048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson
147048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        if (releaseLockException != null) {
148048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson            throw releaseLockException;
149602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        }
150602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
151602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        Log.i(TAG, "load found " + files.size() + " secondary dex files");
152602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        return files;
153602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    }
154602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
155048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson    /**
156048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     * Load previously extracted secondary dex files. Should be called only while owning the lock on
157048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     * {@link #LOCK_FILENAME}.
158048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     */
1599958145a97586375966bc133c900ead0e1550f2aYohann Roussel    private static List<ExtractedDex> loadExistingExtractions(
1609958145a97586375966bc133c900ead0e1550f2aYohann Roussel            Context context, File sourceApk, File dexDir)
161602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            throws IOException {
162602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        Log.i(TAG, "loading existing secondary dex files");
163602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
164602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
1659958145a97586375966bc133c900ead0e1550f2aYohann Roussel        SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
1669958145a97586375966bc133c900ead0e1550f2aYohann Roussel        int totalDexNumber = multiDexPreferences.getInt(KEY_DEX_NUMBER, 1);
1679958145a97586375966bc133c900ead0e1550f2aYohann Roussel        final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1);
168602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
169602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
170602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
1719958145a97586375966bc133c900ead0e1550f2aYohann Roussel            ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
172602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            if (extractedFile.isFile()) {
1739958145a97586375966bc133c900ead0e1550f2aYohann Roussel                extractedFile.crc = getZipCrc(extractedFile);
1749958145a97586375966bc133c900ead0e1550f2aYohann Roussel                long expectedCrc =
1759958145a97586375966bc133c900ead0e1550f2aYohann Roussel                        multiDexPreferences.getLong(KEY_DEX_CRC + secondaryNumber, NO_VALUE);
1769958145a97586375966bc133c900ead0e1550f2aYohann Roussel                long expectedModTime =
1779958145a97586375966bc133c900ead0e1550f2aYohann Roussel                        multiDexPreferences.getLong(KEY_DEX_TIME + secondaryNumber, NO_VALUE);
1789958145a97586375966bc133c900ead0e1550f2aYohann Roussel                long lastModified = extractedFile.lastModified();
1799958145a97586375966bc133c900ead0e1550f2aYohann Roussel                if ((expectedModTime != lastModified)
1809958145a97586375966bc133c900ead0e1550f2aYohann Roussel                        || (expectedCrc != extractedFile.crc)) {
1819958145a97586375966bc133c900ead0e1550f2aYohann Roussel                    throw new IOException("Invalid extracted dex: " + extractedFile +
1829958145a97586375966bc133c900ead0e1550f2aYohann Roussel                            ", expected modification time: "
1839958145a97586375966bc133c900ead0e1550f2aYohann Roussel                            + expectedModTime + ", modification time: "
1849958145a97586375966bc133c900ead0e1550f2aYohann Roussel                            + lastModified + ", expected crc: "
1859958145a97586375966bc133c900ead0e1550f2aYohann Roussel                            + expectedCrc + ", file crc: " + extractedFile.crc);
186602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                }
1879958145a97586375966bc133c900ead0e1550f2aYohann Roussel                files.add(extractedFile);
188602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            } else {
189602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                throw new IOException("Missing extracted secondary dex file '" +
190602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                        extractedFile.getPath() + "'");
191602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            }
192602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        }
193602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
194602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        return files;
195602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    }
196602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
197048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson
198048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson    /**
199048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     * Compare current archive and crc with values stored in {@link SharedPreferences}. Should be
200048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     * called only while owning the lock on {@link #LOCK_FILENAME}.
201048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     */
202602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    private static boolean isModified(Context context, File archive, long currentCrc) {
203602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        SharedPreferences prefs = getMultiDexPreferences(context);
204602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
205602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                || (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
206602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    }
207602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
208602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    private static long getTimeStamp(File archive) {
209602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        long timeStamp = archive.lastModified();
210602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        if (timeStamp == NO_VALUE) {
211602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            // never return NO_VALUE
212602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            timeStamp--;
213602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        }
214602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        return timeStamp;
215602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    }
216602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
217602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
218602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    private static long getZipCrc(File archive) throws IOException {
219602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        long computedValue = ZipUtil.getZipCrc(archive);
220602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        if (computedValue == NO_VALUE) {
221602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            // never return NO_VALUE
222602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            computedValue--;
223602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        }
224602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        return computedValue;
225602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    }
226602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
2279958145a97586375966bc133c900ead0e1550f2aYohann Roussel    private static List<ExtractedDex> performExtractions(File sourceApk, File dexDir)
228602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel            throws IOException {
229602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
2307e267a38525afac2a571da186e770a2b86a01846Maurice Chu        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
231667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
232994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu        // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
233994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu        // contains a secondary dex file in there is not consistent with the latest apk.  Otherwise,
234994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu        // multi-process race conditions can cause a crash loop where one process deletes the zip
235994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu        // while another had created it.
236994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu        prepareDexDir(dexDir, extractedFilePrefix);
237667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
2389958145a97586375966bc133c900ead0e1550f2aYohann Roussel        List<ExtractedDex> files = new ArrayList<ExtractedDex>();
2397e267a38525afac2a571da186e770a2b86a01846Maurice Chu
240602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        final ZipFile apk = new ZipFile(sourceApk);
241667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        try {
242667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
243667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            int secondaryNumber = 2;
244667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
245667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
246667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            while (dexFile != null) {
247667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
2489958145a97586375966bc133c900ead0e1550f2aYohann Roussel                ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
249667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                files.add(extractedFile);
250667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
251602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                Log.i(TAG, "Extraction is needed for file " + extractedFile);
252602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                int numAttempts = 0;
253602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                boolean isExtractionSuccessful = false;
254602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
255602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                    numAttempts++;
256602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
257602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                    // Create a zip file (extractedFile) containing only the secondary dex file
258602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                    // (dexFile) from the apk.
259602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                    extract(apk, dexFile, extractedFile, extractedFilePrefix);
260602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
2619958145a97586375966bc133c900ead0e1550f2aYohann Roussel                    // Read zip crc of extracted dex
2629958145a97586375966bc133c900ead0e1550f2aYohann Roussel                    try {
2639958145a97586375966bc133c900ead0e1550f2aYohann Roussel                        extractedFile.crc = getZipCrc(extractedFile);
2649958145a97586375966bc133c900ead0e1550f2aYohann Roussel                        isExtractionSuccessful = true;
2659958145a97586375966bc133c900ead0e1550f2aYohann Roussel                    } catch (IOException e) {
2669958145a97586375966bc133c900ead0e1550f2aYohann Roussel                        isExtractionSuccessful = false;
2679958145a97586375966bc133c900ead0e1550f2aYohann Roussel                        Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e);
2689958145a97586375966bc133c900ead0e1550f2aYohann Roussel                    }
269602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel
2709958145a97586375966bc133c900ead0e1550f2aYohann Roussel                    // Log size and crc of the extracted zip file
2719958145a97586375966bc133c900ead0e1550f2aYohann Roussel                    Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") +
272602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                            " - length " + extractedFile.getAbsolutePath() + ": " +
2739958145a97586375966bc133c900ead0e1550f2aYohann Roussel                            extractedFile.length() + " - crc: " + extractedFile.crc);
274602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                    if (!isExtractionSuccessful) {
275602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                        // Delete the extracted file
276602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                        extractedFile.delete();
277602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                        if (extractedFile.exists()) {
278602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                            Log.w(TAG, "Failed to delete corrupted secondary dex '" +
279602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                                    extractedFile.getPath() + "'");
280f6d1f23926672c8dd61da515f8d1bcb37ef4292dMaurice Chu                        }
281f6d1f23926672c8dd61da515f8d1bcb37ef4292dMaurice Chu                    }
282602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                }
283602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                if (!isExtractionSuccessful) {
284602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                    throw new IOException("Could not create zip file " +
285602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                            extractedFile.getAbsolutePath() + " for secondary dex (" +
286602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel                            secondaryNumber + ")");
287667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                }
288667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                secondaryNumber++;
289667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
290667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            }
291667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        } finally {
292667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            try {
293667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                apk.close();
294667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            } catch (IOException e) {
295667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                Log.w(TAG, "Failed to close resource", e);
296667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            }
297667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        }
298667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
299667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        return files;
300667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    }
301667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
302048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson    /**
303048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     * Save {@link SharedPreferences}. Should be called only while owning the lock on
304048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     * {@link #LOCK_FILENAME}.
305048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     */
306602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel    private static void putStoredApkInfo(Context context, long timeStamp, long crc,
3079958145a97586375966bc133c900ead0e1550f2aYohann Roussel            List<ExtractedDex> extractedDexes) {
308994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu        SharedPreferences prefs = getMultiDexPreferences(context);
3097e267a38525afac2a571da186e770a2b86a01846Maurice Chu        SharedPreferences.Editor edit = prefs.edit();
310602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        edit.putLong(KEY_TIME_STAMP, timeStamp);
311602c6ca8cae4718ba8ff9f65e53305d002479359Yohann Roussel        edit.putLong(KEY_CRC, crc);
3129958145a97586375966bc133c900ead0e1550f2aYohann Roussel        edit.putInt(KEY_DEX_NUMBER, extractedDexes.size() + 1);
3139958145a97586375966bc133c900ead0e1550f2aYohann Roussel
3149958145a97586375966bc133c900ead0e1550f2aYohann Roussel        int extractedDexId = 2;
3159958145a97586375966bc133c900ead0e1550f2aYohann Roussel        for (ExtractedDex dex : extractedDexes) {
3169958145a97586375966bc133c900ead0e1550f2aYohann Roussel            edit.putLong(KEY_DEX_CRC + extractedDexId, dex.crc);
3179958145a97586375966bc133c900ead0e1550f2aYohann Roussel            edit.putLong(KEY_DEX_TIME + extractedDexId, dex.lastModified());
3189958145a97586375966bc133c900ead0e1550f2aYohann Roussel            extractedDexId++;
3199958145a97586375966bc133c900ead0e1550f2aYohann Roussel        }
320048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        /* Use commit() and not apply() as advised by the doc because we need synchronous writing of
321048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson         * the editor content and apply is doing an "asynchronous commit to disk".
322048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson         */
323048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson        edit.commit();
3247e267a38525afac2a571da186e770a2b86a01846Maurice Chu    }
3257e267a38525afac2a571da186e770a2b86a01846Maurice Chu
326048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson    /**
327048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     * Get the MuliDex {@link SharedPreferences} for the current application. Should be called only
328048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     * while owning the lock on {@link #LOCK_FILENAME}.
329048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     */
330994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu    private static SharedPreferences getMultiDexPreferences(Context context) {
331994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu        return context.getSharedPreferences(PREFS_FILE,
332e99daea7a3aec5ffac13b4283685e8d2a5994ad9Yohann Roussel                Build.VERSION.SDK_INT < 11 /* Build.VERSION_CODES.HONEYCOMB */
333994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu                        ? Context.MODE_PRIVATE
334e99daea7a3aec5ffac13b4283685e8d2a5994ad9Yohann Roussel                        : Context.MODE_PRIVATE | 0x0004 /* Context.MODE_MULTI_PROCESS */);
335994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu    }
336994fa84b3af07700fd5dcd477c02a6bd824dde45Maurice Chu
3377e267a38525afac2a571da186e770a2b86a01846Maurice Chu    /**
338048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson     * This removes old files.
339cc63eda4e6defe6b0dd5dd3c8fa608cf6ff26011Maurice Chu     */
340606af94785cb96d418d87fe5a90bb2e09ccfa97fYohann Roussel    private static void prepareDexDir(File dexDir, final String extractedFilePrefix) {
341d9eda5550540306f2037e2db2aba2919fda90057Yohann Roussel        FileFilter filter = new FileFilter() {
342d9eda5550540306f2037e2db2aba2919fda90057Yohann Roussel
343667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            @Override
344d9eda5550540306f2037e2db2aba2919fda90057Yohann Roussel            public boolean accept(File pathname) {
345048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                String name = pathname.getName();
346048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                return !(name.startsWith(extractedFilePrefix)
347048fbf7ccf53782a265c277df38c273d43e5450eAndrew Johnson                        || name.equals(LOCK_FILENAME));
348667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            }
349667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        };
350667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        File[] files = dexDir.listFiles(filter);
351667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        if (files == null) {
352667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
353667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            return;
354667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        }
355667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        for (File oldFile : files) {
356d79604bd38c101b54e41745f85ddc2e04d978af2Yohann Roussel            Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
3577e267a38525afac2a571da186e770a2b86a01846Maurice Chu                    oldFile.length());
358667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            if (!oldFile.delete()) {
359667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
36088117c37e2c3f8601f295b74a3e804877afb78eeYohann Roussel            } else {
361d79604bd38c101b54e41745f85ddc2e04d978af2Yohann Roussel                Log.i(TAG, "Deleted old file " + oldFile.getPath());
362667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            }
363667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        }
364667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    }
365667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
36652eafa01b1e61e410cb4c5609eacee93c2a3e853Yohann Roussel    private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
3677e267a38525afac2a571da186e770a2b86a01846Maurice Chu            String extractedFilePrefix) throws IOException, FileNotFoundException {
368667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
369667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        InputStream in = apk.getInputStream(dexFile);
370667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        ZipOutputStream out = null;
3714b56ee2bb0401ccf35bdecaaf86b2212e98db919Jon Noack        // Temp files must not start with extractedFilePrefix to get cleaned up in prepareDexDir()
3724b56ee2bb0401ccf35bdecaaf86b2212e98db919Jon Noack        File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX,
373667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                extractTo.getParentFile());
374667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        Log.i(TAG, "Extracting " + tmp.getPath());
375667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        try {
376a159fd5dbd60171c9bb602f316ad8272419afe40Maurice Chu            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
377667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            try {
378667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                ZipEntry classesDex = new ZipEntry("classes.dex");
379edf0717d4203bd7e9c9435019e3c256d564b4583Yohann Roussel                // keep zip entry time since it is the criteria used by Dalvik
380edf0717d4203bd7e9c9435019e3c256d564b4583Yohann Roussel                classesDex.setTime(dexFile.getTime());
381667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                out.putNextEntry(classesDex);
382667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
383667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                byte[] buffer = new byte[BUFFER_SIZE];
384667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                int length = in.read(buffer);
3851f8c349b6524aa39a10a570115ce0afb039bd06fMaurice Chu                while (length != -1) {
38652eafa01b1e61e410cb4c5609eacee93c2a3e853Yohann Roussel                    out.write(buffer, 0, length);
387667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                    length = in.read(buffer);
388667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu                }
389a159fd5dbd60171c9bb602f316ad8272419afe40Maurice Chu                out.closeEntry();
390667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            } finally {
391a0c1a85f60a44ef1c0592821270c20e988c64370Maurice Chu                out.close();
392667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            }
393f5832474ebd6ec48f0f37bc1fd9e2576068e4ef2Yohann Roussel            if (!tmp.setReadOnly()) {
394f5832474ebd6ec48f0f37bc1fd9e2576068e4ef2Yohann Roussel                throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() +
395f5832474ebd6ec48f0f37bc1fd9e2576068e4ef2Yohann Roussel                        "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
396f5832474ebd6ec48f0f37bc1fd9e2576068e4ef2Yohann Roussel            }
397667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            Log.i(TAG, "Renaming to " + extractTo.getPath());
398a159fd5dbd60171c9bb602f316ad8272419afe40Maurice Chu            if (!tmp.renameTo(extractTo)) {
399a159fd5dbd60171c9bb602f316ad8272419afe40Maurice Chu                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
400a159fd5dbd60171c9bb602f316ad8272419afe40Maurice Chu                        "\" to \"" + extractTo.getAbsolutePath() + "\"");
401667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            }
402667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        } finally {
403667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            closeQuietly(in);
404667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            tmp.delete(); // return status ignored
405667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        }
406667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    }
407667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu
408667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    /**
409667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     * Closes the given {@code Closeable}. Suppresses any IO exceptions.
410667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu     */
411667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    private static void closeQuietly(Closeable closeable) {
412667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        try {
413667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            closeable.close();
414667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        } catch (IOException e) {
415667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu            Log.w(TAG, "Failed to close resource", e);
416667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu        }
417667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu    }
418667f9a8a8155c41970a83be1414b57b5e37de336Maurice Chu}
419