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