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