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 androidx.multidex; 18 19 import android.app.Application; 20 import android.app.Instrumentation; 21 import android.content.Context; 22 import android.content.pm.ApplicationInfo; 23 import android.os.Build; 24 import android.util.Log; 25 26 import dalvik.system.DexFile; 27 28 import java.io.File; 29 import java.io.IOException; 30 import java.lang.reflect.Array; 31 import java.lang.reflect.Constructor; 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.StringTokenizer; 42 import java.util.zip.ZipFile; 43 44 /** 45 * MultiDex patches {@link Context#getClassLoader() the application context class 46 * loader} in order to load classes from more than one dex file. The primary 47 * {@code classes.dex} must contain the classes necessary for calling this 48 * class methods. Secondary dex files named classes2.dex, classes3.dex... found 49 * in the application apk will be added to the classloader after first call to 50 * {@link #install(Context)}. 51 * 52 * <p/> 53 * This library provides compatibility for platforms with API level 4 through 20. This library does 54 * nothing on newer versions of the platform which provide built-in support for secondary dex files. 55 */ 56 public final class MultiDex { 57 58 static final String TAG = "MultiDex"; 59 60 private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes"; 61 62 private static final String CODE_CACHE_NAME = "code_cache"; 63 64 private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "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 String NO_KEY_PREFIX = ""; 75 76 private static final Set<File> installedApk = new HashSet<File>(); 77 78 private static final boolean IS_VM_MULTIDEX_CAPABLE = 79 isVMMultidexCapable(System.getProperty("java.vm.version")); 80 MultiDex()81 private MultiDex() {} 82 83 /** 84 * Patches the application context class loader by appending extra dex files 85 * loaded from the application apk. This method should be called in the 86 * attachBaseContext of your {@link Application}, see 87 * {@link MultiDexApplication} for more explanation and an example. 88 * 89 * @param context application context. 90 * @throws RuntimeException if an error occurred preventing the classloader 91 * extension. 92 */ install(Context context)93 public static void install(Context context) { 94 Log.i(TAG, "Installing application"); 95 if (IS_VM_MULTIDEX_CAPABLE) { 96 Log.i(TAG, "VM has multidex support, MultiDex support library is disabled."); 97 return; 98 } 99 100 if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) { 101 throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT 102 + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + "."); 103 } 104 105 try { 106 ApplicationInfo applicationInfo = getApplicationInfo(context); 107 if (applicationInfo == null) { 108 Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:" 109 + " MultiDex support library is disabled."); 110 return; 111 } 112 113 doInstallation(context, 114 new File(applicationInfo.sourceDir), 115 new File(applicationInfo.dataDir), 116 CODE_CACHE_SECONDARY_FOLDER_NAME, 117 NO_KEY_PREFIX, 118 true); 119 120 } catch (RuntimeException e) { 121 Log.e(TAG, "MultiDex installation failure", e); 122 throw e; 123 } catch (Exception e) { 124 Log.e(TAG, "MultiDex installation failure", e); 125 throw new RuntimeException("MultiDex installation failed.", e); 126 } 127 Log.i(TAG, "install done"); 128 } 129 130 /** 131 * Patches the instrumentation context class loader by appending extra dex files 132 * loaded from the instrumentation apk and the application apk. This method should be called in 133 * the onCreate of your {@link Instrumentation}, see 134 * {@link com.android.test.runner.MultiDexTestRunner} for an example. 135 * 136 * @param instrumentationContext instrumentation context. 137 * @param targetContext target application context. 138 * @throws RuntimeException if an error occurred preventing the classloader 139 * extension. 140 */ installInstrumentation(Context instrumentationContext, Context targetContext)141 public static void installInstrumentation(Context instrumentationContext, 142 Context targetContext) { 143 Log.i(TAG, "Installing instrumentation"); 144 145 if (IS_VM_MULTIDEX_CAPABLE) { 146 Log.i(TAG, "VM has multidex support, MultiDex support library is disabled."); 147 return; 148 } 149 150 if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) { 151 throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT 152 + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + "."); 153 } 154 try { 155 156 ApplicationInfo instrumentationInfo = getApplicationInfo(instrumentationContext); 157 if (instrumentationInfo == null) { 158 Log.i(TAG, "No ApplicationInfo available for instrumentation, i.e. running on a" 159 + " test Context: MultiDex support library is disabled."); 160 return; 161 } 162 163 ApplicationInfo applicationInfo = getApplicationInfo(targetContext); 164 if (applicationInfo == null) { 165 Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:" 166 + " MultiDex support library is disabled."); 167 return; 168 } 169 170 String instrumentationPrefix = instrumentationContext.getPackageName() + "."; 171 172 File dataDir = new File(applicationInfo.dataDir); 173 174 doInstallation(targetContext, 175 new File(instrumentationInfo.sourceDir), 176 dataDir, 177 instrumentationPrefix + CODE_CACHE_SECONDARY_FOLDER_NAME, 178 instrumentationPrefix, 179 false); 180 181 doInstallation(targetContext, 182 new File(applicationInfo.sourceDir), 183 dataDir, 184 CODE_CACHE_SECONDARY_FOLDER_NAME, 185 NO_KEY_PREFIX, 186 false); 187 } catch (RuntimeException e) { 188 Log.e(TAG, "MultiDex installation failure", e); 189 throw e; 190 } catch (Exception e) { 191 Log.e(TAG, "MultiDex installation failure", e); 192 throw new RuntimeException("MultiDex installation failed.", e); 193 } 194 Log.i(TAG, "Installation done"); 195 } 196 197 /** 198 * @param mainContext context used to get filesDir, to save preference and to get the 199 * classloader to patch. 200 * @param sourceApk Apk file. 201 * @param dataDir data directory to use for code cache simulation. 202 * @param secondaryFolderName name of the folder for storing extractions. 203 * @param prefsKeyPrefix prefix of all stored preference keys. 204 * @param reinstallOnPatchRecoverableException if set to true, will attempt a clean extraction 205 * if a possibly recoverable exception occurs during classloader patching. 206 */ doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException)207 private static void doInstallation(Context mainContext, File sourceApk, File dataDir, 208 String secondaryFolderName, String prefsKeyPrefix, 209 boolean reinstallOnPatchRecoverableException) throws IOException, 210 IllegalArgumentException, IllegalAccessException, NoSuchFieldException, 211 InvocationTargetException, NoSuchMethodException, SecurityException, 212 ClassNotFoundException, InstantiationException { 213 synchronized (installedApk) { 214 if (installedApk.contains(sourceApk)) { 215 return; 216 } 217 installedApk.add(sourceApk); 218 219 if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) { 220 Log.w(TAG, "MultiDex is not guaranteed to work in SDK version " 221 + Build.VERSION.SDK_INT + ": SDK version higher than " 222 + MAX_SUPPORTED_SDK_VERSION + " should be backed by " 223 + "runtime with built-in multidex capabilty but it's not the " 224 + "case here: java.vm.version=\"" 225 + System.getProperty("java.vm.version") + "\""); 226 } 227 228 /* The patched class loader is expected to be a ClassLoader capable of loading DEX 229 * bytecode. We modify its pathList field to append additional DEX file entries. 230 */ 231 ClassLoader loader = getDexClassloader(mainContext); 232 if (loader == null) { 233 return; 234 } 235 236 try { 237 clearOldDexDir(mainContext); 238 } catch (Throwable t) { 239 Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, " 240 + "continuing without cleaning.", t); 241 } 242 243 File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName); 244 // MultiDexExtractor is taking the file lock and keeping it until it is closed. 245 // Keep it open during installSecondaryDexes and through forced extraction to ensure no 246 // extraction or optimizing dexopt is running in parallel. 247 MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir); 248 IOException closeException = null; 249 try { 250 List<? extends File> files = 251 extractor.load(mainContext, prefsKeyPrefix, false); 252 try { 253 installSecondaryDexes(loader, dexDir, files); 254 // Some IOException causes may be fixed by a clean extraction. 255 } catch (IOException e) { 256 if (!reinstallOnPatchRecoverableException) { 257 throw e; 258 } 259 Log.w(TAG, "Failed to install extracted secondary dex files, retrying with " 260 + "forced extraction", e); 261 files = extractor.load(mainContext, prefsKeyPrefix, true); 262 installSecondaryDexes(loader, dexDir, files); 263 } 264 } finally { 265 try { 266 extractor.close(); 267 } catch (IOException e) { 268 // Delay throw of close exception to ensure we don't override some exception 269 // thrown during the try block. 270 closeException = e; 271 } 272 } 273 if (closeException != null) { 274 throw closeException; 275 } 276 } 277 } 278 279 /** 280 * Returns a {@link Classloader} from the {@link Context} that is capable of reading dex 281 * bytecode or null if the Classloader is not dex-capable e.g: when running on a JVM testing 282 * environment such as Robolectric. 283 */ getDexClassloader(Context context)284 private static ClassLoader getDexClassloader(Context context) { 285 ClassLoader loader; 286 try { 287 loader = context.getClassLoader(); 288 } catch (RuntimeException e) { 289 /* Ignore those exceptions so that we don't break tests relying on Context like 290 * a android.test.mock.MockContext or a android.content.ContextWrapper with a 291 * null base Context. 292 */ 293 Log.w(TAG, "Failure while trying to obtain Context class loader. " 294 + "Must be running in test mode. Skip patching.", e); 295 return null; 296 } 297 298 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 299 if (loader instanceof dalvik.system.BaseDexClassLoader) { 300 return loader; 301 } 302 } else if (loader instanceof dalvik.system.DexClassLoader 303 || loader instanceof dalvik.system.PathClassLoader) { 304 return loader; 305 } 306 Log.e(TAG, "Context class loader is null or not dex-capable. " 307 + "Must be running in test mode. Skip patching."); 308 return null; 309 } 310 getApplicationInfo(Context context)311 private static ApplicationInfo getApplicationInfo(Context context) { 312 try { 313 /* Due to package install races it is possible for a process to be started from an old 314 * apk even though that apk has been replaced. Querying for ApplicationInfo by package 315 * name may return information for the new apk, leading to a runtime with the old main 316 * dex file and new secondary dex files. This leads to various problems like 317 * ClassNotFoundExceptions. Using context.getApplicationInfo() should result in the 318 * process having a consistent view of the world (even if it is of the old world). The 319 * package install races are eventually resolved and old processes are killed. 320 */ 321 return context.getApplicationInfo(); 322 } catch (RuntimeException e) { 323 /* Ignore those exceptions so that we don't break tests relying on Context like 324 * a android.test.mock.MockContext or a android.content.ContextWrapper with a null 325 * base Context. 326 */ 327 Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " + 328 "Must be running in test mode. Skip patching.", e); 329 return null; 330 } 331 } 332 333 /** 334 * Identifies if the current VM has a native support for multidex, meaning there is no need for 335 * additional installation by this library. 336 * @return true if the VM handles multidex 337 */ 338 /* package visible for test */ isVMMultidexCapable(String versionString)339 static boolean isVMMultidexCapable(String versionString) { 340 boolean isMultidexCapable = false; 341 if (versionString != null) { 342 StringTokenizer tokenizer = new StringTokenizer(versionString, "."); 343 String majorToken = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null; 344 String minorToken = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null; 345 if (majorToken != null && minorToken != null) { 346 try { 347 int major = Integer.parseInt(majorToken); 348 int minor = Integer.parseInt(minorToken); 349 isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR) 350 || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR) 351 && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR)); 352 } catch (NumberFormatException e) { 353 // let isMultidexCapable be false 354 } 355 } 356 } 357 Log.i(TAG, "VM with version " + versionString + 358 (isMultidexCapable ? 359 " has multidex support" : 360 " does not have multidex support")); 361 return isMultidexCapable; 362 } 363 installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files)364 private static void installSecondaryDexes(ClassLoader loader, File dexDir, 365 List<? extends File> files) 366 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, 367 InvocationTargetException, NoSuchMethodException, IOException, SecurityException, 368 ClassNotFoundException, InstantiationException { 369 if (!files.isEmpty()) { 370 if (Build.VERSION.SDK_INT >= 19) { 371 V19.install(loader, files, dexDir); 372 } else if (Build.VERSION.SDK_INT >= 14) { 373 V14.install(loader, files); 374 } else { 375 V4.install(loader, files); 376 } 377 } 378 } 379 380 /** 381 * Locates a given field anywhere in the class inheritance hierarchy. 382 * 383 * @param instance an object to search the field into. 384 * @param name field name 385 * @return a field object 386 * @throws NoSuchFieldException if the field cannot be located 387 */ findField(Object instance, String name)388 private static Field findField(Object instance, String name) throws NoSuchFieldException { 389 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { 390 try { 391 Field field = clazz.getDeclaredField(name); 392 393 394 if (!field.isAccessible()) { 395 field.setAccessible(true); 396 } 397 398 return field; 399 } catch (NoSuchFieldException e) { 400 // ignore and search next 401 } 402 } 403 404 throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass()); 405 } 406 407 /** 408 * Locates a given method anywhere in the class inheritance hierarchy. 409 * 410 * @param instance an object to search the method into. 411 * @param name method name 412 * @param parameterTypes method parameter types 413 * @return a method object 414 * @throws NoSuchMethodException if the method cannot be located 415 */ findMethod(Object instance, String name, Class<?>... parameterTypes)416 private static Method findMethod(Object instance, String name, Class<?>... parameterTypes) 417 throws NoSuchMethodException { 418 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { 419 try { 420 Method method = clazz.getDeclaredMethod(name, parameterTypes); 421 422 423 if (!method.isAccessible()) { 424 method.setAccessible(true); 425 } 426 427 return method; 428 } catch (NoSuchMethodException e) { 429 // ignore and search next 430 } 431 } 432 433 throw new NoSuchMethodException("Method " + name + " with parameters " + 434 Arrays.asList(parameterTypes) + " not found in " + instance.getClass()); 435 } 436 437 /** 438 * Replace the value of a field containing a non null array, by a new array containing the 439 * elements of the original array plus the elements of extraElements. 440 * @param instance the instance whose field is to be modified. 441 * @param fieldName the field to modify. 442 * @param extraElements elements to append at the end of the array. 443 */ expandFieldArray(Object instance, String fieldName, Object[] extraElements)444 private static void expandFieldArray(Object instance, String fieldName, 445 Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, 446 IllegalAccessException { 447 Field jlrField = findField(instance, fieldName); 448 Object[] original = (Object[]) jlrField.get(instance); 449 Object[] combined = (Object[]) Array.newInstance( 450 original.getClass().getComponentType(), original.length + extraElements.length); 451 System.arraycopy(original, 0, combined, 0, original.length); 452 System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); 453 jlrField.set(instance, combined); 454 } 455 clearOldDexDir(Context context)456 private static void clearOldDexDir(Context context) throws Exception { 457 File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME); 458 if (dexDir.isDirectory()) { 459 Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ")."); 460 File[] files = dexDir.listFiles(); 461 if (files == null) { 462 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); 463 return; 464 } 465 for (File oldFile : files) { 466 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " 467 + oldFile.length()); 468 if (!oldFile.delete()) { 469 Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); 470 } else { 471 Log.i(TAG, "Deleted old file " + oldFile.getPath()); 472 } 473 } 474 if (!dexDir.delete()) { 475 Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath()); 476 } else { 477 Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath()); 478 } 479 } 480 } 481 getDexDir(Context context, File dataDir, String secondaryFolderName)482 private static File getDexDir(Context context, File dataDir, String secondaryFolderName) 483 throws IOException { 484 File cache = new File(dataDir, CODE_CACHE_NAME); 485 try { 486 mkdirChecked(cache); 487 } catch (IOException e) { 488 /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless 489 * files on disk if the device ever updates to android 5+. But since this seems to 490 * happen only on some devices running android 2, this should cause no pollution. 491 */ 492 cache = new File(context.getFilesDir(), CODE_CACHE_NAME); 493 mkdirChecked(cache); 494 } 495 File dexDir = new File(cache, secondaryFolderName); 496 mkdirChecked(dexDir); 497 return dexDir; 498 } 499 mkdirChecked(File dir)500 private static void mkdirChecked(File dir) throws IOException { 501 dir.mkdir(); 502 if (!dir.isDirectory()) { 503 File parent = dir.getParentFile(); 504 if (parent == null) { 505 Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null."); 506 } else { 507 Log.e(TAG, "Failed to create dir " + dir.getPath() + 508 ". parent file is a dir " + parent.isDirectory() + 509 ", a file " + parent.isFile() + 510 ", exists " + parent.exists() + 511 ", readable " + parent.canRead() + 512 ", writable " + parent.canWrite()); 513 } 514 throw new IOException("Failed to create directory " + dir.getPath()); 515 } 516 } 517 518 /** 519 * Installer for platform versions 19. 520 */ 521 private static final class V19 { 522 install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory)523 static void install(ClassLoader loader, 524 List<? extends File> additionalClassPathEntries, 525 File optimizedDirectory) 526 throws IllegalArgumentException, IllegalAccessException, 527 NoSuchFieldException, InvocationTargetException, NoSuchMethodException, 528 IOException { 529 /* The patched class loader is expected to be a descendant of 530 * dalvik.system.BaseDexClassLoader. We modify its 531 * dalvik.system.DexPathList pathList field to append additional DEX 532 * file entries. 533 */ 534 Field pathListField = findField(loader, "pathList"); 535 Object dexPathList = pathListField.get(loader); 536 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); 537 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, 538 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, 539 suppressedExceptions)); 540 if (suppressedExceptions.size() > 0) { 541 for (IOException e : suppressedExceptions) { 542 Log.w(TAG, "Exception in makeDexElement", e); 543 } 544 Field suppressedExceptionsField = 545 findField(dexPathList, "dexElementsSuppressedExceptions"); 546 IOException[] dexElementsSuppressedExceptions = 547 (IOException[]) suppressedExceptionsField.get(dexPathList); 548 549 if (dexElementsSuppressedExceptions == null) { 550 dexElementsSuppressedExceptions = 551 suppressedExceptions.toArray( 552 new IOException[suppressedExceptions.size()]); 553 } else { 554 IOException[] combined = 555 new IOException[suppressedExceptions.size() + 556 dexElementsSuppressedExceptions.length]; 557 suppressedExceptions.toArray(combined); 558 System.arraycopy(dexElementsSuppressedExceptions, 0, combined, 559 suppressedExceptions.size(), dexElementsSuppressedExceptions.length); 560 dexElementsSuppressedExceptions = combined; 561 } 562 563 suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions); 564 565 IOException exception = new IOException("I/O exception during makeDexElement"); 566 exception.initCause(suppressedExceptions.get(0)); 567 throw exception; 568 } 569 } 570 571 /** 572 * A wrapper around 573 * {@code private static final dalvik.system.DexPathList#makeDexElements}. 574 */ makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions)575 private static Object[] makeDexElements( 576 Object dexPathList, ArrayList<File> files, File optimizedDirectory, 577 ArrayList<IOException> suppressedExceptions) 578 throws IllegalAccessException, InvocationTargetException, 579 NoSuchMethodException { 580 Method makeDexElements = 581 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, 582 ArrayList.class); 583 584 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, 585 suppressedExceptions); 586 } 587 } 588 589 /** 590 * Installer for platform versions 14, 15, 16, 17 and 18. 591 */ 592 private static final class V14 { 593 594 private interface ElementConstructor { newInstance(File file, DexFile dex)595 Object newInstance(File file, DexFile dex) 596 throws IllegalArgumentException, InstantiationException, 597 IllegalAccessException, InvocationTargetException, IOException; 598 } 599 600 /** 601 * Applies for ICS and early JB (initial release and MR1). 602 */ 603 private static class ICSElementConstructor implements ElementConstructor { 604 private final Constructor<?> elementConstructor; 605 ICSElementConstructor(Class<?> elementClass)606 ICSElementConstructor(Class<?> elementClass) 607 throws SecurityException, NoSuchMethodException { 608 elementConstructor = 609 elementClass.getConstructor(File.class, ZipFile.class, DexFile.class); 610 elementConstructor.setAccessible(true); 611 } 612 613 @Override newInstance(File file, DexFile dex)614 public Object newInstance(File file, DexFile dex) 615 throws IllegalArgumentException, InstantiationException, 616 IllegalAccessException, InvocationTargetException, IOException { 617 return elementConstructor.newInstance(file, new ZipFile(file), dex); 618 } 619 } 620 621 /** 622 * Applies for some intermediate JB (MR1.1). 623 * 624 * See Change-Id: I1a5b5d03572601707e1fb1fd4424c1ae2fd2217d 625 */ 626 private static class JBMR11ElementConstructor implements ElementConstructor { 627 private final Constructor<?> elementConstructor; 628 JBMR11ElementConstructor(Class<?> elementClass)629 JBMR11ElementConstructor(Class<?> elementClass) 630 throws SecurityException, NoSuchMethodException { 631 elementConstructor = elementClass 632 .getConstructor(File.class, File.class, DexFile.class); 633 elementConstructor.setAccessible(true); 634 } 635 636 @Override newInstance(File file, DexFile dex)637 public Object newInstance(File file, DexFile dex) 638 throws IllegalArgumentException, InstantiationException, 639 IllegalAccessException, InvocationTargetException { 640 return elementConstructor.newInstance(file, file, dex); 641 } 642 } 643 644 /** 645 * Applies for latest JB (MR2). 646 * 647 * See Change-Id: Iec4dca2244db9c9c793ac157e258fd61557a7a5d 648 */ 649 private static class JBMR2ElementConstructor implements ElementConstructor { 650 private final Constructor<?> elementConstructor; 651 JBMR2ElementConstructor(Class<?> elementClass)652 JBMR2ElementConstructor(Class<?> elementClass) 653 throws SecurityException, NoSuchMethodException { 654 elementConstructor = elementClass 655 .getConstructor(File.class, Boolean.TYPE, File.class, DexFile.class); 656 elementConstructor.setAccessible(true); 657 } 658 659 @Override newInstance(File file, DexFile dex)660 public Object newInstance(File file, DexFile dex) 661 throws IllegalArgumentException, InstantiationException, 662 IllegalAccessException, InvocationTargetException { 663 return elementConstructor.newInstance(file, Boolean.FALSE, file, dex); 664 } 665 } 666 667 private static final int EXTRACTED_SUFFIX_LENGTH = 668 MultiDexExtractor.EXTRACTED_SUFFIX.length(); 669 670 private final ElementConstructor elementConstructor; 671 install(ClassLoader loader, List<? extends File> additionalClassPathEntries)672 static void install(ClassLoader loader, 673 List<? extends File> additionalClassPathEntries) 674 throws IOException, SecurityException, IllegalArgumentException, 675 ClassNotFoundException, NoSuchMethodException, InstantiationException, 676 IllegalAccessException, InvocationTargetException, NoSuchFieldException { 677 /* The patched class loader is expected to be a descendant of 678 * dalvik.system.BaseDexClassLoader. We modify its 679 * dalvik.system.DexPathList pathList field to append additional DEX 680 * file entries. 681 */ 682 Field pathListField = findField(loader, "pathList"); 683 Object dexPathList = pathListField.get(loader); 684 Object[] elements = new V14().makeDexElements(additionalClassPathEntries); 685 try { 686 expandFieldArray(dexPathList, "dexElements", elements); 687 } catch (NoSuchFieldException e) { 688 // dexElements was renamed pathElements for a short period during JB development, 689 // eventually it was renamed back shortly after. 690 Log.w(TAG, "Failed find field 'dexElements' attempting 'pathElements'", e); 691 expandFieldArray(dexPathList, "pathElements", elements); 692 } 693 } 694 V14()695 private V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException { 696 ElementConstructor constructor; 697 Class<?> elementClass = Class.forName("dalvik.system.DexPathList$Element"); 698 try { 699 constructor = new ICSElementConstructor(elementClass); 700 } catch (NoSuchMethodException e1) { 701 try { 702 constructor = new JBMR11ElementConstructor(elementClass); 703 } catch (NoSuchMethodException e2) { 704 constructor = new JBMR2ElementConstructor(elementClass); 705 } 706 } 707 this.elementConstructor = constructor; 708 } 709 710 /** 711 * An emulation of {@code private static final dalvik.system.DexPathList#makeDexElements} 712 * accepting only extracted secondary dex files. 713 * OS version is catching IOException and just logging some of them, this version is letting 714 * them through. 715 */ makeDexElements(List<? extends File> files)716 private Object[] makeDexElements(List<? extends File> files) 717 throws IOException, SecurityException, IllegalArgumentException, 718 InstantiationException, IllegalAccessException, InvocationTargetException { 719 Object[] elements = new Object[files.size()]; 720 for (int i = 0; i < elements.length; i++) { 721 File file = files.get(i); 722 elements[i] = elementConstructor.newInstance( 723 file, 724 DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0)); 725 } 726 return elements; 727 } 728 729 /** 730 * Converts a zip file path of an extracted secondary dex to an output file path for an 731 * associated optimized dex file. 732 */ optimizedPathFor(File path)733 private static String optimizedPathFor(File path) { 734 // Any reproducible name ending with ".dex" should do but lets keep the same name 735 // as DexPathList.optimizedPathFor 736 737 File optimizedDirectory = path.getParentFile(); 738 String fileName = path.getName(); 739 String optimizedFileName = 740 fileName.substring(0, fileName.length() - EXTRACTED_SUFFIX_LENGTH) 741 + MultiDexExtractor.DEX_SUFFIX; 742 File result = new File(optimizedDirectory, optimizedFileName); 743 return result.getPath(); 744 } 745 } 746 747 /** 748 * Installer for platform versions 4 to 13. 749 */ 750 private static final class V4 { install(ClassLoader loader, List<? extends File> additionalClassPathEntries)751 static void install(ClassLoader loader, 752 List<? extends File> additionalClassPathEntries) 753 throws IllegalArgumentException, IllegalAccessException, 754 NoSuchFieldException, IOException { 755 /* The patched class loader is expected to be a descendant of 756 * dalvik.system.DexClassLoader. We modify its 757 * fields mPaths, mFiles, mZips and mDexs to append additional DEX 758 * file entries. 759 */ 760 int extraSize = additionalClassPathEntries.size(); 761 762 Field pathField = findField(loader, "path"); 763 764 StringBuilder path = new StringBuilder((String) pathField.get(loader)); 765 String[] extraPaths = new String[extraSize]; 766 File[] extraFiles = new File[extraSize]; 767 ZipFile[] extraZips = new ZipFile[extraSize]; 768 DexFile[] extraDexs = new DexFile[extraSize]; 769 for (ListIterator<? extends File> iterator = additionalClassPathEntries.listIterator(); 770 iterator.hasNext();) { 771 File additionalEntry = iterator.next(); 772 String entryPath = additionalEntry.getAbsolutePath(); 773 path.append(':').append(entryPath); 774 int index = iterator.previousIndex(); 775 extraPaths[index] = entryPath; 776 extraFiles[index] = additionalEntry; 777 extraZips[index] = new ZipFile(additionalEntry); 778 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); 779 } 780 781 pathField.set(loader, path.toString()); 782 expandFieldArray(loader, "mPaths", extraPaths); 783 expandFieldArray(loader, "mFiles", extraFiles); 784 expandFieldArray(loader, "mZips", extraZips); 785 expandFieldArray(loader, "mDexs", extraDexs); 786 } 787 } 788 789 } 790