17 package com.google.appinventor.components.runtime.multidex;
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.content.pm.ApplicationInfo;
22 import android.os.Build;
23 import android.util.Log;
25 import java.io.BufferedOutputStream;
26 import java.io.Closeable;
28 import java.io.FileFilter;
29 import java.io.FileNotFoundException;
30 import java.io.FileOutputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.zip.ZipEntry;
38 import java.util.zip.ZipException;
39 import java.util.zip.ZipFile;
40 import java.util.zip.ZipOutputStream;
48 final class MultiDexExtractor {
50 private static final String TAG = MultiDex.TAG;
56 private static final String DEX_PREFIX =
"classes";
57 private static final String DEX_SUFFIX =
".dex";
59 private static final String EXTRACTED_NAME_EXT =
".classes";
60 private static final String EXTRACTED_SUFFIX =
".zip";
61 private static final int MAX_EXTRACT_ATTEMPTS = 3;
63 private static final String PREFS_FILE =
"multidex.version";
64 private static final String KEY_TIME_STAMP =
"timestamp";
65 private static final String KEY_CRC =
"crc";
66 private static final String KEY_DEX_NUMBER =
"dex.number";
71 private static final int BUFFER_SIZE = 0x4000;
73 private static final long NO_VALUE = -1L;
82 public static boolean mustLoad(Context context, ApplicationInfo applicationInfo) {
83 File sourceApk =
new File(applicationInfo.sourceDir);
86 currentCrc = getZipCrc(sourceApk);
87 if (isModified(context, sourceApk, currentCrc)) {
90 }
catch (IOException e) {
105 static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
106 boolean forceReload)
throws IOException {
107 Log.i(TAG,
"MultiDexExtractor.load(" + applicationInfo.sourceDir +
", " + forceReload +
")");
108 final File sourceApk =
new File(applicationInfo.sourceDir);
110 long currentCrc = getZipCrc(sourceApk);
113 if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
115 files = loadExistingExtractions(context, sourceApk, dexDir);
116 }
catch (IOException ioe) {
117 Log.w(TAG,
"Failed to reload existing extracted secondary dex files,"
118 +
" falling back to fresh extraction", ioe);
119 files = performExtractions(sourceApk, dexDir);
120 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
124 Log.i(TAG,
"Detected that extraction must be performed.");
125 files = performExtractions(sourceApk, dexDir);
126 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
129 Log.i(TAG,
"load found " + files.size() +
" secondary dex files");
133 private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
135 Log.i(TAG,
"loading existing secondary dex files");
137 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
138 int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
139 final List<File> files =
new ArrayList<File>(totalDexNumber);
141 for (
int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
142 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
143 File extractedFile =
new File(dexDir, fileName);
144 if (extractedFile.isFile()) {
145 files.add(extractedFile);
146 if (!verifyZipFile(extractedFile)) {
147 Log.i(TAG,
"Invalid zip file: " + extractedFile);
148 throw new IOException(
"Invalid ZIP file.");
151 throw new IOException(
"Missing extracted secondary dex file '" +
152 extractedFile.getPath() +
"'");
159 private static boolean isModified(Context context, File archive,
long currentCrc) {
160 SharedPreferences prefs = getMultiDexPreferences(context);
161 return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
162 || (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
165 private static long getTimeStamp(File archive) {
166 long timeStamp = archive.lastModified();
167 if (timeStamp == NO_VALUE) {
175 private static long getZipCrc(File archive)
throws IOException {
176 long computedValue = ZipUtil.getZipCrc(archive);
177 if (computedValue == NO_VALUE) {
181 return computedValue;
184 private static List<File> performExtractions(File sourceApk, File dexDir)
187 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
193 prepareDexDir(dexDir, extractedFilePrefix);
195 List<File> files =
new ArrayList<File>();
197 final ZipFile apk =
new ZipFile(sourceApk);
200 int secondaryNumber = 2;
202 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
203 while (dexFile !=
null) {
204 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
205 File extractedFile =
new File(dexDir, fileName);
206 files.add(extractedFile);
208 Log.i(TAG,
"Extraction is needed for file " + extractedFile);
210 boolean isExtractionSuccessful =
false;
211 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
216 extract(apk, dexFile, extractedFile, extractedFilePrefix);
219 isExtractionSuccessful = verifyZipFile(extractedFile);
222 Log.i(TAG,
"Extraction " + (isExtractionSuccessful ?
"success" :
"failed") +
223 " - length " + extractedFile.getAbsolutePath() +
": " +
224 extractedFile.length());
225 if (!isExtractionSuccessful) {
227 extractedFile.delete();
228 if (extractedFile.exists()) {
229 Log.w(TAG,
"Failed to delete corrupted secondary dex '" +
230 extractedFile.getPath() +
"'");
234 if (!isExtractionSuccessful) {
235 throw new IOException(
"Could not create zip file " +
236 extractedFile.getAbsolutePath() +
" for secondary dex (" +
237 secondaryNumber +
")");
240 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
245 }
catch (IOException e) {
246 Log.w(TAG,
"Failed to close resource", e);
253 private static void putStoredApkInfo(Context context,
long timeStamp,
long crc,
254 int totalDexNumber) {
255 SharedPreferences prefs = getMultiDexPreferences(context);
256 SharedPreferences.Editor edit = prefs.edit();
257 edit.putLong(KEY_TIME_STAMP, timeStamp);
258 edit.putLong(KEY_CRC, crc);
263 edit.putInt(KEY_DEX_NUMBER, totalDexNumber);
267 private static SharedPreferences getMultiDexPreferences(Context context) {
268 return context.getSharedPreferences(PREFS_FILE,
269 Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
270 ? Context.MODE_PRIVATE
271 : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
277 private static void prepareDexDir(File dexDir,
final String extractedFilePrefix)
280 if (!dexDir.isDirectory()) {
281 throw new IOException(
"Failed to create dex directory " + dexDir.getPath());
285 FileFilter filter =
new FileFilter() {
288 public boolean accept(File pathname) {
289 return !pathname.getName().startsWith(extractedFilePrefix);
292 File[] files = dexDir.listFiles(filter);
294 Log.w(TAG,
"Failed to list secondary dex dir content (" + dexDir.getPath() +
").");
297 for (File oldFile : files) {
298 Log.i(TAG,
"Trying to delete old file " + oldFile.getPath() +
" of size " +
300 if (!oldFile.delete()) {
301 Log.w(TAG,
"Failed to delete old file " + oldFile.getPath());
303 Log.i(TAG,
"Deleted old file " + oldFile.getPath());
308 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
309 String extractedFilePrefix)
throws IOException, FileNotFoundException {
311 InputStream in = apk.getInputStream(dexFile);
312 ZipOutputStream out =
null;
313 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
314 extractTo.getParentFile());
315 Log.i(TAG,
"Extracting " + tmp.getPath());
317 out =
new ZipOutputStream(
new BufferedOutputStream(
new FileOutputStream(tmp)));
319 ZipEntry classesDex =
new ZipEntry(
"classes.dex");
321 classesDex.setTime(dexFile.getTime());
322 out.putNextEntry(classesDex);
324 byte[] buffer =
new byte[BUFFER_SIZE];
325 int length = in.read(buffer);
326 while (length != -1) {
327 out.write(buffer, 0, length);
328 length = in.read(buffer);
334 Log.i(TAG,
"Renaming to " + extractTo.getPath());
335 if (!tmp.renameTo(extractTo)) {
336 throw new IOException(
"Failed to rename \"" + tmp.getAbsolutePath() +
337 "\" to \"" + extractTo.getAbsolutePath() +
"\"");
340 IOUtils.closeQuietly(TAG, in);
348 static boolean verifyZipFile(File file) {
350 ZipFile zipFile =
new ZipFile(file);
354 }
catch (IOException e) {
355 Log.w(TAG,
"Failed to close zip file: " + file.getAbsolutePath());
357 }
catch (ZipException ex) {
358 Log.w(TAG,
"File " + file.getAbsolutePath() +
" is not a valid zip file.", ex);
359 }
catch (IOException ex) {
360 Log.w(TAG,
"Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
367 private static Method sApplyMethod;
370 Class<?> cls = SharedPreferences.Editor.class;
371 sApplyMethod = cls.getMethod(
"apply");
372 }
catch (NoSuchMethodException unused) {
377 private static void apply(SharedPreferences.Editor editor) {
378 if (sApplyMethod !=
null) {
380 sApplyMethod.invoke(editor);
382 }
catch (InvocationTargetException unused) {
384 }
catch (IllegalAccessException unused) {