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