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