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