blob: ab7f668b8a2c337b94a99ccd10aee787dbaa204e [file] [log] [blame]
Maurice Chu667f9a82013-10-16 13:12:22 -07001/*
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
17package android.support.multidex;
18
Yohann Rousseldd3cc222014-04-22 10:47:53 +020019import android.app.Application;
Yohann Roussel50823412016-09-20 18:01:13 +020020import android.app.Instrumentation;
Maurice Chu667f9a82013-10-16 13:12:22 -070021import android.content.Context;
22import android.content.pm.ApplicationInfo;
Maurice Chu667f9a82013-10-16 13:12:22 -070023import android.os.Build;
24import android.util.Log;
25
Yohann Roussel602c6ca2014-03-28 17:35:02 +010026import dalvik.system.DexFile;
27
Maurice Chu667f9a82013-10-16 13:12:22 -070028import java.io.File;
29import java.io.IOException;
30import java.lang.reflect.Array;
31import java.lang.reflect.Field;
32import java.lang.reflect.InvocationTargetException;
33import java.lang.reflect.Method;
34import java.util.ArrayList;
35import java.util.Arrays;
36import java.util.HashSet;
37import java.util.List;
38import java.util.ListIterator;
39import java.util.Set;
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020040import java.util.regex.Matcher;
41import java.util.regex.Pattern;
Maurice Chu66f379f2013-11-14 19:08:32 -080042import java.util.zip.ZipFile;
Maurice Chu667f9a82013-10-16 13:12:22 -070043
44/**
Sebastien Hertz8be7c7a2016-07-12 10:03:13 +020045 * MultiDex patches {@link Context#getClassLoader() the application context class
Maurice Chu667f9a82013-10-16 13:12:22 -070046 * loader} in order to load classes from more than one dex file. The primary
Yohann Rousseldd3cc222014-04-22 10:47:53 +020047 * {@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
Maurice Chu667f9a82013-10-16 13:12:22 -070050 * {@link #install(Context)}.
51 *
52 * <p/>
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020053 * 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.
Maurice Chu667f9a82013-10-16 13:12:22 -070055 */
56public final class MultiDex {
57
58 static final String TAG = "MultiDex";
59
Yohann Roussel590a07e2014-07-21 17:47:26 +020060 private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
61
Yohann Roussel606af942015-05-12 17:40:52 +020062 private static final String CODE_CACHE_NAME = "code_cache";
63
64 private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "secondary-dexes";
Maurice Chu667f9a82013-10-16 13:12:22 -070065
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020066 private static final int MAX_SUPPORTED_SDK_VERSION = 20;
Maurice Chu667f9a82013-10-16 13:12:22 -070067
68 private static final int MIN_SDK_VERSION = 4;
69
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020070 private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
71
72 private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
73
Yohann Roussel50823412016-09-20 18:01:13 +020074 private static final String NO_KEY_PREFIX = "";
75
76 private static final Set<File> installedApk = new HashSet<File>();
Maurice Chu667f9a82013-10-16 13:12:22 -070077
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020078 private static final boolean IS_VM_MULTIDEX_CAPABLE =
79 isVMMultidexCapable(System.getProperty("java.vm.version"));
80
Maurice Chu667f9a82013-10-16 13:12:22 -070081 private MultiDex() {}
82
83 /**
84 * Patches the application context class loader by appending extra dex files
Yohann Rousseldd3cc222014-04-22 10:47:53 +020085 * 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.
Maurice Chu667f9a82013-10-16 13:12:22 -070088 *
89 * @param context application context.
90 * @throws RuntimeException if an error occurred preventing the classloader
91 * extension.
92 */
93 public static void install(Context context) {
Yohann Roussel50823412016-09-20 18:01:13 +020094 Log.i(TAG, "Installing application");
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020095 if (IS_VM_MULTIDEX_CAPABLE) {
96 Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
97 return;
98 }
Maurice Chu667f9a82013-10-16 13:12:22 -070099
100 if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
Yohann Roussel50823412016-09-20 18:01:13 +0200101 throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
Maurice Chu667f9a82013-10-16 13:12:22 -0700102 + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
103 }
104
Maurice Chu667f9a82013-10-16 13:12:22 -0700105 try {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200106 ApplicationInfo applicationInfo = getApplicationInfo(context);
Maurice Chu667f9a82013-10-16 13:12:22 -0700107 if (applicationInfo == null) {
Yohann Roussel50823412016-09-20 18:01:13 +0200108 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
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 */
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.");
Maurice Chu667f9a82013-10-16 13:12:22 -0700156 return;
157 }
158
Yohann Roussel50823412016-09-20 18:01:13 +0200159 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;
Maurice Chu667f9a82013-10-16 13:12:22 -0700164 }
165
Yohann Roussel50823412016-09-20 18:01:13 +0200166 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
176 doInstallation(targetContext,
177 new File(applicationInfo.sourceDir),
178 dataDir,
179 CODE_CACHE_SECONDARY_FOLDER_NAME,
180 NO_KEY_PREFIX);
Maurice Chu667f9a82013-10-16 13:12:22 -0700181 } catch (Exception e) {
Yohann Roussel50823412016-09-20 18:01:13 +0200182 Log.e(TAG, "MultiDex installation failure", e);
183 throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
Maurice Chu667f9a82013-10-16 13:12:22 -0700184 }
Yohann Roussel50823412016-09-20 18:01:13 +0200185 Log.i(TAG, "Installation done");
186 }
187
188 /**
189 * @param mainContext context used to get filesDir, to save preference and to get the
190 * classloader to patch.
191 * @param sourceApk Apk file.
192 * @param dataDir data directory to use for code cache simulation.
193 * @param secondaryFolderName name of the folder for storing extractions.
194 * @param prefsKeyPrefix prefix of all stored preference keys.
195 */
196 private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
197 String secondaryFolderName, String prefsKeyPrefix) throws IOException,
198 IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
199 InvocationTargetException, NoSuchMethodException {
200 synchronized (installedApk) {
201 if (installedApk.contains(sourceApk)) {
202 return;
203 }
204 installedApk.add(sourceApk);
205
206 if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
207 Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
208 + Build.VERSION.SDK_INT + ": SDK version higher than "
209 + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
210 + "runtime with built-in multidex capabilty but it's not the "
211 + "case here: java.vm.version=\""
212 + System.getProperty("java.vm.version") + "\"");
213 }
214
215 /* The patched class loader is expected to be a descendant of
216 * dalvik.system.BaseDexClassLoader. We modify its
217 * dalvik.system.DexPathList pathList field to append additional DEX
218 * file entries.
219 */
220 ClassLoader loader;
221 try {
222 loader = mainContext.getClassLoader();
223 } catch (RuntimeException e) {
224 /* Ignore those exceptions so that we don't break tests relying on Context like
225 * a android.test.mock.MockContext or a android.content.ContextWrapper with a
226 * null base Context.
227 */
228 Log.w(TAG, "Failure while trying to obtain Context class loader. " +
229 "Must be running in test mode. Skip patching.", e);
230 return;
231 }
232 if (loader == null) {
233 // Note, the context class loader is null when running Robolectric tests.
234 Log.e(TAG,
235 "Context class loader is null. Must be running in test mode. "
236 + "Skip patching.");
237 return;
238 }
239
240 try {
241 clearOldDexDir(mainContext);
242 } catch (Throwable t) {
243 Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
244 + "continuing without cleaning.", t);
245 }
246
247 File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
248 List<? extends File> files =
249 MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);
250 installSecondaryDexes(loader, dexDir, files);
251 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700252 }
253
Jon Noack87738872017-01-12 12:16:58 -0600254 private static ApplicationInfo getApplicationInfo(Context context) {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200255 try {
Jon Noack87738872017-01-12 12:16:58 -0600256 /* Due to package install races it is possible for a process to be started from an old
257 * apk even though that apk has been replaced. Querying for ApplicationInfo by package
258 * name may return information for the new apk, leading to a runtime with the old main
259 * dex file and new secondary dex files. This leads to various problems like
260 * ClassNotFoundExceptions. Using context.getApplicationInfo() should result in the
261 * process having a consistent view of the world (even if it is of the old world). The
262 * package install races are eventually resolved and old processes are killed.
263 */
264 return context.getApplicationInfo();
Yohann Rousseld79604b2014-07-08 16:50:10 +0200265 } catch (RuntimeException e) {
266 /* Ignore those exceptions so that we don't break tests relying on Context like
267 * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
268 * base Context.
269 */
270 Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
271 "Must be running in test mode. Skip patching.", e);
272 return null;
273 }
Yohann Rousseld79604b2014-07-08 16:50:10 +0200274 }
275
Yohann Roussel7b86f7e2014-04-22 11:12:54 +0200276 /**
277 * Identifies if the current VM has a native support for multidex, meaning there is no need for
278 * additional installation by this library.
279 * @return true if the VM handles multidex
280 */
281 /* package visible for test */
282 static boolean isVMMultidexCapable(String versionString) {
283 boolean isMultidexCapable = false;
284 if (versionString != null) {
285 Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
286 if (matcher.matches()) {
287 try {
288 int major = Integer.parseInt(matcher.group(1));
289 int minor = Integer.parseInt(matcher.group(2));
290 isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
291 || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
292 && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
293 } catch (NumberFormatException e) {
294 // let isMultidexCapable be false
295 }
296 }
297 }
298 Log.i(TAG, "VM with version " + versionString +
299 (isMultidexCapable ?
300 " has multidex support" :
301 " does not have multidex support"));
302 return isMultidexCapable;
303 }
304
Yohann Roussel99581452017-01-02 17:57:27 +0100305 private static void installSecondaryDexes(ClassLoader loader, File dexDir,
306 List<? extends File> files)
Maurice Chucc63eda2013-12-02 15:39:59 -0800307 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
308 InvocationTargetException, NoSuchMethodException, IOException {
309 if (!files.isEmpty()) {
310 if (Build.VERSION.SDK_INT >= 19) {
311 V19.install(loader, files, dexDir);
312 } else if (Build.VERSION.SDK_INT >= 14) {
313 V14.install(loader, files, dexDir);
314 } else {
315 V4.install(loader, files);
316 }
317 }
318 }
319
320 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700321 * Locates a given field anywhere in the class inheritance hierarchy.
322 *
323 * @param instance an object to search the field into.
324 * @param name field name
325 * @return a field object
326 * @throws NoSuchFieldException if the field cannot be located
327 */
328 private static Field findField(Object instance, String name) throws NoSuchFieldException {
329 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
330 try {
331 Field field = clazz.getDeclaredField(name);
332
333
334 if (!field.isAccessible()) {
335 field.setAccessible(true);
336 }
337
338 return field;
339 } catch (NoSuchFieldException e) {
340 // ignore and search next
341 }
342 }
343
344 throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
345 }
346
347 /**
348 * Locates a given method anywhere in the class inheritance hierarchy.
349 *
350 * @param instance an object to search the method into.
351 * @param name method name
352 * @param parameterTypes method parameter types
353 * @return a method object
354 * @throws NoSuchMethodException if the method cannot be located
355 */
356 private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
357 throws NoSuchMethodException {
358 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
359 try {
360 Method method = clazz.getDeclaredMethod(name, parameterTypes);
361
362
363 if (!method.isAccessible()) {
364 method.setAccessible(true);
365 }
366
367 return method;
368 } catch (NoSuchMethodException e) {
369 // ignore and search next
370 }
371 }
372
373 throw new NoSuchMethodException("Method " + name + " with parameters " +
374 Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
375 }
376
377 /**
378 * Replace the value of a field containing a non null array, by a new array containing the
379 * elements of the original array plus the elements of extraElements.
380 * @param instance the instance whose field is to be modified.
381 * @param fieldName the field to modify.
382 * @param extraElements elements to append at the end of the array.
383 */
384 private static void expandFieldArray(Object instance, String fieldName,
385 Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
386 IllegalAccessException {
387 Field jlrField = findField(instance, fieldName);
388 Object[] original = (Object[]) jlrField.get(instance);
389 Object[] combined = (Object[]) Array.newInstance(
390 original.getClass().getComponentType(), original.length + extraElements.length);
391 System.arraycopy(original, 0, combined, 0, original.length);
392 System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
393 jlrField.set(instance, combined);
394 }
395
Yohann Rousseld79604b2014-07-08 16:50:10 +0200396 private static void clearOldDexDir(Context context) throws Exception {
Yohann Roussel590a07e2014-07-21 17:47:26 +0200397 File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
Yohann Rousseld79604b2014-07-08 16:50:10 +0200398 if (dexDir.isDirectory()) {
399 Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
400 File[] files = dexDir.listFiles();
401 if (files == null) {
402 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
403 return;
404 }
405 for (File oldFile : files) {
406 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size "
407 + oldFile.length());
408 if (!oldFile.delete()) {
409 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
410 } else {
411 Log.i(TAG, "Deleted old file " + oldFile.getPath());
412 }
413 }
414 if (!dexDir.delete()) {
415 Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath());
416 } else {
417 Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath());
418 }
419 }
420 }
421
Yohann Roussel50823412016-09-20 18:01:13 +0200422 private static File getDexDir(Context context, File dataDir, String secondaryFolderName)
Yohann Roussel606af942015-05-12 17:40:52 +0200423 throws IOException {
Yohann Roussel50823412016-09-20 18:01:13 +0200424 File cache = new File(dataDir, CODE_CACHE_NAME);
Yohann Roussel606af942015-05-12 17:40:52 +0200425 try {
426 mkdirChecked(cache);
427 } catch (IOException e) {
428 /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless
429 * files on disk if the device ever updates to android 5+. But since this seems to
430 * happen only on some devices running android 2, this should cause no pollution.
431 */
432 cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
433 mkdirChecked(cache);
434 }
Yohann Roussel50823412016-09-20 18:01:13 +0200435 File dexDir = new File(cache, secondaryFolderName);
Yohann Roussel606af942015-05-12 17:40:52 +0200436 mkdirChecked(dexDir);
437 return dexDir;
438 }
439
440 private static void mkdirChecked(File dir) throws IOException {
441 dir.mkdir();
442 if (!dir.isDirectory()) {
443 File parent = dir.getParentFile();
444 if (parent == null) {
445 Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null.");
446 } else {
447 Log.e(TAG, "Failed to create dir " + dir.getPath() +
448 ". parent file is a dir " + parent.isDirectory() +
449 ", a file " + parent.isFile() +
450 ", exists " + parent.exists() +
451 ", readable " + parent.canRead() +
452 ", writable " + parent.canWrite());
453 }
454 throw new IOException("Failed to create directory " + dir.getPath());
455 }
456 }
457
Maurice Chu667f9a82013-10-16 13:12:22 -0700458 /**
459 * Installer for platform versions 19.
460 */
461 private static final class V19 {
462
Yohann Roussel99581452017-01-02 17:57:27 +0100463 private static void install(ClassLoader loader,
464 List<? extends File> additionalClassPathEntries,
Maurice Chu667f9a82013-10-16 13:12:22 -0700465 File optimizedDirectory)
466 throws IllegalArgumentException, IllegalAccessException,
467 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
468 /* The patched class loader is expected to be a descendant of
469 * dalvik.system.BaseDexClassLoader. We modify its
470 * dalvik.system.DexPathList pathList field to append additional DEX
471 * file entries.
472 */
473 Field pathListField = findField(loader, "pathList");
474 Object dexPathList = pathListField.get(loader);
475 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
476 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
477 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
478 suppressedExceptions));
479 if (suppressedExceptions.size() > 0) {
Yohann Roussel88117c32013-11-28 23:22:11 +0100480 for (IOException e : suppressedExceptions) {
481 Log.w(TAG, "Exception in makeDexElement", e);
482 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700483 Field suppressedExceptionsField =
Yohann Roussel74e66b82016-05-17 18:00:18 +0200484 findField(dexPathList, "dexElementsSuppressedExceptions");
Maurice Chu667f9a82013-10-16 13:12:22 -0700485 IOException[] dexElementsSuppressedExceptions =
Yohann Roussel74e66b82016-05-17 18:00:18 +0200486 (IOException[]) suppressedExceptionsField.get(dexPathList);
Maurice Chu667f9a82013-10-16 13:12:22 -0700487
488 if (dexElementsSuppressedExceptions == null) {
489 dexElementsSuppressedExceptions =
490 suppressedExceptions.toArray(
491 new IOException[suppressedExceptions.size()]);
492 } else {
493 IOException[] combined =
494 new IOException[suppressedExceptions.size() +
495 dexElementsSuppressedExceptions.length];
496 suppressedExceptions.toArray(combined);
497 System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
498 suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
499 dexElementsSuppressedExceptions = combined;
500 }
501
Yohann Roussel74e66b82016-05-17 18:00:18 +0200502 suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
Maurice Chu667f9a82013-10-16 13:12:22 -0700503 }
504 }
505
506 /**
507 * A wrapper around
508 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
509 */
510 private static Object[] makeDexElements(
511 Object dexPathList, ArrayList<File> files, File optimizedDirectory,
512 ArrayList<IOException> suppressedExceptions)
513 throws IllegalAccessException, InvocationTargetException,
514 NoSuchMethodException {
515 Method makeDexElements =
516 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
517 ArrayList.class);
518
519 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
520 suppressedExceptions);
521 }
522 }
523
524 /**
525 * Installer for platform versions 14, 15, 16, 17 and 18.
526 */
527 private static final class V14 {
528
Yohann Roussel99581452017-01-02 17:57:27 +0100529 private static void install(ClassLoader loader,
530 List<? extends File> additionalClassPathEntries,
Maurice Chu667f9a82013-10-16 13:12:22 -0700531 File optimizedDirectory)
532 throws IllegalArgumentException, IllegalAccessException,
533 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
534 /* The patched class loader is expected to be a descendant of
535 * dalvik.system.BaseDexClassLoader. We modify its
536 * dalvik.system.DexPathList pathList field to append additional DEX
537 * file entries.
538 */
539 Field pathListField = findField(loader, "pathList");
540 Object dexPathList = pathListField.get(loader);
541 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
542 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
543 }
544
545 /**
546 * A wrapper around
547 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
548 */
549 private static Object[] makeDexElements(
550 Object dexPathList, ArrayList<File> files, File optimizedDirectory)
551 throws IllegalAccessException, InvocationTargetException,
552 NoSuchMethodException {
553 Method makeDexElements =
554 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
555
556 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
557 }
558 }
559
560 /**
561 * Installer for platform versions 4 to 13.
562 */
563 private static final class V4 {
Yohann Roussel99581452017-01-02 17:57:27 +0100564 private static void install(ClassLoader loader,
565 List<? extends File> additionalClassPathEntries)
Maurice Chu667f9a82013-10-16 13:12:22 -0700566 throws IllegalArgumentException, IllegalAccessException,
567 NoSuchFieldException, IOException {
568 /* The patched class loader is expected to be a descendant of
569 * dalvik.system.DexClassLoader. We modify its
Yohann Roussel52eafa02013-11-21 11:46:53 +0100570 * fields mPaths, mFiles, mZips and mDexs to append additional DEX
Maurice Chu667f9a82013-10-16 13:12:22 -0700571 * file entries.
572 */
573 int extraSize = additionalClassPathEntries.size();
574
575 Field pathField = findField(loader, "path");
576
577 StringBuilder path = new StringBuilder((String) pathField.get(loader));
578 String[] extraPaths = new String[extraSize];
579 File[] extraFiles = new File[extraSize];
Maurice Chu66f379f2013-11-14 19:08:32 -0800580 ZipFile[] extraZips = new ZipFile[extraSize];
Maurice Chu667f9a82013-10-16 13:12:22 -0700581 DexFile[] extraDexs = new DexFile[extraSize];
Yohann Roussel99581452017-01-02 17:57:27 +0100582 for (ListIterator<? extends File> iterator = additionalClassPathEntries.listIterator();
Maurice Chu667f9a82013-10-16 13:12:22 -0700583 iterator.hasNext();) {
584 File additionalEntry = iterator.next();
585 String entryPath = additionalEntry.getAbsolutePath();
586 path.append(':').append(entryPath);
587 int index = iterator.previousIndex();
588 extraPaths[index] = entryPath;
589 extraFiles[index] = additionalEntry;
Maurice Chu66f379f2013-11-14 19:08:32 -0800590 extraZips[index] = new ZipFile(additionalEntry);
Maurice Chu667f9a82013-10-16 13:12:22 -0700591 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
592 }
593
594 pathField.set(loader, path.toString());
595 expandFieldArray(loader, "mPaths", extraPaths);
596 expandFieldArray(loader, "mFiles", extraFiles);
Maurice Chu66f379f2013-11-14 19:08:32 -0800597 expandFieldArray(loader, "mZips", extraZips);
Maurice Chu667f9a82013-10-16 13:12:22 -0700598 expandFieldArray(loader, "mDexs", extraDexs);
599 }
600 }
601
602}