MultiDexExtractor.java revision 1f8c349b6524aa39a10a570115ce0afb039bd06f
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.Context; 20import android.content.pm.ApplicationInfo; 21import android.util.Log; 22 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.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 54 private static final int BUFFER_SIZE = 0x4000; 55 56 /** 57 * Extracts application secondary dexes into files in the application data 58 * directory. 59 * 60 * @param dexDir 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(Context context, ApplicationInfo applicationInfo, File dexDir) 68 throws IOException { 69 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 if (!extractedFile.isFile()) { 90 extract(context, apk, dexFile, extractedFile, extractedFilePrefix, 91 lastModified); 92 } 93 secondaryNumber++; 94 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 95 } 96 } finally { 97 try { 98 apk.close(); 99 } catch (IOException e) { 100 Log.w(TAG, "Failed to close resource", e); 101 } 102 } 103 104 return files; 105 } 106 107 private static void prepareDexDir(File dexDir, final String extractedFilePrefix, 108 final long sourceLastModified) throws IOException { 109 dexDir.mkdir(); 110 if (!dexDir.isDirectory()) { 111 throw new IOException("Failed to create dex directory " + dexDir.getPath()); 112 } 113 114 // Clean possible old files 115 FileFilter filter = new FileFilter() { 116 117 @Override 118 public boolean accept(File pathname) { 119 return (!pathname.getName().startsWith(extractedFilePrefix)) 120 || (pathname.lastModified() < sourceLastModified); 121 } 122 }; 123 File[] files = dexDir.listFiles(filter); 124 if (files == null) { 125 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); 126 return; 127 } 128 for (File oldFile : files) { 129 if (!oldFile.delete()) { 130 Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); 131 } 132 } 133 } 134 135 private static void extract( 136 Context context, ZipFile apk, ZipEntry dexFile, File extractTo, 137 String extractedFilePrefix, long sourceLastModified) 138 throws IOException, FileNotFoundException { 139 140 InputStream in = apk.getInputStream(dexFile); 141 ZipOutputStream out = null; 142 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX, 143 extractTo.getParentFile()); 144 Log.i(TAG, "Extracting " + tmp.getPath()); 145 try { 146 out = new ZipOutputStream(new FileOutputStream(tmp)); 147 try { 148 ZipEntry classesDex = new ZipEntry("classes.dex"); 149 // keep zip entry time since it is the criteria used by Dalvik 150 classesDex.setTime(dexFile.getTime()); 151 out.putNextEntry(classesDex); 152 153 byte[] buffer = new byte[BUFFER_SIZE]; 154 int length = in.read(buffer); 155 while (length != -1) { 156 if (length > 0) { 157 out.write(buffer, 0, length); 158 } 159 length = in.read(buffer); 160 } 161 } finally { 162 closeQuietly(out); 163 } 164 if (!tmp.setLastModified(sourceLastModified)) { 165 Log.e(TAG, "Failed to set time of \"" + tmp.getAbsolutePath() + "\"." + 166 " This may cause problems with later updates of the apk."); 167 } 168 Log.i(TAG, "Renaming to " + extractTo.getPath()); 169 if (!tmp.renameTo(extractTo)) { 170 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + 171 extractTo.getAbsolutePath() + "\""); 172 } 173 } finally { 174 closeQuietly(in); 175 tmp.delete(); // return status ignored 176 } 177 } 178 179 /** 180 * Closes the given {@code Closeable}. Suppresses any IO exceptions. 181 */ 182 private static void closeQuietly(Closeable closeable) { 183 try { 184 closeable.close(); 185 } catch (IOException e) { 186 Log.w(TAG, "Failed to close resource", e); 187 } 188 } 189} 190