MultiDexExtractor.java revision f6d1f23926672c8dd61da515f8d1bcb37ef4292d
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.FileInputStream; 26import java.io.FileNotFoundException; 27import java.io.FileOutputStream; 28import java.io.IOException; 29import java.io.InputStream; 30import java.security.MessageDigest; 31import java.security.NoSuchAlgorithmException; 32import java.util.ArrayList; 33import java.util.List; 34import java.util.zip.ZipEntry; 35import java.util.zip.ZipException; 36import java.util.zip.ZipFile; 37import java.util.zip.ZipOutputStream; 38 39/** 40 * Exposes application secondary dex files as files in the application data 41 * directory. 42 */ 43final class MultiDexExtractor { 44 45 private static final String TAG = MultiDex.TAG; 46 47 /** 48 * We look for additional dex files named {@code classes2.dex}, 49 * {@code classes3.dex}, etc. 50 */ 51 private static final String DEX_PREFIX = "classes"; 52 private static final String DEX_SUFFIX = ".dex"; 53 54 private static final String EXTRACTED_NAME_EXT = ".classes"; 55 private static final String EXTRACTED_SUFFIX = ".zip"; 56 private static final int MAX_EXTRACT_ATTEMPTS = 3; 57 private static final int MAX_ATTEMPTS_NO_SUCH_ALGORITHM = 2; 58 59 60 private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 61 'A', 'B', 'C', 'D', 'E', 'F' }; 62 63 private static final int BUFFER_SIZE = 0x4000; 64 65 /** 66 * Extracts application secondary dexes into files in the application data 67 * directory. 68 * 69 * @return a list of files that were created. The list may be empty if there 70 * are no secondary dex files. 71 * @throws IOException if encounters a problem while reading or writing 72 * secondary dex files 73 */ 74 static List<File> load(ApplicationInfo applicationInfo, File dexDir) 75 throws IOException { 76 77 File sourceApk = new File(applicationInfo.sourceDir); 78 long lastModified = sourceApk.lastModified(); 79 String extractedFilePrefix = sourceApk.getName() 80 + EXTRACTED_NAME_EXT; 81 82 prepareDexDir(dexDir, extractedFilePrefix, lastModified); 83 84 final List<File> files = new ArrayList<File>(); 85 ZipFile apk = new ZipFile(applicationInfo.sourceDir); 86 try { 87 88 int secondaryNumber = 2; 89 90 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 91 while (dexFile != null) { 92 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; 93 File extractedFile = new File(dexDir, fileName); 94 files.add(extractedFile); 95 96 if (!extractedFile.isFile()) { 97 int numAttempts = 0; 98 boolean isExtractionSuccessful = false; 99 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) { 100 numAttempts++; 101 102 // Create a zip file (extractedFile) containing only the secondary dex file 103 // (dexFile) from the apk. 104 extract(apk, dexFile, extractedFile, extractedFilePrefix, 105 lastModified); 106 107 // Verify that the extracted file is indeed a zip file. 108 isExtractionSuccessful = verifyZipFile(extractedFile); 109 110 // Log the sha1 of the extracted zip file 111 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") + 112 " - SHA1 of " + extractedFile.getAbsolutePath() + ": " + 113 computeSha1Digest(extractedFile)); 114 if (!isExtractionSuccessful) { 115 // Delete the extracted file 116 extractedFile.delete(); 117 } 118 } 119 if (!isExtractionSuccessful) { 120 throw new IOException("Could not create zip file " + 121 extractedFile.getAbsolutePath() + " for secondary dex (" + 122 secondaryNumber + ")"); 123 } 124 } 125 secondaryNumber++; 126 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 127 } 128 } finally { 129 try { 130 apk.close(); 131 } catch (IOException e) { 132 Log.w(TAG, "Failed to close resource", e); 133 } 134 } 135 136 return files; 137 } 138 139 private static void prepareDexDir(File dexDir, final String extractedFilePrefix, 140 final long sourceLastModified) throws IOException { 141 dexDir.mkdir(); 142 if (!dexDir.isDirectory()) { 143 throw new IOException("Failed to create dex directory " + dexDir.getPath()); 144 } 145 146 // Clean possible old files 147 FileFilter filter = new FileFilter() { 148 149 @Override 150 public boolean accept(File pathname) { 151 return (!pathname.getName().startsWith(extractedFilePrefix)) 152 || (pathname.lastModified() < sourceLastModified); 153 } 154 }; 155 File[] files = dexDir.listFiles(filter); 156 if (files == null) { 157 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); 158 return; 159 } 160 for (File oldFile : files) { 161 if (!oldFile.delete()) { 162 Log.w(TAG, "Failed to delete 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 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 } finally { 191 closeQuietly(out); 192 } 193 if (!tmp.setLastModified(sourceLastModified)) { 194 Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." + 195 " This may cause problems with later updates of the apk."); 196 } 197 Log.i(TAG, "Renaming to " + extractTo.getPath()); 198 if (!tmp.renameTo(extractTo)) { 199 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + 200 extractTo.getAbsolutePath() + "\""); 201 } 202 } finally { 203 closeQuietly(in); 204 tmp.delete(); // return status ignored 205 } 206 } 207 208 /** 209 * Returns whether the file is a valid zip file. 210 */ 211 private static boolean verifyZipFile(File file) { 212 try { 213 ZipFile zipFile = new ZipFile(file); 214 try { 215 zipFile.close(); 216 } catch (IOException e) { 217 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath()); 218 } 219 return true; 220 } catch (ZipException ex) { 221 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex); 222 } catch (IOException ex) { 223 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex); 224 } 225 return false; 226 } 227 228 /** 229 * Closes the given {@code Closeable}. Suppresses any IO exceptions. 230 */ 231 private static void closeQuietly(Closeable closeable) { 232 try { 233 closeable.close(); 234 } catch (IOException e) { 235 Log.w(TAG, "Failed to close resource", e); 236 } 237 } 238 239 private static synchronized String computeSha1Digest(File file) { 240 MessageDigest messageDigest = getMessageDigest("SHA1"); 241 if (messageDigest == null) { 242 return ""; 243 } 244 FileInputStream in = null; 245 try { 246 in = new FileInputStream(file); 247 byte[] bytes = new byte[8192]; 248 int byteCount; 249 while ((byteCount = in.read(bytes)) != -1) { 250 messageDigest.update(bytes, 0, byteCount); 251 } 252 return toHex(messageDigest.digest(), false /* zeroTerminated */) 253 .toLowerCase(); 254 } catch (IOException e) { 255 return ""; 256 } finally { 257 if (in != null) { 258 closeQuietly(in); 259 } 260 } 261 } 262 263 /** 264 * Encodes a byte array as a hexadecimal representation of bytes. 265 */ 266 private static String toHex(byte[] in, boolean zeroTerminated) { 267 int length = in.length; 268 StringBuilder out = new StringBuilder(length * 2); 269 for (int i = 0; i < length; i++) { 270 if (zeroTerminated && i == length - 1 && (in[i] & 0xff) == 0) { 271 break; 272 } 273 out.append(HEX_DIGITS[(in[i] & 0xf0) >>> 4]); 274 out.append(HEX_DIGITS[in[i] & 0x0f]); 275 } 276 return out.toString(); 277 } 278 279 /** 280 * Retrieves the message digest instance for a given hash algorithm. Makes 281 * {@link #MAX_ATTEMPTS_NO_SUCH_ALGORITHM} to successfully retrieve the 282 * MessageDigest or will return null. 283 */ 284 private static MessageDigest getMessageDigest(String hashAlgorithm) { 285 for (int i = 0; i < MAX_ATTEMPTS_NO_SUCH_ALGORITHM; i++) { 286 try { 287 MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm); 288 if (messageDigest != null) { 289 return messageDigest; 290 } 291 } catch (NoSuchAlgorithmException e) { 292 // try again - this is needed due to a bug in MessageDigest that can have corrupted 293 // internal state. 294 continue; 295 } 296 } 297 return null; 298 } 299} 300