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