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) {