blob: 646e023fc5b5a2b323778f4ecf534367fa1b5785 [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,
Yohann Roussel08133852018-01-15 11:44:07 +0100117 NO_KEY_PREFIX,
118 true);
Yohann Roussel50823412016-09-20 18:01:13 +0200119
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 */
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.");
Maurice Chu667f9a82013-10-16 13:12:22 -0700157 return;
158 }
159
Yohann Roussel50823412016-09-20 18:01:13 +0200160 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;
Maurice Chu667f9a82013-10-16 13:12:22 -0700165 }
166
Yohann Roussel50823412016-09-20 18:01:13 +0200167 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,
Yohann Roussel08133852018-01-15 11:44:07 +0100175 instrumentationPrefix,
176 false);
Yohann Roussel50823412016-09-20 18:01:13 +0200177
178 doInstallation(targetContext,
179 new File(applicationInfo.sourceDir),
180 dataDir,
181 CODE_CACHE_SECONDARY_FOLDER_NAME,
Yohann Roussel08133852018-01-15 11:44:07 +0100182 NO_KEY_PREFIX,
183 false);
Maurice Chu667f9a82013-10-16 13:12:22 -0700184 } catch (Exception e) {
Yohann Roussel50823412016-09-20 18:01:13 +0200185 Log.e(TAG, "MultiDex installation failure", e);
186 throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
Maurice Chu667f9a82013-10-16 13:12:22 -0700187 }
Yohann Roussel50823412016-09-20 18:01:13 +0200188 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.
Yohann Roussel08133852018-01-15 11:44:07 +0100198 * @param reinstallOnPatchRecoverableException if set to true, will attempt a clean extraction
199 * if a possibly recoverable exception occurs during classloader patching.
Yohann Roussel50823412016-09-20 18:01:13 +0200200 */
201 private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
Yohann Roussel08133852018-01-15 11:44:07 +0100202 String secondaryFolderName, String prefsKeyPrefix,
203 boolean reinstallOnPatchRecoverableException) throws IOException,
Yohann Roussel50823412016-09-20 18:01:13 +0200204 IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
205 InvocationTargetException, NoSuchMethodException {
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);
Yohann Roussel08133852018-01-15 11:44:07 +0100254 // 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 }
Yohann Roussel50823412016-09-20 18:01:13 +0200286 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700287 }
288
Jon Noack87738872017-01-12 12:16:58 -0600289 private static ApplicationInfo getApplicationInfo(Context context) {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200290 try {
Jon Noack87738872017-01-12 12:16:58 -0600291 /* 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();
Yohann Rousseld79604b2014-07-08 16:50:10 +0200300 } 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 }
Yohann Rousseld79604b2014-07-08 16:50:10 +0200309 }
310
Yohann Roussel7b86f7e2014-04-22 11:12:54 +0200311 /**
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 */
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
Yohann Roussel99581452017-01-02 17:57:27 +0100340 private static void installSecondaryDexes(ClassLoader loader, File dexDir,
341 List<? extends File> files)
Maurice Chucc63eda2013-12-02 15:39:59 -0800342 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
343 InvocationTargetException, NoSuchMethodException, IOException {
344 if (!files.isEmpty()) {
345 if (Build.VERSION.SDK_INT >= 19) {
346 V19.install(loader, files, dexDir);
347 } else if (Build.VERSION.SDK_INT >= 14) {
348 V14.install(loader, files, dexDir);
349 } else {
350 V4.install(loader, files);
351 }
352 }
353 }
354
355 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700356 * Locates a given field anywhere in the class inheritance hierarchy.
357 *
358 * @param instance an object to search the field into.
359 * @param name field name
360 * @return a field object
361 * @throws NoSuchFieldException if the field cannot be located
362 */
363 private static Field findField(Object instance, String name) throws NoSuchFieldException {
364 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
365 try {
366 Field field = clazz.getDeclaredField(name);
367
368
369 if (!field.isAccessible()) {
370 field.setAccessible(true);
371 }
372
373 return field;
374 } catch (NoSuchFieldException e) {
375 // ignore and search next
376 }
377 }
378
379 throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
380 }
381
382 /**
383 * Locates a given method anywhere in the class inheritance hierarchy.
384 *
385 * @param instance an object to search the method into.
386 * @param name method name
387 * @param parameterTypes method parameter types
388 * @return a method object
389 * @throws NoSuchMethodException if the method cannot be located
390 */
391 private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
392 throws NoSuchMethodException {
393 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
394 try {
395 Method method = clazz.getDeclaredMethod(name, parameterTypes);
396
397
398 if (!method.isAccessible()) {
399 method.setAccessible(true);
400 }
401
402 return method;
403 } catch (NoSuchMethodException e) {
404 // ignore and search next
405 }
406 }
407
408 throw new NoSuchMethodException("Method " + name + " with parameters " +
409 Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
410 }
411
412 /**
413 * Replace the value of a field containing a non null array, by a new array containing the
414 * elements of the original array plus the elements of extraElements.
415 * @param instance the instance whose field is to be modified.
416 * @param fieldName the field to modify.
417 * @param extraElements elements to append at the end of the array.
418 */
419 private static void expandFieldArray(Object instance, String fieldName,
420 Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
421 IllegalAccessException {
422 Field jlrField = findField(instance, fieldName);
423 Object[] original = (Object[]) jlrField.get(instance);
424 Object[] combined = (Object[]) Array.newInstance(
425 original.getClass().getComponentType(), original.length + extraElements.length);
426 System.arraycopy(original, 0, combined, 0, original.length);
427 System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
428 jlrField.set(instance, combined);
429 }
430
Yohann Rousseld79604b2014-07-08 16:50:10 +0200431 private static void clearOldDexDir(Context context) throws Exception {
Yohann Roussel590a07e2014-07-21 17:47:26 +0200432 File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
Yohann Rousseld79604b2014-07-08 16:50:10 +0200433 if (dexDir.isDirectory()) {
434 Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
435 File[] files = dexDir.listFiles();
436 if (files == null) {
437 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
438 return;
439 }
440 for (File oldFile : files) {
441 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size "
442 + oldFile.length());
443 if (!oldFile.delete()) {
444 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
445 } else {
446 Log.i(TAG, "Deleted old file " + oldFile.getPath());
447 }
448 }
449 if (!dexDir.delete()) {
450 Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath());
451 } else {
452 Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath());
453 }
454 }
455 }
456
Yohann Roussel50823412016-09-20 18:01:13 +0200457 private static File getDexDir(Context context, File dataDir, String secondaryFolderName)
Yohann Roussel606af942015-05-12 17:40:52 +0200458 throws IOException {
Yohann Roussel50823412016-09-20 18:01:13 +0200459 File cache = new File(dataDir, CODE_CACHE_NAME);
Yohann Roussel606af942015-05-12 17:40:52 +0200460 try {
461 mkdirChecked(cache);
462 } catch (IOException e) {
463 /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless
464 * files on disk if the device ever updates to android 5+. But since this seems to
465 * happen only on some devices running android 2, this should cause no pollution.
466 */
467 cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
468 mkdirChecked(cache);
469 }
Yohann Roussel50823412016-09-20 18:01:13 +0200470 File dexDir = new File(cache, secondaryFolderName);
Yohann Roussel606af942015-05-12 17:40:52 +0200471 mkdirChecked(dexDir);
472 return dexDir;
473 }
474
475 private static void mkdirChecked(File dir) throws IOException {
476 dir.mkdir();
477 if (!dir.isDirectory()) {
478 File parent = dir.getParentFile();
479 if (parent == null) {
480 Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null.");
481 } else {
482 Log.e(TAG, "Failed to create dir " + dir.getPath() +
483 ". parent file is a dir " + parent.isDirectory() +
484 ", a file " + parent.isFile() +
485 ", exists " + parent.exists() +
486 ", readable " + parent.canRead() +
487 ", writable " + parent.canWrite());
488 }
489 throw new IOException("Failed to create directory " + dir.getPath());
490 }
491 }
492
Maurice Chu667f9a82013-10-16 13:12:22 -0700493 /**
494 * Installer for platform versions 19.
495 */
496 private static final class V19 {
497
Yohann Roussel99581452017-01-02 17:57:27 +0100498 private static void install(ClassLoader loader,
499 List<? extends File> additionalClassPathEntries,
Maurice Chu667f9a82013-10-16 13:12:22 -0700500 File optimizedDirectory)
501 throws IllegalArgumentException, IllegalAccessException,
Yohann Roussel08133852018-01-15 11:44:07 +0100502 NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
503 IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700504 /* The patched class loader is expected to be a descendant of
505 * dalvik.system.BaseDexClassLoader. We modify its
506 * dalvik.system.DexPathList pathList field to append additional DEX
507 * file entries.
508 */
509 Field pathListField = findField(loader, "pathList");
510 Object dexPathList = pathListField.get(loader);
511 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
512 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
513 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
514 suppressedExceptions));
515 if (suppressedExceptions.size() > 0) {
Yohann Roussel88117c32013-11-28 23:22:11 +0100516 for (IOException e : suppressedExceptions) {
517 Log.w(TAG, "Exception in makeDexElement", e);
518 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700519 Field suppressedExceptionsField =
Yohann Roussel74e66b82016-05-17 18:00:18 +0200520 findField(dexPathList, "dexElementsSuppressedExceptions");
Maurice Chu667f9a82013-10-16 13:12:22 -0700521 IOException[] dexElementsSuppressedExceptions =
Yohann Roussel74e66b82016-05-17 18:00:18 +0200522 (IOException[]) suppressedExceptionsField.get(dexPathList);
Maurice Chu667f9a82013-10-16 13:12:22 -0700523
524 if (dexElementsSuppressedExceptions == null) {
525 dexElementsSuppressedExceptions =
526 suppressedExceptions.toArray(
527 new IOException[suppressedExceptions.size()]);
528 } else {
529 IOException[] combined =
530 new IOException[suppressedExceptions.size() +
531 dexElementsSuppressedExceptions.length];
532 suppressedExceptions.toArray(combined);
533 System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
534 suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
535 dexElementsSuppressedExceptions = combined;
536 }
537
Yohann Roussel74e66b82016-05-17 18:00:18 +0200538 suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
Yohann Roussel08133852018-01-15 11:44:07 +0100539
540 IOException exception = new IOException("I/O exception during makeDexElement");
541 exception.initCause(suppressedExceptions.get(0));
542 throw exception;
Maurice Chu667f9a82013-10-16 13:12:22 -0700543 }
544 }
545
546 /**
547 * A wrapper around
548 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
549 */
550 private static Object[] makeDexElements(
551 Object dexPathList, ArrayList<File> files, File optimizedDirectory,
552 ArrayList<IOException> suppressedExceptions)
553 throws IllegalAccessException, InvocationTargetException,
554 NoSuchMethodException {
555 Method makeDexElements =
556 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
557 ArrayList.class);
558
559 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
560 suppressedExceptions);
561 }
562 }
563
564 /**
565 * Installer for platform versions 14, 15, 16, 17 and 18.
566 */
567 private static final class V14 {
568
Yohann Roussel99581452017-01-02 17:57:27 +0100569 private static void install(ClassLoader loader,
570 List<? extends File> additionalClassPathEntries,
Maurice Chu667f9a82013-10-16 13:12:22 -0700571 File optimizedDirectory)
572 throws IllegalArgumentException, IllegalAccessException,
573 NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
574 /* The patched class loader is expected to be a descendant of
575 * dalvik.system.BaseDexClassLoader. We modify its
576 * dalvik.system.DexPathList pathList field to append additional DEX
577 * file entries.
578 */
579 Field pathListField = findField(loader, "pathList");
580 Object dexPathList = pathListField.get(loader);
581 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
582 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
583 }
584
585 /**
586 * A wrapper around
587 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
588 */
589 private static Object[] makeDexElements(
590 Object dexPathList, ArrayList<File> files, File optimizedDirectory)
591 throws IllegalAccessException, InvocationTargetException,
592 NoSuchMethodException {
593 Method makeDexElements =
594 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
595
596 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
597 }
598 }
599
600 /**
601 * Installer for platform versions 4 to 13.
602 */
603 private static final class V4 {
Yohann Roussel99581452017-01-02 17:57:27 +0100604 private static void install(ClassLoader loader,
605 List<? extends File> additionalClassPathEntries)
Maurice Chu667f9a82013-10-16 13:12:22 -0700606 throws IllegalArgumentException, IllegalAccessException,
607 NoSuchFieldException, IOException {
608 /* The patched class loader is expected to be a descendant of
609 * dalvik.system.DexClassLoader. We modify its
Yohann Roussel52eafa02013-11-21 11:46:53 +0100610 * fields mPaths, mFiles, mZips and mDexs to append additional DEX
Maurice Chu667f9a82013-10-16 13:12:22 -0700611 * file entries.
612 */
613 int extraSize = additionalClassPathEntries.size();
614
615 Field pathField = findField(loader, "path");
616
617 StringBuilder path = new StringBuilder((String) pathField.get(loader));
618 String[] extraPaths = new String[extraSize];
619 File[] extraFiles = new File[extraSize];
Maurice Chu66f379f2013-11-14 19:08:32 -0800620 ZipFile[] extraZips = new ZipFile[extraSize];
Maurice Chu667f9a82013-10-16 13:12:22 -0700621 DexFile[] extraDexs = new DexFile[extraSize];
Yohann Roussel99581452017-01-02 17:57:27 +0100622 for (ListIterator<? extends File> iterator = additionalClassPathEntries.listIterator();
Maurice Chu667f9a82013-10-16 13:12:22 -0700623 iterator.hasNext();) {
624 File additionalEntry = iterator.next();
625 String entryPath = additionalEntry.getAbsolutePath();
626 path.append(':').append(entryPath);
627 int index = iterator.previousIndex();
628 extraPaths[index] = entryPath;
629 extraFiles[index] = additionalEntry;
Maurice Chu66f379f2013-11-14 19:08:32 -0800630 extraZips[index] = new ZipFile(additionalEntry);
Maurice Chu667f9a82013-10-16 13:12:22 -0700631 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
632 }
633
634 pathField.set(loader, path.toString());
635 expandFieldArray(loader, "mPaths", extraPaths);
636 expandFieldArray(loader, "mFiles", extraFiles);
Maurice Chu66f379f2013-11-14 19:08:32 -0800637 expandFieldArray(loader, "mZips", extraZips);
Maurice Chu667f9a82013-10-16 13:12:22 -0700638 expandFieldArray(loader, "mDexs", extraDexs);
639 }
640 }
641
642}