MultiDexExtractor.java revision 88117c37e2c3f8601f295b74a3e804877afb78ee
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) 68 throws IOException { 69 Log.i(TAG, "load(" + applicationInfo.sourceDir + ")"); 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 (!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 private static void prepareDexDir(File dexDir, final String extractedFilePrefix, 137 final long sourceLastModified) throws IOException { 138 dexDir.mkdir(); 139 if (!dexDir.isDirectory()) { 140 throw new IOException("Failed to create dex directory " + dexDir.getPath()); 141 } 142 143 // Clean possible old files 144 FileFilter filter = new FileFilter() { 145 146 @Override 147 public boolean accept(File pathname) { 148 return (!pathname.getName().startsWith(extractedFilePrefix)) 149 || (pathname.lastModified() < sourceLastModified); 150 } 151 }; 152 File[] files = dexDir.listFiles(filter); 153 if (files == null) { 154 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); 155 return; 156 } 157 for (File oldFile : files) { 158 Log.w(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length()); 159 if (!oldFile.delete()) { 160 Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); 161 } else { 162 Log.w(TAG, "Deleted old file " + oldFile.getPath()); 163 } 164 } 165 } 166 167 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, 168 String extractedFilePrefix, long sourceLastModified) 169 throws IOException, FileNotFoundException { 170 171 InputStream in = apk.getInputStream(dexFile); 172 ZipOutputStream out = null; 173 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX, 174 extractTo.getParentFile()); 175 Log.i(TAG, "Extracting " + tmp.getPath()); 176 try { 177 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); 178 try { 179 ZipEntry classesDex = new ZipEntry("classes.dex"); 180 // keep zip entry time since it is the criteria used by Dalvik 181 classesDex.setTime(dexFile.getTime()); 182 out.putNextEntry(classesDex); 183 184 byte[] buffer = new byte[BUFFER_SIZE]; 185 int length = in.read(buffer); 186 while (length != -1) { 187 out.write(buffer, 0, length); 188 length = in.read(buffer); 189 } 190 out.closeEntry(); 191 } finally { 192 out.close(); 193 } 194 if (!tmp.setLastModified(sourceLastModified)) { 195 Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." + 196 " This may cause problems with later updates of the apk."); 197 } 198 Log.i(TAG, "Renaming to " + extractTo.getPath()); 199 if (!tmp.renameTo(extractTo)) { 200 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + 201 "\" to \"" + extractTo.getAbsolutePath() + "\""); 202 } 203 } finally { 204 closeQuietly(in); 205 tmp.delete(); // return status ignored 206 } 207 } 208 209 /** 210 * Returns whether the file is a valid zip file. 211 */ 212 private static boolean verifyZipFile(File file) { 213 try { 214 ZipFile zipFile = new ZipFile(file); 215 try { 216 zipFile.close(); 217 return true; 218 } catch (IOException e) { 219 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath()); 220 } 221 } catch (ZipException ex) { 222 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex); 223 } catch (IOException ex) { 224 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex); 225 } 226 return false; 227 } 228 229 /** 230 * Closes the given {@code Closeable}. Suppresses any IO exceptions. 231 */ 232 private static void closeQuietly(Closeable closeable) { 233 try { 234 closeable.close(); 235 } catch (IOException e) { 236 Log.w(TAG, "Failed to close resource", e); 237 } 238 } 239} 240