MultiDexExtractor.java revision cc63eda4e6defe6b0dd5dd3c8fa608cf6ff26011
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.pm.ApplicationInfo; 20import android.util.Log; 21 22import java.io.BufferedOutputStream; 23import java.io.Closeable; 24import java.io.File; 25import java.io.FileFilter; 26import java.io.FileNotFoundException; 27import java.io.FileOutputStream; 28import java.io.IOException; 29import java.io.InputStream; 30import java.util.ArrayList; 31import java.util.List; 32import java.util.zip.ZipEntry; 33import java.util.zip.ZipException; 34import java.util.zip.ZipFile; 35import java.util.zip.ZipOutputStream; 36 37/** 38 * Exposes application secondary dex files as files in the application data 39 * directory. 40 */ 41final class MultiDexExtractor { 42 43 private static final String TAG = MultiDex.TAG; 44 45 /** 46 * We look for additional dex files named {@code classes2.dex}, 47 * {@code classes3.dex}, etc. 48 */ 49 private static final String DEX_PREFIX = "classes"; 50 private static final String DEX_SUFFIX = ".dex"; 51 52 private static final String EXTRACTED_NAME_EXT = ".classes"; 53 private static final String EXTRACTED_SUFFIX = ".zip"; 54 private static final int MAX_EXTRACT_ATTEMPTS = 3; 55 56 private static final int BUFFER_SIZE = 0x4000; 57 58 /** 59 * Extracts application secondary dexes into files in the application data 60 * directory. 61 * 62 * @return a list of files that were created. The list may be empty if there 63 * are no secondary dex files. 64 * @throws IOException if encounters a problem while reading or writing 65 * secondary dex files 66 */ 67 static List<File> load(ApplicationInfo applicationInfo, File dexDir, boolean forceReload) 68 throws IOException { 69 Log.i(TAG, "load(" + applicationInfo.sourceDir + ", forceReload=" + forceReload + ")"); 70 File sourceApk = new File(applicationInfo.sourceDir); 71 long lastModified = sourceApk.lastModified(); 72 String extractedFilePrefix = sourceApk.getName() 73 + EXTRACTED_NAME_EXT; 74 75 prepareDexDir(dexDir, extractedFilePrefix, lastModified); 76 77 final List<File> files = new ArrayList<File>(); 78 ZipFile apk = new ZipFile(applicationInfo.sourceDir); 79 try { 80 81 int secondaryNumber = 2; 82 83 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 84 while (dexFile != null) { 85 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; 86 File extractedFile = new File(dexDir, fileName); 87 files.add(extractedFile); 88 89 Log.i(TAG, "Need extracted file " + extractedFile); 90 if (forceReload || !extractedFile.isFile()) { 91 Log.i(TAG, "Extraction is needed for file " + extractedFile); 92 int numAttempts = 0; 93 boolean isExtractionSuccessful = false; 94 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) { 95 numAttempts++; 96 97 // Create a zip file (extractedFile) containing only the secondary dex file 98 // (dexFile) from the apk. 99 extract(apk, dexFile, extractedFile, extractedFilePrefix, 100 lastModified); 101 102 // Verify that the extracted file is indeed a zip file. 103 isExtractionSuccessful = verifyZipFile(extractedFile); 104 105 // Log the sha1 of the extracted zip file 106 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") + 107 " - length " + extractedFile.getAbsolutePath() + ": " + 108 extractedFile.length()); 109 if (!isExtractionSuccessful) { 110 // Delete the extracted file 111 extractedFile.delete(); 112 } 113 } 114 if (!isExtractionSuccessful) { 115 throw new IOException("Could not create zip file " + 116 extractedFile.getAbsolutePath() + " for secondary dex (" + 117 secondaryNumber + ")"); 118 } 119 } else { 120 Log.i(TAG, "No extraction needed for " + extractedFile + " of size " + extractedFile.length()); 121 } 122 secondaryNumber++; 123 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 124 } 125 } finally { 126 try { 127 apk.close(); 128 } catch (IOException e) { 129 Log.w(TAG, "Failed to close resource", e); 130 } 131 } 132 133 return files; 134 } 135 136 /** 137 * This removes any old zip and dex files or extraneous files from the secondary-dexes 138 * directory. 139 */ 140 private static void prepareDexDir(File dexDir, final String extractedFilePrefix, 141 final long sourceLastModified) throws IOException { 142 dexDir.mkdir(); 143 if (!dexDir.isDirectory()) { 144 throw new IOException("Failed to create dex directory " + dexDir.getPath()); 145 } 146 147 // Clean possible old files 148 FileFilter filter = new FileFilter() { 149 150 @Override 151 public boolean accept(File pathname) { 152 return (!pathname.getName().startsWith(extractedFilePrefix)) 153 || (pathname.lastModified() < sourceLastModified); 154 } 155 }; 156 File[] files = dexDir.listFiles(filter); 157 if (files == null) { 158 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); 159 return; 160 } 161 for (File oldFile : files) { 162 Log.w(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length()); 163 if (!oldFile.delete()) { 164 Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); 165 } else { 166 Log.w(TAG, "Deleted old file " + oldFile.getPath()); 167 } 168 } 169 } 170 171 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, 172 String extractedFilePrefix, long sourceLastModified) 173 throws IOException, FileNotFoundException { 174 175 InputStream in = apk.getInputStream(dexFile); 176 ZipOutputStream out = null; 177 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX, 178 extractTo.getParentFile()); 179 Log.i(TAG, "Extracting " + tmp.getPath()); 180 try { 181 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); 182 try { 183 ZipEntry classesDex = new ZipEntry("classes.dex"); 184 // keep zip entry time since it is the criteria used by Dalvik 185 classesDex.setTime(dexFile.getTime()); 186 out.putNextEntry(classesDex); 187 188 byte[] buffer = new byte[BUFFER_SIZE]; 189 int length = in.read(buffer); 190 while (length != -1) { 191 out.write(buffer, 0, length); 192 length = in.read(buffer); 193 } 194 out.closeEntry(); 195 } finally { 196 out.close(); 197 } 198 if (!tmp.setLastModified(sourceLastModified)) { 199 Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." + 200 " This may cause problems with later updates of the apk."); 201 } 202 Log.i(TAG, "Renaming to " + extractTo.getPath()); 203 if (!tmp.renameTo(extractTo)) { 204 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + 205 "\" to \"" + extractTo.getAbsolutePath() + "\""); 206 } 207 } finally { 208 closeQuietly(in); 209 tmp.delete(); // return status ignored 210 } 211 } 212 213 /** 214 * Returns whether the file is a valid zip file. 215 */ 216 static boolean verifyZipFile(File file) { 217 try { 218 ZipFile zipFile = new ZipFile(file); 219 try { 220 zipFile.close(); 221 return true; 222 } catch (IOException e) { 223 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath()); 224 } 225 } catch (ZipException ex) { 226 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex); 227 } catch (IOException ex) { 228 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex); 229 } 230 return false; 231 } 232 233 /** 234 * Closes the given {@code Closeable}. Suppresses any IO exceptions. 235 */ 236 private static void closeQuietly(Closeable closeable) { 237 try { 238 closeable.close(); 239 } catch (IOException e) { 240 Log.w(TAG, "Failed to close resource", e); 241 } 242 } 243} 244