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