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