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