AI2 Component  (Version nb184)
MultiDex.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.app.Application;
20 import android.content.Context;
21 import android.content.pm.ApplicationInfo;
22 import android.content.pm.PackageManager;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.os.Build;
25 import android.util.Log;
26 
27 import dalvik.system.DexFile;
28 
29 import java.io.File;
30 import java.io.IOException;
31 import java.lang.reflect.Array;
32 import java.lang.reflect.Field;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.ListIterator;
40 import java.util.Set;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43 import java.util.zip.ZipFile;
44 
57 public final class MultiDex {
58 
59  static final String TAG = "MultiDex";
60 
61  private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
62 
63  private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +
64  "secondary-dexes";
65 
66  private static final int MAX_SUPPORTED_SDK_VERSION = 20;
67 
68  private static final int MIN_SDK_VERSION = 4;
69 
70  private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
71 
72  private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
73 
74  private static final Set<String> installedApk = new HashSet<String>();
75 
76  private static final boolean IS_VM_MULTIDEX_CAPABLE =
77  isVMMultidexCapable(System.getProperty("java.vm.version"));
78 
79  private MultiDex() {}
80 
94  public static boolean install(Context context, boolean doIt) {
95  installedApk.clear();
96  Log.i(TAG, "install: doIt = " + doIt);
97  if (IS_VM_MULTIDEX_CAPABLE) {
98  Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
99  return true;
100  }
101 
102  if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
103  throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
104  + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
105  }
106 
107  try {
108  ApplicationInfo applicationInfo = getApplicationInfo(context);
109  if (applicationInfo == null) {
110  // Looks like running on a test Context, so just return without patching.
111  Log.d(TAG, "applicationInfo is null, returning");
112  return true;
113  }
114 
115  synchronized (installedApk) {
116  String apkPath = applicationInfo.sourceDir;
117  if (installedApk.contains(apkPath)) {
118  return true;
119  }
120  installedApk.add(apkPath);
121 
122  if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
123  Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
124  + Build.VERSION.SDK_INT + ": SDK version higher than "
125  + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
126  + "runtime with built-in multidex capabilty but it's not the "
127  + "case here: java.vm.version=\""
128  + System.getProperty("java.vm.version") + "\"");
129  }
130 
131  /* The patched class loader is expected to be a descendant of
132  * dalvik.system.BaseDexClassLoader. We modify its
133  * dalvik.system.DexPathList pathList field to append additional DEX
134  * file entries.
135  */
136  ClassLoader loader;
137  try {
138  loader = context.getClassLoader();
139  } catch (RuntimeException e) {
140  /* Ignore those exceptions so that we don't break tests relying on Context like
141  * a android.test.mock.MockContext or a android.content.ContextWrapper with a
142  * null base Context.
143  */
144  Log.w(TAG, "Failure while trying to obtain Context class loader. " +
145  "Must be running in test mode. Skip patching.", e);
146  return true;
147  }
148  if (loader == null) {
149  // Note, the context class loader is null when running Robolectric tests.
150  Log.e(TAG,
151  "Context class loader is null. Must be running in test mode. "
152  + "Skip patching.");
153  return true;
154  }
155 
156  try {
157  clearOldDexDir(context);
158  } catch (Throwable t) {
159  Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
160  + "continuing without cleaning.", t);
161  }
162 
163  File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
164  if (!doIt && MultiDexExtractor.mustLoad(context, applicationInfo)) {
165  Log.d(TAG, "Returning because of mustLoad");
166  return false; // We need to do the long loading and DexOpting
167  }
168  Log.d(TAG, "Proceeding with installation...");
169  List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
170  if (checkValidZipFiles(files)) {
171  installSecondaryDexes(loader, dexDir, files);
172  } else {
173  Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
174  // Try again, but this time force a reload of the zip file.
175  files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
176 
177  if (checkValidZipFiles(files)) {
178  installSecondaryDexes(loader, dexDir, files);
179  } else {
180  // Second time didn't work, give up
181  throw new RuntimeException("Zip files were not valid.");
182  }
183  }
184  }
185 
186  } catch (Exception e) {
187  Log.e(TAG, "Multidex installation failure", e);
188  throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
189  }
190  Log.i(TAG, "install done");
191  return true; // Finished
192  }
193 
194  private static ApplicationInfo getApplicationInfo(Context context)
195  throws NameNotFoundException {
196  PackageManager pm;
197  String packageName;
198  try {
199  pm = context.getPackageManager();
200  packageName = context.getPackageName();
201  } catch (RuntimeException e) {
202  /* Ignore those exceptions so that we don't break tests relying on Context like
203  * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
204  * base Context.
205  */
206  Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
207  "Must be running in test mode. Skip patching.", e);
208  return null;
209  }
210  if (pm == null || packageName == null) {
211  // This is most likely a mock context, so just return without patching.
212  return null;
213  }
214  ApplicationInfo applicationInfo =
215  pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
216  return applicationInfo;
217  }
218 
224  /* package visible for test */
225  static boolean isVMMultidexCapable(String versionString) {
226  boolean isMultidexCapable = false;
227  if (versionString != null) {
228  Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
229  if (matcher.matches()) {
230  try {
231  int major = Integer.parseInt(matcher.group(1));
232  int minor = Integer.parseInt(matcher.group(2));
233  isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
234  || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
235  && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
236  } catch (NumberFormatException e) {
237  // let isMultidexCapable be false
238  }
239  }
240  }
241  Log.i(TAG, "VM with version " + versionString +
242  (isMultidexCapable ?
243  " has multidex support" :
244  " does not have multidex support"));
245  return isMultidexCapable;
246  }
247 
248  private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
249  throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
250  InvocationTargetException, NoSuchMethodException, IOException {
251  if (!files.isEmpty()) {
252  if (Build.VERSION.SDK_INT >= 19) {
253  V19.install(loader, files, dexDir);
254  } else if (Build.VERSION.SDK_INT >= 14) {
255  V14.install(loader, files, dexDir);
256  } else {
257  V4.install(loader, files);
258  }
259  }
260  }
261 
266  private static boolean checkValidZipFiles(List<File> files) {
267  for (File file : files) {
268  if (!MultiDexExtractor.verifyZipFile(file)) {
269  return false;
270  }
271  }
272  return true;
273  }
274 
283  private static Field findField(Object instance, String name) throws NoSuchFieldException {
284  for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
285  try {
286  Field field = clazz.getDeclaredField(name);
287 
288 
289  if (!field.isAccessible()) {
290  field.setAccessible(true);
291  }
292 
293  return field;
294  } catch (NoSuchFieldException e) {
295  // ignore and search next
296  }
297  }
298 
299  throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
300  }
301 
311  private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
312  throws NoSuchMethodException {
313  for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
314  try {
315  Method method = clazz.getDeclaredMethod(name, parameterTypes);
316 
317 
318  if (!method.isAccessible()) {
319  method.setAccessible(true);
320  }
321 
322  return method;
323  } catch (NoSuchMethodException e) {
324  // ignore and search next
325  }
326  }
327 
328  throw new NoSuchMethodException("Method " + name + " with parameters " +
329  Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
330  }
331 
339  private static void expandFieldArray(Object instance, String fieldName,
340  Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
341  IllegalAccessException {
342  Field jlrField = findField(instance, fieldName);
343  Object[] original = (Object[]) jlrField.get(instance);
344  Object[] combined = (Object[]) Array.newInstance(
345  original.getClass().getComponentType(), original.length + extraElements.length);
346  System.arraycopy(original, 0, combined, 0, original.length);
347  System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
348  jlrField.set(instance, combined);
349  }
350 
351  private static void clearOldDexDir(Context context) throws Exception {
352  File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
353  if (dexDir.isDirectory()) {
354  Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
355  File[] files = dexDir.listFiles();
356  if (files == null) {
357  Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
358  return;
359  }
360  for (File oldFile : files) {
361  Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size "
362  + oldFile.length());
363  if (!oldFile.delete()) {
364  Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
365  } else {
366  Log.i(TAG, "Deleted old file " + oldFile.getPath());
367  }
368  }
369  if (!dexDir.delete()) {
370  Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath());
371  } else {
372  Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath());
373  }
374  }
375  }
376 
380  private static final class V19 {
381 
382  private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
383  File optimizedDirectory)
384  throws IllegalArgumentException, IllegalAccessException,
385  NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
386  /* The patched class loader is expected to be a descendant of
387  * dalvik.system.BaseDexClassLoader. We modify its
388  * dalvik.system.DexPathList pathList field to append additional DEX
389  * file entries.
390  */
391  Field pathListField = findField(loader, "pathList");
392  Object dexPathList = pathListField.get(loader);
393  ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
394  expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
395  new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
396  suppressedExceptions));
397  if (suppressedExceptions.size() > 0) {
398  for (IOException e : suppressedExceptions) {
399  Log.w(TAG, "Exception in makeDexElement", e);
400  }
401  Field suppressedExceptionsField =
402  findField(loader, "dexElementsSuppressedExceptions");
403  IOException[] dexElementsSuppressedExceptions =
404  (IOException[]) suppressedExceptionsField.get(loader);
405 
406  if (dexElementsSuppressedExceptions == null) {
407  dexElementsSuppressedExceptions =
408  suppressedExceptions.toArray(
409  new IOException[suppressedExceptions.size()]);
410  } else {
411  IOException[] combined =
412  new IOException[suppressedExceptions.size() +
413  dexElementsSuppressedExceptions.length];
414  suppressedExceptions.toArray(combined);
415  System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
416  suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
417  dexElementsSuppressedExceptions = combined;
418  }
419 
420  suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
421  }
422  }
423 
428  private static Object[] makeDexElements(
429  Object dexPathList, ArrayList<File> files, File optimizedDirectory,
430  ArrayList<IOException> suppressedExceptions)
431  throws IllegalAccessException, InvocationTargetException,
432  NoSuchMethodException {
433  Method makeDexElements =
434  findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
435  ArrayList.class);
436 
437  return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
438  suppressedExceptions);
439  }
440  }
441 
445  private static final class V14 {
446 
447  private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
448  File optimizedDirectory)
449  throws IllegalArgumentException, IllegalAccessException,
450  NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
451  /* The patched class loader is expected to be a descendant of
452  * dalvik.system.BaseDexClassLoader. We modify its
453  * dalvik.system.DexPathList pathList field to append additional DEX
454  * file entries.
455  */
456  Field pathListField = findField(loader, "pathList");
457  Object dexPathList = pathListField.get(loader);
458  expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
459  new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
460  }
461 
466  private static Object[] makeDexElements(
467  Object dexPathList, ArrayList<File> files, File optimizedDirectory)
468  throws IllegalAccessException, InvocationTargetException,
469  NoSuchMethodException {
470  Method makeDexElements =
471  findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
472 
473  return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
474  }
475  }
476 
480  private static final class V4 {
481  private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
482  throws IllegalArgumentException, IllegalAccessException,
483  NoSuchFieldException, IOException {
484  /* The patched class loader is expected to be a descendant of
485  * dalvik.system.DexClassLoader. We modify its
486  * fields mPaths, mFiles, mZips and mDexs to append additional DEX
487  * file entries.
488  */
489  int extraSize = additionalClassPathEntries.size();
490 
491  Field pathField = findField(loader, "path");
492 
493  StringBuilder path = new StringBuilder((String) pathField.get(loader));
494  String[] extraPaths = new String[extraSize];
495  File[] extraFiles = new File[extraSize];
496  ZipFile[] extraZips = new ZipFile[extraSize];
497  DexFile[] extraDexs = new DexFile[extraSize];
498  for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
499  iterator.hasNext();) {
500  File additionalEntry = iterator.next();
501  String entryPath = additionalEntry.getAbsolutePath();
502  path.append(':').append(entryPath);
503  int index = iterator.previousIndex();
504  extraPaths[index] = entryPath;
505  extraFiles[index] = additionalEntry;
506  extraZips[index] = new ZipFile(additionalEntry);
507  extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
508  }
509 
510  pathField.set(loader, path.toString());
511  expandFieldArray(loader, "mPaths", extraPaths);
512  expandFieldArray(loader, "mFiles", extraFiles);
513  expandFieldArray(loader, "mZips", extraZips);
514  expandFieldArray(loader, "mDexs", extraDexs);
515  }
516  }
517 
518 }
com.google.appinventor.components.runtime.multidex.MultiDex
Definition: MultiDex.java:57
com.google.appinventor.components.runtime.multidex.MultiDex.install
static boolean install(Context context, boolean doIt)
Definition: MultiDex.java:94