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