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 
17 package androidx.multidex;
18 
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.os.Build;
22 import android.util.Log;
23 
24 import java.io.BufferedOutputStream;
25 import java.io.Closeable;
26 import java.io.File;
27 import java.io.FileFilter;
28 import java.io.FileNotFoundException;
29 import java.io.FileOutputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.RandomAccessFile;
33 import java.nio.channels.FileChannel;
34 import java.nio.channels.FileLock;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.zip.ZipEntry;
38 import java.util.zip.ZipFile;
39 import java.util.zip.ZipOutputStream;
40 
41 /**
42  * Exposes application secondary dex files as files in the application data
43  * directory.
44  * {@link MultiDexExtractor} is taking the file lock in the dex dir on creation and release it
45  * during close.
46  */
47 final class MultiDexExtractor implements Closeable {
48 
49     /**
50      * Zip file containing one secondary dex file.
51      */
52     private static class ExtractedDex extends File {
53         public long crc = NO_VALUE;
54 
ExtractedDex(File dexDir, String fileName)55         public ExtractedDex(File dexDir, String fileName) {
56             super(dexDir, fileName);
57         }
58     }
59 
60     private static final String TAG = MultiDex.TAG;
61 
62     /**
63      * We look for additional dex files named {@code classes2.dex},
64      * {@code classes3.dex}, etc.
65      */
66     private static final String DEX_PREFIX = "classes";
67     static final String DEX_SUFFIX = ".dex";
68 
69     private static final String EXTRACTED_NAME_EXT = ".classes";
70     static final String EXTRACTED_SUFFIX = ".zip";
71     private static final int MAX_EXTRACT_ATTEMPTS = 3;
72 
73     private static final String PREFS_FILE = "multidex.version";
74     private static final String KEY_TIME_STAMP = "timestamp";
75     private static final String KEY_CRC = "crc";
76     private static final String KEY_DEX_NUMBER = "dex.number";
77     private static final String KEY_DEX_CRC = "dex.crc.";
78     private static final String KEY_DEX_TIME = "dex.time.";
79 
80     /**
81      * Size of reading buffers.
82      */
83     private static final int BUFFER_SIZE = 0x4000;
84     /* Keep value away from 0 because it is a too probable time stamp value */
85     private static final long NO_VALUE = -1L;
86 
87     private static final String LOCK_FILENAME = "MultiDex.lock";
88     private final File sourceApk;
89     private final long sourceCrc;
90     private final File dexDir;
91     private final RandomAccessFile lockRaf;
92     private final FileChannel lockChannel;
93     private final FileLock cacheLock;
94 
MultiDexExtractor(File sourceApk, File dexDir)95     MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
96         Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")");
97         this.sourceApk = sourceApk;
98         this.dexDir = dexDir;
99         sourceCrc = getZipCrc(sourceApk);
100         File lockFile = new File(dexDir, LOCK_FILENAME);
101         lockRaf = new RandomAccessFile(lockFile, "rw");
102         try {
103             lockChannel = lockRaf.getChannel();
104             try {
105                 Log.i(TAG, "Blocking on lock " + lockFile.getPath());
106                 cacheLock = lockChannel.lock();
107             } catch (IOException | RuntimeException | Error e) {
108                 closeQuietly(lockChannel);
109                 throw e;
110             }
111             Log.i(TAG, lockFile.getPath() + " locked");
112         } catch (IOException | RuntimeException | Error e) {
113             closeQuietly(lockRaf);
114             throw e;
115         }
116     }
117 
118     /**
119      * Extracts application secondary dexes into files in the application data
120      * directory.
121      *
122      * @return a list of files that were created. The list may be empty if there
123      *         are no secondary dex files. Never return null.
124      * @throws IOException if encounters a problem while reading or writing
125      *         secondary dex files
126      */
load(Context context, String prefsKeyPrefix, boolean forceReload)127     List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
128             throws IOException {
129         Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +
130                 prefsKeyPrefix + ")");
131 
132         if (!cacheLock.isValid()) {
133             throw new IllegalStateException("MultiDexExtractor was closed");
134         }
135 
136         List<ExtractedDex> files;
137         if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
138             try {
139                 files = loadExistingExtractions(context, prefsKeyPrefix);
140             } catch (IOException ioe) {
141                 Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
142                         + " falling back to fresh extraction", ioe);
143                 files = performExtractions();
144                 putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
145                         files);
146             }
147         } else {
148             if (forceReload) {
149                 Log.i(TAG, "Forced extraction must be performed.");
150             } else {
151                 Log.i(TAG, "Detected that extraction must be performed.");
152             }
153             files = performExtractions();
154             putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
155                     files);
156         }
157 
158         Log.i(TAG, "load found " + files.size() + " secondary dex files");
159         return files;
160     }
161 
162     @Override
close()163     public void close() throws IOException {
164         cacheLock.release();
165         lockChannel.close();
166         lockRaf.close();
167     }
168 
169     /**
170      * Load previously extracted secondary dex files. Should be called only while owning the lock on
171      * {@link #LOCK_FILENAME}.
172      */
loadExistingExtractions( Context context, String prefsKeyPrefix)173     private List<ExtractedDex> loadExistingExtractions(
174             Context context,
175             String prefsKeyPrefix)
176             throws IOException {
177         Log.i(TAG, "loading existing secondary dex files");
178 
179         final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
180         SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
181         int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 0);
182         if (totalDexNumber < 1) {
183             // Guard against SharedPreferences corruption
184             throw new IOException("Invalid dex number: " + totalDexNumber);
185         }
186         final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1);
187 
188         for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
189             String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
190             ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
191             if (extractedFile.isFile()) {
192                 extractedFile.crc = getZipCrc(extractedFile);
193                 long expectedCrc = multiDexPreferences.getLong(
194                         prefsKeyPrefix + KEY_DEX_CRC + secondaryNumber, NO_VALUE);
195                 long expectedModTime = multiDexPreferences.getLong(
196                         prefsKeyPrefix + KEY_DEX_TIME + secondaryNumber, NO_VALUE);
197                 long lastModified = extractedFile.lastModified();
198                 if ((expectedModTime != lastModified)
199                         || (expectedCrc != extractedFile.crc)) {
200                     throw new IOException("Invalid extracted dex: " + extractedFile +
201                             " (key \"" + prefsKeyPrefix + "\"), expected modification time: "
202                             + expectedModTime + ", modification time: "
203                             + lastModified + ", expected crc: "
204                             + expectedCrc + ", file crc: " + extractedFile.crc);
205                 }
206                 files.add(extractedFile);
207             } else {
208                 throw new IOException("Missing extracted secondary dex file '" +
209                         extractedFile.getPath() + "'");
210             }
211         }
212 
213         return files;
214     }
215 
216 
217     /**
218      * Compare current archive and crc with values stored in {@link SharedPreferences}. Should be
219      * called only while owning the lock on {@link #LOCK_FILENAME}.
220      */
isModified(Context context, File archive, long currentCrc, String prefsKeyPrefix)221     private static boolean isModified(Context context, File archive, long currentCrc,
222             String prefsKeyPrefix) {
223         SharedPreferences prefs = getMultiDexPreferences(context);
224         return (prefs.getLong(prefsKeyPrefix + KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
225                 || (prefs.getLong(prefsKeyPrefix + KEY_CRC, NO_VALUE) != currentCrc);
226     }
227 
getTimeStamp(File archive)228     private static long getTimeStamp(File archive) {
229         long timeStamp = archive.lastModified();
230         if (timeStamp == NO_VALUE) {
231             // never return NO_VALUE
232             timeStamp--;
233         }
234         return timeStamp;
235     }
236 
237 
getZipCrc(File archive)238     private static long getZipCrc(File archive) throws IOException {
239         long computedValue = ZipUtil.getZipCrc(archive);
240         if (computedValue == NO_VALUE) {
241             // never return NO_VALUE
242             computedValue--;
243         }
244         return computedValue;
245     }
246 
performExtractions()247     private List<ExtractedDex> performExtractions() throws IOException {
248 
249         final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
250 
251         // It is safe to fully clear the dex dir because we own the file lock so no other process is
252         // extracting or running optimizing dexopt. It may cause crash of already running
253         // applications if for whatever reason we end up extracting again over a valid extraction.
254         clearDexDir();
255 
256         List<ExtractedDex> files = new ArrayList<ExtractedDex>();
257 
258         final ZipFile apk = new ZipFile(sourceApk);
259         try {
260 
261             int secondaryNumber = 2;
262 
263             ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
264             while (dexFile != null) {
265                 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
266                 ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
267                 files.add(extractedFile);
268 
269                 Log.i(TAG, "Extraction is needed for file " + extractedFile);
270                 int numAttempts = 0;
271                 boolean isExtractionSuccessful = false;
272                 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
273                     numAttempts++;
274 
275                     // Create a zip file (extractedFile) containing only the secondary dex file
276                     // (dexFile) from the apk.
277                     extract(apk, dexFile, extractedFile, extractedFilePrefix);
278 
279                     // Read zip crc of extracted dex
280                     try {
281                         extractedFile.crc = getZipCrc(extractedFile);
282                         isExtractionSuccessful = true;
283                     } catch (IOException e) {
284                         isExtractionSuccessful = false;
285                         Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e);
286                     }
287 
288                     // Log size and crc of the extracted zip file
289                     Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
290                             + " '" + extractedFile.getAbsolutePath() + "': length "
291                             + extractedFile.length() + " - crc: " + extractedFile.crc);
292                     if (!isExtractionSuccessful) {
293                         // Delete the extracted file
294                         extractedFile.delete();
295                         if (extractedFile.exists()) {
296                             Log.w(TAG, "Failed to delete corrupted secondary dex '" +
297                                     extractedFile.getPath() + "'");
298                         }
299                     }
300                 }
301                 if (!isExtractionSuccessful) {
302                     throw new IOException("Could not create zip file " +
303                             extractedFile.getAbsolutePath() + " for secondary dex (" +
304                             secondaryNumber + ")");
305                 }
306                 secondaryNumber++;
307                 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
308             }
309         } finally {
310             try {
311                 apk.close();
312             } catch (IOException e) {
313                 Log.w(TAG, "Failed to close resource", e);
314             }
315         }
316 
317         return files;
318     }
319 
320     /**
321      * Save {@link SharedPreferences}. Should be called only while owning the lock on
322      * {@link #LOCK_FILENAME}.
323      */
putStoredApkInfo(Context context, String keyPrefix, long timeStamp, long crc, List<ExtractedDex> extractedDexes)324     private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp,
325             long crc, List<ExtractedDex> extractedDexes) {
326         SharedPreferences prefs = getMultiDexPreferences(context);
327         SharedPreferences.Editor edit = prefs.edit();
328         edit.putLong(keyPrefix + KEY_TIME_STAMP, timeStamp);
329         edit.putLong(keyPrefix + KEY_CRC, crc);
330         edit.putInt(keyPrefix + KEY_DEX_NUMBER, extractedDexes.size() + 1);
331 
332         int extractedDexId = 2;
333         for (ExtractedDex dex : extractedDexes) {
334             edit.putLong(keyPrefix + KEY_DEX_CRC + extractedDexId, dex.crc);
335             edit.putLong(keyPrefix + KEY_DEX_TIME + extractedDexId, dex.lastModified());
336             extractedDexId++;
337         }
338         /* Use commit() and not apply() as advised by the doc because we need synchronous writing of
339          * the editor content and apply is doing an "asynchronous commit to disk".
340          */
341         edit.commit();
342     }
343 
344     /**
345      * Get the MuliDex {@link SharedPreferences} for the current application. Should be called only
346      * while owning the lock on {@link #LOCK_FILENAME}.
347      */
getMultiDexPreferences(Context context)348     private static SharedPreferences getMultiDexPreferences(Context context) {
349         return context.getSharedPreferences(PREFS_FILE,
350                 Build.VERSION.SDK_INT < 11 /* Build.VERSION_CODES.HONEYCOMB */
351                         ? Context.MODE_PRIVATE
352                         : Context.MODE_PRIVATE | 0x0004 /* Context.MODE_MULTI_PROCESS */);
353     }
354 
355     /**
356      * Clear the dex dir from all files but the lock.
357      */
clearDexDir()358     private void clearDexDir() {
359         File[] files = dexDir.listFiles(new FileFilter() {
360             @Override
361             public boolean accept(File pathname) {
362                 return !pathname.getName().equals(LOCK_FILENAME);
363             }
364         });
365         if (files == null) {
366             Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
367             return;
368         }
369         for (File oldFile : files) {
370             Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
371                     oldFile.length());
372             if (!oldFile.delete()) {
373                 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
374             } else {
375                 Log.i(TAG, "Deleted old file " + oldFile.getPath());
376             }
377         }
378     }
379 
extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix)380     private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
381             String extractedFilePrefix) throws IOException, FileNotFoundException {
382 
383         InputStream in = apk.getInputStream(dexFile);
384         ZipOutputStream out = null;
385         // Temp files must not start with extractedFilePrefix to get cleaned up in prepareDexDir()
386         File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX,
387                 extractTo.getParentFile());
388         Log.i(TAG, "Extracting " + tmp.getPath());
389         try {
390             out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
391             try {
392                 ZipEntry classesDex = new ZipEntry("classes.dex");
393                 // keep zip entry time since it is the criteria used by Dalvik
394                 classesDex.setTime(dexFile.getTime());
395                 out.putNextEntry(classesDex);
396 
397                 byte[] buffer = new byte[BUFFER_SIZE];
398                 int length = in.read(buffer);
399                 while (length != -1) {
400                     out.write(buffer, 0, length);
401                     length = in.read(buffer);
402                 }
403                 out.closeEntry();
404             } finally {
405                 out.close();
406             }
407             if (!tmp.setReadOnly()) {
408                 throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() +
409                         "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
410             }
411             Log.i(TAG, "Renaming to " + extractTo.getPath());
412             if (!tmp.renameTo(extractTo)) {
413                 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
414                         "\" to \"" + extractTo.getAbsolutePath() + "\"");
415             }
416         } finally {
417             closeQuietly(in);
418             tmp.delete(); // return status ignored
419         }
420     }
421 
422     /**
423      * Closes the given {@code Closeable}. Suppresses any IO exceptions.
424      */
closeQuietly(Closeable closeable)425     private static void closeQuietly(Closeable closeable) {
426         try {
427             closeable.close();
428         } catch (IOException e) {
429             Log.w(TAG, "Failed to close resource", e);
430         }
431     }
432 }
433