blob: 2b681db05c84d1a55250d9ff90c0bde511ffd655 [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;
Yohann Rousselb2483812018-01-17 14:50:03 +010031import java.lang.reflect.Constructor;
Maurice Chu667f9a82013-10-16 13:12:22 -070032import java.lang.reflect.Field;
33import java.lang.reflect.InvocationTargetException;
34import java.lang.reflect.Method;
35import java.util.ArrayList;
36import java.util.Arrays;
37import java.util.HashSet;
38import java.util.List;
39import java.util.ListIterator;
40import java.util.Set;
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020041import java.util.regex.Matcher;
42import java.util.regex.Pattern;
Maurice Chu66f379f2013-11-14 19:08:32 -080043import java.util.zip.ZipFile;
Maurice Chu667f9a82013-10-16 13:12:22 -070044
45/**
Sebastien Hertz8be7c7a2016-07-12 10:03:13 +020046 * MultiDex patches {@link Context#getClassLoader() the application context class
Maurice Chu667f9a82013-10-16 13:12:22 -070047 * loader} in order to load classes from more than one dex file. The primary
Yohann Rousseldd3cc222014-04-22 10:47:53 +020048 * {@code classes.dex} must contain the classes necessary for calling this
49 * class methods. Secondary dex files named classes2.dex, classes3.dex... found
50 * in the application apk will be added to the classloader after first call to
Maurice Chu667f9a82013-10-16 13:12:22 -070051 * {@link #install(Context)}.
52 *
53 * <p/>
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020054 * This library provides compatibility for platforms with API level 4 through 20. This library does
55 * nothing on newer versions of the platform which provide built-in support for secondary dex files.
Maurice Chu667f9a82013-10-16 13:12:22 -070056 */
57public final class MultiDex {
58
59 static final String TAG = "MultiDex";
60
Yohann Roussel590a07e2014-07-21 17:47:26 +020061 private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
62
Yohann Roussel606af942015-05-12 17:40:52 +020063 private static final String CODE_CACHE_NAME = "code_cache";
64
65 private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "secondary-dexes";
Maurice Chu667f9a82013-10-16 13:12:22 -070066
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020067 private static final int MAX_SUPPORTED_SDK_VERSION = 20;
Maurice Chu667f9a82013-10-16 13:12:22 -070068
69 private static final int MIN_SDK_VERSION = 4;
70
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020071 private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
72
73 private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
74
Yohann Roussel50823412016-09-20 18:01:13 +020075 private static final String NO_KEY_PREFIX = "";
76
77 private static final Set<File> installedApk = new HashSet<File>();
Maurice Chu667f9a82013-10-16 13:12:22 -070078
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020079 private static final boolean IS_VM_MULTIDEX_CAPABLE =
80 isVMMultidexCapable(System.getProperty("java.vm.version"));
81
Maurice Chu667f9a82013-10-16 13:12:22 -070082 private MultiDex() {}
83
84 /**
85 * Patches the application context class loader by appending extra dex files
Yohann Rousseldd3cc222014-04-22 10:47:53 +020086 * loaded from the application apk. This method should be called in the
87 * attachBaseContext of your {@link Application}, see
88 * {@link MultiDexApplication} for more explanation and an example.
Maurice Chu667f9a82013-10-16 13:12:22 -070089 *
90 * @param context application context.
91 * @throws RuntimeException if an error occurred preventing the classloader
92 * extension.
93 */
94 public static void install(Context context) {
Yohann Roussel50823412016-09-20 18:01:13 +020095 Log.i(TAG, "Installing application");
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020096 if (IS_VM_MULTIDEX_CAPABLE) {
97 Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
98 return;
99 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700100
101 if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
Yohann Roussel50823412016-09-20 18:01:13 +0200102 throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
Maurice Chu667f9a82013-10-16 13:12:22 -0700103 + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
104 }
105
Maurice Chu667f9a82013-10-16 13:12:22 -0700106 try {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200107 ApplicationInfo applicationInfo = getApplicationInfo(context);
Maurice Chu667f9a82013-10-16 13:12:22 -0700108 if (applicationInfo == null) {
Yohann Roussel50823412016-09-20 18:01:13 +0200109 Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
110 + " MultiDex support library is disabled.");
111 return;
112 }
113
114 doInstallation(context,
115 new File(applicationInfo.sourceDir),
116 new File(applicationInfo.dataDir),
117 CODE_CACHE_SECONDARY_FOLDER_NAME,
Yohann Roussel08133852018-01-15 11:44:07 +0100118 NO_KEY_PREFIX,
119 true);
Yohann Roussel50823412016-09-20 18:01:13 +0200120
121 } catch (Exception e) {
122 Log.e(TAG, "MultiDex installation failure", e);
123 throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
124 }
125 Log.i(TAG, "install done");
126 }
127
128 /**
129 * Patches the instrumentation context class loader by appending extra dex files
130 * loaded from the instrumentation apk and the application apk. This method should be called in
131 * the onCreate of your {@link Instrumentation}, see
132 * {@link com.android.test.runner.MultiDexTestRunner} for an example.
133 *
134 * @param instrumentationContext instrumentation context.
135 * @param targetContext target application context.
136 * @throws RuntimeException if an error occurred preventing the classloader
137 * extension.
138 */
139 public static void installInstrumentation(Context instrumentationContext,
140 Context targetContext) {
141 Log.i(TAG, "Installing instrumentation");
142
143 if (IS_VM_MULTIDEX_CAPABLE) {
144 Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
145 return;
146 }
147
148 if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
149 throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
150 + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
151 }
152 try {
153
154 ApplicationInfo instrumentationInfo = getApplicationInfo(instrumentationContext);
155 if (instrumentationInfo == null) {
156 Log.i(TAG, "No ApplicationInfo available for instrumentation, i.e. running on a"
157 + " test Context: MultiDex support library is disabled.");
Maurice Chu667f9a82013-10-16 13:12:22 -0700158 return;
159 }
160
Yohann Roussel50823412016-09-20 18:01:13 +0200161 ApplicationInfo applicationInfo = getApplicationInfo(targetContext);
162 if (applicationInfo == null) {
163 Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
164 + " MultiDex support library is disabled.");
165 return;
Maurice Chu667f9a82013-10-16 13:12:22 -0700166 }
167
Yohann Roussel50823412016-09-20 18:01:13 +0200168 String instrumentationPrefix = instrumentationContext.getPackageName() + ".";
169
170 File dataDir = new File(applicationInfo.dataDir);
171
172 doInstallation(targetContext,
173 new File(instrumentationInfo.sourceDir),
174 dataDir,
175 instrumentationPrefix + CODE_CACHE_SECONDARY_FOLDER_NAME,
Yohann Roussel08133852018-01-15 11:44:07 +0100176 instrumentationPrefix,
177 false);
Yohann Roussel50823412016-09-20 18:01:13 +0200178
179 doInstallation(targetContext,
180 new File(applicationInfo.sourceDir),
181 dataDir,
182 CODE_CACHE_SECONDARY_FOLDER_NAME,
Yohann Roussel08133852018-01-15 11:44:07 +0100183 NO_KEY_PREFIX,
184 false);
Maurice Chu667f9a82013-10-16 13:12:22 -0700185 } catch (Exception e) {
Yohann Roussel50823412016-09-20 18:01:13 +0200186 Log.e(TAG, "MultiDex installation failure", e);
187 throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
Maurice Chu667f9a82013-10-16 13:12:22 -0700188 }
Yohann Roussel50823412016-09-20 18:01:13 +0200189 Log.i(TAG, "Installation done");
190 }
191
192 /**
193 * @param mainContext context used to get filesDir, to save preference and to get the
194 * classloader to patch.
195 * @param sourceApk Apk file.
196 * @param dataDir data directory to use for code cache simulation.
197 * @param secondaryFolderName name of the folder for storing extractions.
198 * @param prefsKeyPrefix prefix of all stored preference keys.
Yohann Roussel08133852018-01-15 11:44:07 +0100199 * @param reinstallOnPatchRecoverableException if set to true, will attempt a clean extraction
200 * if a possibly recoverable exception occurs during classloader patching.
Yohann Roussel50823412016-09-20 18:01:13 +0200201 */
202 private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
Yohann Roussel08133852018-01-15 11:44:07 +0100203 String secondaryFolderName, String prefsKeyPrefix,
204 boolean reinstallOnPatchRecoverableException) throws IOException,
Yohann Roussel50823412016-09-20 18:01:13 +0200205 IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
Yohann Rousselb2483812018-01-17 14:50:03 +0100206 InvocationTargetException, NoSuchMethodException, SecurityException,
207 ClassNotFoundException, InstantiationException {
Yohann Roussel50823412016-09-20 18:01:13 +0200208 synchronized (installedApk) {
209 if (installedApk.contains(sourceApk)) {
210 return;
211 }
212 installedApk.add(sourceApk);
213
214 if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
215 Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
216 + Build.VERSION.SDK_INT + ": SDK version higher than "
217 + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
218 + "runtime with built-in multidex capabilty but it's not the "
219 + "case here: java.vm.version=\""
220 + System.getProperty("java.vm.version") + "\"");
221 }
222
223 /* The patched class loader is expected to be a descendant of
224 * dalvik.system.BaseDexClassLoader. We modify its
225 * dalvik.system.DexPathList pathList field to append additional DEX
226 * file entries.
227 */
228 ClassLoader loader;
229 try {
230 loader = mainContext.getClassLoader();
231 } catch (RuntimeException e) {
232 /* Ignore those exceptions so that we don't break tests relying on Context like
233 * a android.test.mock.MockContext or a android.content.ContextWrapper with a
234 * null base Context.
235 */
236 Log.w(TAG, "Failure while trying to obtain Context class loader. " +
237 "Must be running in test mode. Skip patching.", e);
238 return;
239 }
240 if (loader == null) {
241 // Note, the context class loader is null when running Robolectric tests.
242 Log.e(TAG,
243 "Context class loader is null. Must be running in test mode. "
244 + "Skip patching.");
245 return;
246 }
247
248 try {
249 clearOldDexDir(mainContext);
250 } catch (Throwable t) {
251 Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
252 + "continuing without cleaning.", t);
253 }
254
255 File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
Yohann Roussel08133852018-01-15 11:44:07 +0100256 // MultiDexExtractor is taking the file lock and keeping it until it is closed.
257 // Keep it open during installSecondaryDexes and through forced extraction to ensure no
258 // extraction or optimizing dexopt is running in parallel.
259 MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
260 IOException closeException = null;
261 try {
262 List<? extends File> files =
263 extractor.load(mainContext, prefsKeyPrefix, false);
264 try {
265 installSecondaryDexes(loader, dexDir, files);
266 // Some IOException causes may be fixed by a clean extraction.
267 } catch (IOException e) {
268 if (!reinstallOnPatchRecoverableException) {
269 throw e;
270 }
271 Log.w(TAG, "Failed to install extracted secondary dex files, retrying with "
272 + "forced extraction", e);
273 files = extractor.load(mainContext, prefsKeyPrefix, true);
274 installSecondaryDexes(loader, dexDir, files);
275 }
276 } finally {
277 try {
278 extractor.close();
279 } catch (IOException e) {
280 // Delay throw of close exception to ensure we don't override some exception
281 // thrown during the try block.
282 closeException = e;
283 }
284 }
285 if (closeException != null) {
286 throw closeException;
287 }
Yohann Roussel50823412016-09-20 18:01:13 +0200288 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700289 }
290
Jon Noack87738872017-01-12 12:16:58 -0600291 private static ApplicationInfo getApplicationInfo(Context context) {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200292 try {
Jon Noack87738872017-01-12 12:16:58 -0600293 /* Due to package install races it is possible for a process to be started from an old
294 * apk even though that apk has been replaced. Querying for ApplicationInfo by package
295 * name may return information for the new apk, leading to a runtime with the old main
296 * dex file and new secondary dex files. This leads to various problems like
297 * ClassNotFoundExceptions. Using context.getApplicationInfo() should result in the
298 * process having a consistent view of the world (even if it is of the old world). The
299 * package install races are eventually resolved and old processes are killed.
300 */
301 return context.getApplicationInfo();
Yohann Rousseld79604b2014-07-08 16:50:10 +0200302 } catch (RuntimeException e) {
303 /* Ignore those exceptions so that we don't break tests relying on Context like
304 * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
305 * base Context.
306 */
307 Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
308 "Must be running in test mode. Skip patching.", e);
309 return null;
310 }
Yohann Rousseld79604b2014-07-08 16:50:10 +0200311 }
312
Yohann Roussel7b86f7e2014-04-22 11:12:54 +0200313 /**
314 * Identifies if the current VM has a native support for multidex, meaning there is no need for
315 * additional installation by this library.
316 * @return true if the VM handles multidex
317 */
318 /* package visible for test */
319 static boolean isVMMultidexCapable(String versionString) {
320 boolean isMultidexCapable = false;
321 if (versionString != null) {
322 Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
323 if (matcher.matches()) {
324 try {
325 int major = Integer.parseInt(matcher.group(1));
326 int minor = Integer.parseInt(matcher.group(2));
327 isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
328 || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
329 && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
330 } catch (NumberFormatException e) {
331 // let isMultidexCapable be false
332 }
333 }
334 }
335 Log.i(TAG, "VM with version " + versionString +
336 (isMultidexCapable ?
337 " has multidex support" :
338 " does not have multidex support"));
339 return isMultidexCapable;
340 }
341
Yohann Roussel99581452017-01-02 17:57:27 +0100342 private static void installSecondaryDexes(ClassLoader loader, File dexDir,
343 List<? extends File> files)
Maurice Chucc63eda2013-12-02 15:39:59 -0800344 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
Yohann Rousselb2483812018-01-17 14:50:03 +0100345 InvocationTargetException, NoSuchMethodException, IOException, SecurityException,
346 ClassNotFoundException, InstantiationException {
Maurice Chucc63eda2013-12-02 15:39:59 -0800347 if (!files.isEmpty()) {
348 if (Build.VERSION.SDK_INT >= 19) {
349 V19.install(loader, files, dexDir);
350 } else if (Build.VERSION.SDK_INT >= 14) {
Yohann Rousselb2483812018-01-17 14:50:03 +0100351 V14.install(loader, files);
Maurice Chucc63eda2013-12-02 15:39:59 -0800352 } else {
353 V4.install(loader, files);
354 }
355 }
356 }
357
358 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700359 * Locates a given field anywhere in the class inheritance hierarchy.
360 *
361 * @param instance an object to search the field into.
362 * @param name field name
363 * @return a field object
364 * @throws NoSuchFieldException if the field cannot be located
365 */
366 private static Field findField(Object instance, String name) throws NoSuchFieldException {
367 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
368 try {
369 Field field = clazz.getDeclaredField(name);
370
371
372 if (!field.isAccessible()) {
373 field.setAccessible(true);
374 }
375
376 return field;
377 } catch (NoSuchFieldException e) {
378 // ignore and search next
379 }
380 }
381
382 throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
383 }
384
385 /**
386 * Locates a given method anywhere in the class inheritance hierarchy.
387 *
388 * @param instance an object to search the method into.
389 * @param name method name
390 * @param parameterTypes method parameter types
391 * @return a method object
392 * @throws NoSuchMethodException if the method cannot be located
393 */
394 private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
395 throws NoSuchMethodException {
396 for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
397 try {
398 Method method = clazz.getDeclaredMethod(name, parameterTypes);
399
400
401 if (!method.isAccessible()) {
402 method.setAccessible(true);
403 }
404
405 return method;
406 } catch (NoSuchMethodException e) {
407 // ignore and search next
408 }
409 }
410
411 throw new NoSuchMethodException("Method " + name + " with parameters " +
412 Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
413 }
414
415 /**
416 * Replace the value of a field containing a non null array, by a new array containing the
417 * elements of the original array plus the elements of extraElements.
418 * @param instance the instance whose field is to be modified.
419 * @param fieldName the field to modify.
420 * @param extraElements elements to append at the end of the array.
421 */
422 private static void expandFieldArray(Object instance, String fieldName,
423 Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
424 IllegalAccessException {
425 Field jlrField = findField(instance, fieldName);
426 Object[] original = (Object[]) jlrField.get(instance);
427 Object[] combined = (Object[]) Array.newInstance(
428 original.getClass().getComponentType(), original.length + extraElements.length);
429 System.arraycopy(original, 0, combined, 0, original.length);
430 System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
431 jlrField.set(instance, combined);
432 }
433
Yohann Rousseld79604b2014-07-08 16:50:10 +0200434 private static void clearOldDexDir(Context context) throws Exception {
Yohann Roussel590a07e2014-07-21 17:47:26 +0200435 File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
Yohann Rousseld79604b2014-07-08 16:50:10 +0200436 if (dexDir.isDirectory()) {
437 Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
438 File[] files = dexDir.listFiles();
439 if (files == null) {
440 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
441 return;
442 }
443 for (File oldFile : files) {
444 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size "
445 + oldFile.length());
446 if (!oldFile.delete()) {
447 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
448 } else {
449 Log.i(TAG, "Deleted old file " + oldFile.getPath());
450 }
451 }
452 if (!dexDir.delete()) {
453 Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath());
454 } else {
455 Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath());
456 }
457 }
458 }
459
Yohann Roussel50823412016-09-20 18:01:13 +0200460 private static File getDexDir(Context context, File dataDir, String secondaryFolderName)
Yohann Roussel606af942015-05-12 17:40:52 +0200461 throws IOException {
Yohann Roussel50823412016-09-20 18:01:13 +0200462 File cache = new File(dataDir, CODE_CACHE_NAME);
Yohann Roussel606af942015-05-12 17:40:52 +0200463 try {
464 mkdirChecked(cache);
465 } catch (IOException e) {
466 /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless
467 * files on disk if the device ever updates to android 5+. But since this seems to
468 * happen only on some devices running android 2, this should cause no pollution.
469 */
470 cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
471 mkdirChecked(cache);
472 }
Yohann Roussel50823412016-09-20 18:01:13 +0200473 File dexDir = new File(cache, secondaryFolderName);
Yohann Roussel606af942015-05-12 17:40:52 +0200474 mkdirChecked(dexDir);
475 return dexDir;
476 }
477
478 private static void mkdirChecked(File dir) throws IOException {
479 dir.mkdir();
480 if (!dir.isDirectory()) {
481 File parent = dir.getParentFile();
482 if (parent == null) {
483 Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null.");
484 } else {
485 Log.e(TAG, "Failed to create dir " + dir.getPath() +
486 ". parent file is a dir " + parent.isDirectory() +
487 ", a file " + parent.isFile() +
488 ", exists " + parent.exists() +
489 ", readable " + parent.canRead() +
490 ", writable " + parent.canWrite());
491 }
492 throw new IOException("Failed to create directory " + dir.getPath());
493 }
494 }
495
Maurice Chu667f9a82013-10-16 13:12:22 -0700496 /**
497 * Installer for platform versions 19.
498 */
499 private static final class V19 {
500
Yohann Rousselb2483812018-01-17 14:50:03 +0100501 static void install(ClassLoader loader,
Yohann Roussel99581452017-01-02 17:57:27 +0100502 List<? extends File> additionalClassPathEntries,
Maurice Chu667f9a82013-10-16 13:12:22 -0700503 File optimizedDirectory)
504 throws IllegalArgumentException, IllegalAccessException,
Yohann Roussel08133852018-01-15 11:44:07 +0100505 NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
506 IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700507 /* The patched class loader is expected to be a descendant of
508 * dalvik.system.BaseDexClassLoader. We modify its
509 * dalvik.system.DexPathList pathList field to append additional DEX
510 * file entries.
511 */
512 Field pathListField = findField(loader, "pathList");
513 Object dexPathList = pathListField.get(loader);
514 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
515 expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
516 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
517 suppressedExceptions));
518 if (suppressedExceptions.size() > 0) {
Yohann Roussel88117c32013-11-28 23:22:11 +0100519 for (IOException e : suppressedExceptions) {
520 Log.w(TAG, "Exception in makeDexElement", e);
521 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700522 Field suppressedExceptionsField =
Yohann Roussel74e66b82016-05-17 18:00:18 +0200523 findField(dexPathList, "dexElementsSuppressedExceptions");
Maurice Chu667f9a82013-10-16 13:12:22 -0700524 IOException[] dexElementsSuppressedExceptions =
Yohann Roussel74e66b82016-05-17 18:00:18 +0200525 (IOException[]) suppressedExceptionsField.get(dexPathList);
Maurice Chu667f9a82013-10-16 13:12:22 -0700526
527 if (dexElementsSuppressedExceptions == null) {
528 dexElementsSuppressedExceptions =
529 suppressedExceptions.toArray(
530 new IOException[suppressedExceptions.size()]);
531 } else {
532 IOException[] combined =
533 new IOException[suppressedExceptions.size() +
534 dexElementsSuppressedExceptions.length];
535 suppressedExceptions.toArray(combined);
536 System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
537 suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
538 dexElementsSuppressedExceptions = combined;
539 }
540
Yohann Roussel74e66b82016-05-17 18:00:18 +0200541 suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
Yohann Roussel08133852018-01-15 11:44:07 +0100542
543 IOException exception = new IOException("I/O exception during makeDexElement");
544 exception.initCause(suppressedExceptions.get(0));
545 throw exception;
Maurice Chu667f9a82013-10-16 13:12:22 -0700546 }
547 }
548
549 /**
550 * A wrapper around
551 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
552 */
553 private static Object[] makeDexElements(
554 Object dexPathList, ArrayList<File> files, File optimizedDirectory,
555 ArrayList<IOException> suppressedExceptions)
556 throws IllegalAccessException, InvocationTargetException,
557 NoSuchMethodException {
558 Method makeDexElements =
559 findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
560 ArrayList.class);
561
562 return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
563 suppressedExceptions);
564 }
565 }
566
567 /**
568 * Installer for platform versions 14, 15, 16, 17 and 18.
569 */
570 private static final class V14 {
571
Yohann Rousselb2483812018-01-17 14:50:03 +0100572 private static final int EXTRACTED_SUFFIX_LENGTH =
573 MultiDexExtractor.EXTRACTED_SUFFIX.length();
574
575 private final Constructor<?> elementConstructor;
576
577 static void install(ClassLoader loader,
578 List<? extends File> additionalClassPathEntries)
579 throws IOException, SecurityException, IllegalArgumentException,
580 ClassNotFoundException, NoSuchMethodException, InstantiationException,
581 IllegalAccessException, InvocationTargetException, NoSuchFieldException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700582 /* The patched class loader is expected to be a descendant of
583 * dalvik.system.BaseDexClassLoader. We modify its
584 * dalvik.system.DexPathList pathList field to append additional DEX
585 * file entries.
586 */
587 Field pathListField = findField(loader, "pathList");
588 Object dexPathList = pathListField.get(loader);
Yohann Rousselb2483812018-01-17 14:50:03 +0100589 expandFieldArray(dexPathList, "dexElements",
590 new V14().makeDexElements(additionalClassPathEntries));
591 }
592
593 private V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException {
594 Class<?> elementClass = Class.forName("dalvik.system.DexPathList$Element");
595 elementConstructor =
596 elementClass.getConstructor(File.class, ZipFile.class, DexFile.class);
597 elementConstructor.setAccessible(true);
Maurice Chu667f9a82013-10-16 13:12:22 -0700598 }
599
600 /**
Yohann Rousselb2483812018-01-17 14:50:03 +0100601 * An emulation of {@code private static final dalvik.system.DexPathList#makeDexElements}
602 * accepting only extracted secondary dex files.
603 * OS version is catching IOException and just logging some of them, this version is letting
604 * them through.
Maurice Chu667f9a82013-10-16 13:12:22 -0700605 */
Yohann Rousselb2483812018-01-17 14:50:03 +0100606 private Object[] makeDexElements(List<? extends File> files)
607 throws IOException, SecurityException, IllegalArgumentException,
608 InstantiationException, IllegalAccessException, InvocationTargetException {
609 Object[] elements = new Object[files.size()];
610 for (int i = 0; i < elements.length; i++) {
611 File file = files.get(i);
612 elements[i] = elementConstructor.newInstance(
613 file,
614 new ZipFile(file),
615 DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0));
616 }
617 return elements;
618 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700619
Yohann Rousselb2483812018-01-17 14:50:03 +0100620 /**
621 * Converts a zip file path of an extracted secondary dex to an output file path for an
622 * associated optimized dex file.
623 */
624 private static String optimizedPathFor(File path) {
625 // Any reproducible name ending with ".dex" should do but lets keep the same name
626 // as DexPathList.optimizedPathFor
627
628 File optimizedDirectory = path.getParentFile();
629 String fileName = path.getName();
630 String optimizedFileName =
631 fileName.substring(0, fileName.length() - EXTRACTED_SUFFIX_LENGTH)
632 + MultiDexExtractor.DEX_SUFFIX;
633 File result = new File(optimizedDirectory, optimizedFileName);
634 return result.getPath();
Maurice Chu667f9a82013-10-16 13:12:22 -0700635 }
636 }
637
638 /**
639 * Installer for platform versions 4 to 13.
640 */
641 private static final class V4 {
Yohann Rousselb2483812018-01-17 14:50:03 +0100642 static void install(ClassLoader loader,
Yohann Roussel99581452017-01-02 17:57:27 +0100643 List<? extends File> additionalClassPathEntries)
Maurice Chu667f9a82013-10-16 13:12:22 -0700644 throws IllegalArgumentException, IllegalAccessException,
645 NoSuchFieldException, IOException {
646 /* The patched class loader is expected to be a descendant of
647 * dalvik.system.DexClassLoader. We modify its
Yohann Roussel52eafa02013-11-21 11:46:53 +0100648 * fields mPaths, mFiles, mZips and mDexs to append additional DEX
Maurice Chu667f9a82013-10-16 13:12:22 -0700649 * file entries.
650 */
651 int extraSize = additionalClassPathEntries.size();
652
653 Field pathField = findField(loader, "path");
654
655 StringBuilder path = new StringBuilder((String) pathField.get(loader));
656 String[] extraPaths = new String[extraSize];
657 File[] extraFiles = new File[extraSize];
Maurice Chu66f379f2013-11-14 19:08:32 -0800658 ZipFile[] extraZips = new ZipFile[extraSize];
Maurice Chu667f9a82013-10-16 13:12:22 -0700659 DexFile[] extraDexs = new DexFile[extraSize];
Yohann Roussel99581452017-01-02 17:57:27 +0100660 for (ListIterator<? extends File> iterator = additionalClassPathEntries.listIterator();
Maurice Chu667f9a82013-10-16 13:12:22 -0700661 iterator.hasNext();) {
662 File additionalEntry = iterator.next();
663 String entryPath = additionalEntry.getAbsolutePath();
664 path.append(':').append(entryPath);
665 int index = iterator.previousIndex();
666 extraPaths[index] = entryPath;
667 extraFiles[index] = additionalEntry;
Maurice Chu66f379f2013-11-14 19:08:32 -0800668 extraZips[index] = new ZipFile(additionalEntry);
Maurice Chu667f9a82013-10-16 13:12:22 -0700669 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
670 }
671
672 pathField.set(loader, path.toString());
673 expandFieldArray(loader, "mPaths", extraPaths);
674 expandFieldArray(loader, "mFiles", extraFiles);
Maurice Chu66f379f2013-11-14 19:08:32 -0800675 expandFieldArray(loader, "mZips", extraZips);
Maurice Chu667f9a82013-10-16 13:12:22 -0700676 expandFieldArray(loader, "mDexs", extraDexs);
677 }
678 }
679
680}