AI2 Component  (Version nb184)
MultiDexExtractor.java
Go to the documentation of this file.
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 
17 package com.google.appinventor.components.runtime.multidex;
18 
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;
24 
25 import java.io.BufferedOutputStream;
26 import java.io.Closeable;
27 import java.io.File;
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;
41 
43 
48 final class MultiDexExtractor {
49 
50  private static final String TAG = MultiDex.TAG;
51 
56  private static final String DEX_PREFIX = "classes";
57  private static final String DEX_SUFFIX = ".dex";
58 
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;
62 
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";
67 
71  private static final int BUFFER_SIZE = 0x4000;
72  /* Keep value away from 0 because it is a too probable time stamp value */
73  private static final long NO_VALUE = -1L;
74 
75  /*
76  * Tests to see if the current classes file(s) are up to date in the application
77  * directory. returns true if they are not, meaning we are going to take a while
78  * so put up the installation progress dialog.
79  *
80  */
81 
82  public static boolean mustLoad(Context context, ApplicationInfo applicationInfo) {
83  File sourceApk = new File(applicationInfo.sourceDir);
84  long currentCrc;
85  try {
86  currentCrc = getZipCrc(sourceApk);
87  if (isModified(context, sourceApk, currentCrc)) {
88  return true;
89  }
90  } catch (IOException e) {
91  // XXX
92  }
93  return false;
94  }
95 
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);
109 
110  long currentCrc = getZipCrc(sourceApk);
111 
112  List<File> files;
113  if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
114  try {
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);
121 
122  }
123  } else {
124  Log.i(TAG, "Detected that extraction must be performed.");
125  files = performExtractions(sourceApk, dexDir);
126  putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
127  }
128 
129  Log.i(TAG, "load found " + files.size() + " secondary dex files");
130  return files;
131  }
132 
133  private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
134  throws IOException {
135  Log.i(TAG, "loading existing secondary dex files");
136 
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);
140 
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.");
149  }
150  } else {
151  throw new IOException("Missing extracted secondary dex file '" +
152  extractedFile.getPath() + "'");
153  }
154  }
155 
156  return files;
157  }
158 
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);
163  }
164 
165  private static long getTimeStamp(File archive) {
166  long timeStamp = archive.lastModified();
167  if (timeStamp == NO_VALUE) {
168  // never return NO_VALUE
169  timeStamp--;
170  }
171  return timeStamp;
172  }
173 
174 
175  private static long getZipCrc(File archive) throws IOException {
176  long computedValue = ZipUtil.getZipCrc(archive);
177  if (computedValue == NO_VALUE) {
178  // never return NO_VALUE
179  computedValue--;
180  }
181  return computedValue;
182  }
183 
184  private static List<File> performExtractions(File sourceApk, File dexDir)
185  throws IOException {
186 
187  final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
188 
189  // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
190  // contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
191  // multi-process race conditions can cause a crash loop where one process deletes the zip
192  // while another had created it.
193  prepareDexDir(dexDir, extractedFilePrefix);
194 
195  List<File> files = new ArrayList<File>();
196 
197  final ZipFile apk = new ZipFile(sourceApk);
198  try {
199 
200  int secondaryNumber = 2;
201 
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);
207 
208  Log.i(TAG, "Extraction is needed for file " + extractedFile);
209  int numAttempts = 0;
210  boolean isExtractionSuccessful = false;
211  while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
212  numAttempts++;
213 
214  // Create a zip file (extractedFile) containing only the secondary dex file
215  // (dexFile) from the apk.
216  extract(apk, dexFile, extractedFile, extractedFilePrefix);
217 
218  // Verify that the extracted file is indeed a zip file.
219  isExtractionSuccessful = verifyZipFile(extractedFile);
220 
221  // Log the sha1 of the extracted zip file
222  Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
223  " - length " + extractedFile.getAbsolutePath() + ": " +
224  extractedFile.length());
225  if (!isExtractionSuccessful) {
226  // Delete the extracted file
227  extractedFile.delete();
228  if (extractedFile.exists()) {
229  Log.w(TAG, "Failed to delete corrupted secondary dex '" +
230  extractedFile.getPath() + "'");
231  }
232  }
233  }
234  if (!isExtractionSuccessful) {
235  throw new IOException("Could not create zip file " +
236  extractedFile.getAbsolutePath() + " for secondary dex (" +
237  secondaryNumber + ")");
238  }
239  secondaryNumber++;
240  dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
241  }
242  } finally {
243  try {
244  apk.close();
245  } catch (IOException e) {
246  Log.w(TAG, "Failed to close resource", e);
247  }
248  }
249 
250  return files;
251  }
252 
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);
259  /* SharedPreferences.Editor doc says that apply() and commit() "atomically performs the
260  * requested modifications" it should be OK to rely on saving the dex files number (getting
261  * old number value would go along with old crc and time stamp).
262  */
263  edit.putInt(KEY_DEX_NUMBER, totalDexNumber);
264  apply(edit);
265  }
266 
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);
272  }
273 
277  private static void prepareDexDir(File dexDir, final String extractedFilePrefix)
278  throws IOException {
279  dexDir.mkdirs();
280  if (!dexDir.isDirectory()) {
281  throw new IOException("Failed to create dex directory " + dexDir.getPath());
282  }
283 
284  // Clean possible old files
285  FileFilter filter = new FileFilter() {
286 
287  @Override
288  public boolean accept(File pathname) {
289  return !pathname.getName().startsWith(extractedFilePrefix);
290  }
291  };
292  File[] files = dexDir.listFiles(filter);
293  if (files == null) {
294  Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
295  return;
296  }
297  for (File oldFile : files) {
298  Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
299  oldFile.length());
300  if (!oldFile.delete()) {
301  Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
302  } else {
303  Log.i(TAG, "Deleted old file " + oldFile.getPath());
304  }
305  }
306  }
307 
308  private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
309  String extractedFilePrefix) throws IOException, FileNotFoundException {
310 
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());
316  try {
317  out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
318  try {
319  ZipEntry classesDex = new ZipEntry("classes.dex");
320  // keep zip entry time since it is the criteria used by Dalvik
321  classesDex.setTime(dexFile.getTime());
322  out.putNextEntry(classesDex);
323 
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);
329  }
330  out.closeEntry();
331  } finally {
332  out.close();
333  }
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() + "\"");
338  }
339  } finally {
340  IOUtils.closeQuietly(TAG, in);
341  tmp.delete(); // return status ignored
342  }
343  }
344 
348  static boolean verifyZipFile(File file) {
349  try {
350  ZipFile zipFile = new ZipFile(file);
351  try {
352  zipFile.close();
353  return true;
354  } catch (IOException e) {
355  Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
356  }
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);
361  }
362  return false;
363  }
364 
365  // The following is taken from SharedPreferencesCompat to avoid having a dependency of the
366  // multidex support library on another support library.
367  private static Method sApplyMethod; // final
368  static {
369  try {
370  Class<?> cls = SharedPreferences.Editor.class;
371  sApplyMethod = cls.getMethod("apply");
372  } catch (NoSuchMethodException unused) {
373  sApplyMethod = null;
374  }
375  }
376 
377  private static void apply(SharedPreferences.Editor editor) {
378  if (sApplyMethod != null) {
379  try {
380  sApplyMethod.invoke(editor);
381  return;
382  } catch (InvocationTargetException unused) {
383  // fall through
384  } catch (IllegalAccessException unused) {
385  // fall through
386  }
387  }
388  editor.commit();
389  }
390 }
com.google.appinventor.components.runtime.util.IOUtils
Definition: IOUtils.java:13
com.google.appinventor.components.runtime.util
-*- mode: java; c-basic-offset: 2; -*-
Definition: AccountChooser.java:7
com.google.appinventor.components
com.google.appinventor.components.runtime
Copyright 2009-2011 Google, All Rights reserved.
Definition: AccelerometerSensor.java:8
com.google
com
com.google.appinventor