17 package com.google.appinventor.components.runtime.multidex;
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;
27 import dalvik.system.DexFile;
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;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43 import java.util.zip.ZipFile;
59 static final String TAG =
"MultiDex";
61 private static final String OLD_SECONDARY_FOLDER_NAME =
"secondary-dexes";
63 private static final String SECONDARY_FOLDER_NAME =
"code_cache" + File.separator +
66 private static final int MAX_SUPPORTED_SDK_VERSION = 20;
68 private static final int MIN_SDK_VERSION = 4;
70 private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
72 private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
74 private static final Set<String> installedApk =
new HashSet<String>();
76 private static final boolean IS_VM_MULTIDEX_CAPABLE =
77 isVMMultidexCapable(System.getProperty(
"java.vm.version"));
94 public static boolean install(Context context,
boolean doIt) {
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.");
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 +
".");
108 ApplicationInfo applicationInfo = getApplicationInfo(context);
109 if (applicationInfo ==
null) {
111 Log.d(TAG,
"applicationInfo is null, returning");
115 synchronized (installedApk) {
116 String apkPath = applicationInfo.sourceDir;
117 if (installedApk.contains(apkPath)) {
120 installedApk.add(apkPath);
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") +
"\"");
138 loader = context.getClassLoader();
139 }
catch (RuntimeException e) {
144 Log.w(TAG,
"Failure while trying to obtain Context class loader. " +
145 "Must be running in test mode. Skip patching.", e);
148 if (loader ==
null) {
151 "Context class loader is null. Must be running in test mode. "
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);
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");
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);
173 Log.w(TAG,
"Files were not valid zip files. Forcing a reload.");
175 files = MultiDexExtractor.load(context, applicationInfo, dexDir,
true);
177 if (checkValidZipFiles(files)) {
178 installSecondaryDexes(loader, dexDir, files);
181 throw new RuntimeException(
"Zip files were not valid.");
186 }
catch (Exception e) {
187 Log.e(TAG,
"Multidex installation failure", e);
188 throw new RuntimeException(
"Multi dex installation failed (" + e.getMessage() +
").");
190 Log.i(TAG,
"install done");
194 private static ApplicationInfo getApplicationInfo(Context context)
195 throws NameNotFoundException {
199 pm = context.getPackageManager();
200 packageName = context.getPackageName();
201 }
catch (RuntimeException e) {
206 Log.w(TAG,
"Failure while trying to obtain ApplicationInfo from Context. " +
207 "Must be running in test mode. Skip patching.", e);
210 if (pm ==
null || packageName ==
null) {
214 ApplicationInfo applicationInfo =
215 pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
216 return applicationInfo;
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()) {
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) {
241 Log.i(TAG,
"VM with version " + versionString +
243 " has multidex support" :
244 " does not have multidex support"));
245 return isMultidexCapable;
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);
257 V4.install(loader, files);
266 private static boolean checkValidZipFiles(List<File> files) {
267 for (File file : files) {
268 if (!MultiDexExtractor.verifyZipFile(file)) {
283 private static Field findField(Object instance, String name)
throws NoSuchFieldException {
284 for (Class<?> clazz = instance.getClass(); clazz !=
null; clazz = clazz.getSuperclass()) {
286 Field field = clazz.getDeclaredField(name);
289 if (!field.isAccessible()) {
290 field.setAccessible(
true);
294 }
catch (NoSuchFieldException e) {
299 throw new NoSuchFieldException(
"Field " + name +
" not found in " + instance.getClass());
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()) {
315 Method method = clazz.getDeclaredMethod(name, parameterTypes);
318 if (!method.isAccessible()) {
319 method.setAccessible(
true);
323 }
catch (NoSuchMethodException e) {
328 throw new NoSuchMethodException(
"Method " + name +
" with parameters " +
329 Arrays.asList(parameterTypes) +
" not found in " + instance.getClass());
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);
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();
357 Log.w(TAG,
"Failed to list secondary dex dir content (" + dexDir.getPath() +
").");
360 for (File oldFile : files) {
361 Log.i(TAG,
"Trying to delete old file " + oldFile.getPath() +
" of size "
363 if (!oldFile.delete()) {
364 Log.w(TAG,
"Failed to delete old file " + oldFile.getPath());
366 Log.i(TAG,
"Deleted old file " + oldFile.getPath());
369 if (!dexDir.delete()) {
370 Log.w(TAG,
"Failed to delete secondary dex dir " + dexDir.getPath());
372 Log.i(TAG,
"Deleted old secondary dex dir " + dexDir.getPath());
380 private static final class V19 {
382 private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
383 File optimizedDirectory)
384 throws IllegalArgumentException, IllegalAccessException,
385 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
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);
401 Field suppressedExceptionsField =
402 findField(loader,
"dexElementsSuppressedExceptions");
403 IOException[] dexElementsSuppressedExceptions =
404 (IOException[]) suppressedExceptionsField.get(loader);
406 if (dexElementsSuppressedExceptions ==
null) {
407 dexElementsSuppressedExceptions =
408 suppressedExceptions.toArray(
409 new IOException[suppressedExceptions.size()]);
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;
420 suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
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,
437 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
438 suppressedExceptions);
445 private static final class V14 {
447 private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
448 File optimizedDirectory)
449 throws IllegalArgumentException, IllegalAccessException,
450 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
456 Field pathListField = findField(loader,
"pathList");
457 Object dexPathList = pathListField.get(loader);
458 expandFieldArray(dexPathList,
"dexElements", makeDexElements(dexPathList,
459 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
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);
473 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
480 private static final class V4 {
481 private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
482 throws IllegalArgumentException, IllegalAccessException,
483 NoSuchFieldException, IOException {
489 int extraSize = additionalClassPathEntries.size();
491 Field pathField = findField(loader,
"path");
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);
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);