blob: 562311c99e5319efcff09ce0a9c104abd78c994e [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;
Yohann Roussel602c6ca2014-03-28 17:35:02 +010025import dalvik.system.DexFile;
Maurice Chu667f9a82013-10-16 13:12:22 -070026import java.io.File;
27import java.io.IOException;
28import java.lang.reflect.Array;
Yohann Rousselb2483812018-01-17 14:50:03 +010029import java.lang.reflect.Constructor;
Maurice Chu667f9a82013-10-16 13:12:22 -070030import java.lang.reflect.Field;
31import java.lang.reflect.InvocationTargetException;
32import java.lang.reflect.Method;
33import java.util.ArrayList;
34import java.util.Arrays;
35import java.util.HashSet;
36import java.util.List;
37import java.util.ListIterator;
38import java.util.Set;
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020039import java.util.regex.Matcher;
40import java.util.regex.Pattern;
Maurice Chu66f379f2013-11-14 19:08:32 -080041import java.util.zip.ZipFile;
Maurice Chu667f9a82013-10-16 13:12:22 -070042
43/**
Sebastien Hertz8be7c7a2016-07-12 10:03:13 +020044 * MultiDex patches {@link Context#getClassLoader() the application context class
Maurice Chu667f9a82013-10-16 13:12:22 -070045 * loader} in order to load classes from more than one dex file. The primary
Yohann Rousseldd3cc222014-04-22 10:47:53 +020046 * {@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
Maurice Chu667f9a82013-10-16 13:12:22 -070049 * {@link #install(Context)}.
50 *
51 * <p/>
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020052 * 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.
Maurice Chu667f9a82013-10-16 13:12:22 -070054 */
55public final class MultiDex {
56
57 static final String TAG = "MultiDex";
58
Yohann Roussel590a07e2014-07-21 17:47:26 +020059 private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
60
Yohann Roussel606af942015-05-12 17:40:52 +020061 private static final String CODE_CACHE_NAME = "code_cache";
62
63 private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "secondary-dexes";
Maurice Chu667f9a82013-10-16 13:12:22 -070064
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020065 private static final int MAX_SUPPORTED_SDK_VERSION = 20;
Maurice Chu667f9a82013-10-16 13:12:22 -070066
67 private static final int MIN_SDK_VERSION = 4;
68
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020069 private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
70
71 private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
72
Yohann Roussel50823412016-09-20 18:01:13 +020073 private static final String NO_KEY_PREFIX = "";
74
75 private static final Set<File> installedApk = new HashSet<File>();
Maurice Chu667f9a82013-10-16 13:12:22 -070076
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020077 private static final boolean IS_VM_MULTIDEX_CAPABLE =
78 isVMMultidexCapable(System.getProperty("java.vm.version"));
79
Maurice Chu667f9a82013-10-16 13:12:22 -070080 private MultiDex() {}
81
82 /**
83 * Patches the application context class loader by appending extra dex files
Yohann Rousseldd3cc222014-04-22 10:47:53 +020084 * 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.
Maurice Chu667f9a82013-10-16 13:12:22 -070087 *
88 * @param context application context.
89 * @throws RuntimeException if an error occurred preventing the classloader
90 * extension.
91 */
92 public static void install(Context context) {
Yohann Roussel50823412016-09-20 18:01:13 +020093 Log.i(TAG, "Installing application");
Yohann Roussel7b86f7e2014-04-22 11:12:54 +020094 if (IS_VM_MULTIDEX_CAPABLE) {
95 Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
96 return;
97 }
Maurice Chu667f9a82013-10-16 13:12:22 -070098
99 if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
Yohann Roussel50823412016-09-20 18:01:13 +0200100 throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
Maurice Chu667f9a82013-10-16 13:12:22 -0700101 + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
102 }
103
Maurice Chu667f9a82013-10-16 13:12:22 -0700104 try {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200105 ApplicationInfo applicationInfo = getApplicationInfo(context);
Maurice Chu667f9a82013-10-16 13:12:22 -0700106 if (applicationInfo == null) {
Yohann Roussel50823412016-09-20 18:01:13 +0200107 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,
Yohann Roussel08133852018-01-15 11:44:07 +0100116 NO_KEY_PREFIX,
117 true);
Yohann Roussel50823412016-09-20 18:01:13 +0200118
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,
Yohann Roussel08133852018-01-15 11:44:07 +0100174 instrumentationPrefix,
175 false);
Yohann Roussel50823412016-09-20 18:01:13 +0200176
177 doInstallation(targetContext,
178 new File(applicationInfo.sourceDir),
179 dataDir,
180 CODE_CACHE_SECONDARY_FOLDER_NAME,
Yohann Roussel08133852018-01-15 11:44:07 +0100181 NO_KEY_PREFIX,
182 false);
Maurice Chu667f9a82013-10-16 13:12:22 -0700183 } catch (Exception e) {
Yohann Roussel50823412016-09-20 18:01:13 +0200184 Log.e(TAG, "MultiDex installation failure", e);
185 throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
Maurice Chu667f9a82013-10-16 13:12:22 -0700186 }
Yohann Roussel50823412016-09-20 18:01:13 +0200187 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.
Yohann Roussel08133852018-01-15 11:44:07 +0100197 * @param reinstallOnPatchRecoverableException if set to true, will attempt a clean extraction
198 * if a possibly recoverable exception occurs during classloader patching.
Yohann Roussel50823412016-09-20 18:01:13 +0200199 */
200 private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
Yohann Roussel08133852018-01-15 11:44:07 +0100201 String secondaryFolderName, String prefsKeyPrefix,
202 boolean reinstallOnPatchRecoverableException) throws IOException,
Yohann Roussel50823412016-09-20 18:01:13 +0200203 IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
Yohann Rousselb2483812018-01-17 14:50:03 +0100204 InvocationTargetException, NoSuchMethodException, SecurityException,
205 ClassNotFoundException, InstantiationException {
Yohann Roussel50823412016-09-20 18:01:13 +0200206 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,
Yohann Rousselb2483812018-01-17 14:50:03 +0100343 InvocationTargetException, NoSuchMethodException, IOException, SecurityException,
344 ClassNotFoundException, InstantiationException {
Maurice Chucc63eda2013-12-02 15:39:59 -0800345 if (!files.isEmpty()) {
346 if (Build.VERSION.SDK_INT >= 19) {
347 V19.install(loader, files, dexDir);
348 } else if (Build.VERSION.SDK_INT >= 14) {
Yohann Rousselb2483812018-01-17 14:50:03 +0100349 V14.install(loader, files);
Maurice Chucc63eda2013-12-02 15:39:59 -0800350 } else {
351 V4.install(loader, files);
352 }
353 }
354 }
355
356 /**
Maurice Chu667f9a82013-10-16 13:12:22 -0700357 * 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 */
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 */
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 */
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
Yohann Rousseld79604b2014-07-08 16:50:10 +0200432 private static void clearOldDexDir(Context context) throws Exception {
Yohann Roussel590a07e2014-07-21 17:47:26 +0200433 File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
Yohann Rousseld79604b2014-07-08 16:50:10 +0200434 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
Yohann Roussel50823412016-09-20 18:01:13 +0200458 private static File getDexDir(Context context, File dataDir, String secondaryFolderName)
Yohann Roussel606af942015-05-12 17:40:52 +0200459 throws IOException {
Yohann Roussel50823412016-09-20 18:01:13 +0200460 File cache = new File(dataDir, CODE_CACHE_NAME);
Yohann Roussel606af942015-05-12 17:40:52 +0200461 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 }
Yohann Roussel50823412016-09-20 18:01:13 +0200471 File dexDir = new File(cache, secondaryFolderName);
Yohann Roussel606af942015-05-12 17:40:52 +0200472 mkdirChecked(dexDir);
473 return dexDir;
474 }
475
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
Maurice Chu667f9a82013-10-16 13:12:22 -0700494 /**
495 * Installer for platform versions 19.
496 */
497 private static final class V19 {
498
Yohann Rousselb2483812018-01-17 14:50:03 +0100499 static void install(ClassLoader loader,
Yohann Roussel99581452017-01-02 17:57:27 +0100500 List<? extends File> additionalClassPathEntries,
Maurice Chu667f9a82013-10-16 13:12:22 -0700501 File optimizedDirectory)
502 throws IllegalArgumentException, IllegalAccessException,
Yohann Roussel08133852018-01-15 11:44:07 +0100503 NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
504 IOException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700505 /* 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) {
Yohann Roussel88117c32013-11-28 23:22:11 +0100517 for (IOException e : suppressedExceptions) {
518 Log.w(TAG, "Exception in makeDexElement", e);
519 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700520 Field suppressedExceptionsField =
Yohann Roussel74e66b82016-05-17 18:00:18 +0200521 findField(dexPathList, "dexElementsSuppressedExceptions");
Maurice Chu667f9a82013-10-16 13:12:22 -0700522 IOException[] dexElementsSuppressedExceptions =
Yohann Roussel74e66b82016-05-17 18:00:18 +0200523 (IOException[]) suppressedExceptionsField.get(dexPathList);
Maurice Chu667f9a82013-10-16 13:12:22 -0700524
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
Yohann Roussel74e66b82016-05-17 18:00:18 +0200539 suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
Yohann Roussel08133852018-01-15 11:44:07 +0100540
541 IOException exception = new IOException("I/O exception during makeDexElement");
542 exception.initCause(suppressedExceptions.get(0));
543 throw exception;
Maurice Chu667f9a82013-10-16 13:12:22 -0700544 }
545 }
546
547 /**
548 * A wrapper around
549 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
550 */
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
Yohann Roussel38be0f42018-01-31 11:25:32 +0100570 private interface ElementConstructor {
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
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
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
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
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
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
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
Yohann Rousselb2483812018-01-17 14:50:03 +0100643 private static final int EXTRACTED_SUFFIX_LENGTH =
644 MultiDexExtractor.EXTRACTED_SUFFIX.length();
645
Yohann Roussel38be0f42018-01-31 11:25:32 +0100646 private final ElementConstructor elementConstructor;
Yohann Rousselb2483812018-01-17 14:50:03 +0100647
648 static void install(ClassLoader loader,
649 List<? extends File> additionalClassPathEntries)
650 throws IOException, SecurityException, IllegalArgumentException,
651 ClassNotFoundException, NoSuchMethodException, InstantiationException,
652 IllegalAccessException, InvocationTargetException, NoSuchFieldException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700653 /* 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);
Yohann Roussel38be0f42018-01-31 11:25:32 +0100660 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 }
Yohann Rousselb2483812018-01-17 14:50:03 +0100669 }
670
671 private V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException {
Yohann Roussel38be0f42018-01-31 11:25:32 +0100672 ElementConstructor constructor;
Yohann Rousselb2483812018-01-17 14:50:03 +0100673 Class<?> elementClass = Class.forName("dalvik.system.DexPathList$Element");
Yohann Roussel38be0f42018-01-31 11:25:32 +0100674 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;
Maurice Chu667f9a82013-10-16 13:12:22 -0700684 }
685
686 /**
Yohann Rousselb2483812018-01-17 14:50:03 +0100687 * 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.
Maurice Chu667f9a82013-10-16 13:12:22 -0700691 */
Yohann Rousselb2483812018-01-17 14:50:03 +0100692 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,
Yohann Rousselb2483812018-01-17 14:50:03 +0100700 DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0));
701 }
702 return elements;
703 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700704
Yohann Rousselb2483812018-01-17 14:50:03 +0100705 /**
706 * Converts a zip file path of an extracted secondary dex to an output file path for an
707 * associated optimized dex file.
708 */
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();
Maurice Chu667f9a82013-10-16 13:12:22 -0700720 }
721 }
722
723 /**
724 * Installer for platform versions 4 to 13.
725 */
726 private static final class V4 {
Yohann Rousselb2483812018-01-17 14:50:03 +0100727 static void install(ClassLoader loader,
Yohann Roussel99581452017-01-02 17:57:27 +0100728 List<? extends File> additionalClassPathEntries)
Maurice Chu667f9a82013-10-16 13:12:22 -0700729 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
Yohann Roussel52eafa02013-11-21 11:46:53 +0100733 * fields mPaths, mFiles, mZips and mDexs to append additional DEX
Maurice Chu667f9a82013-10-16 13:12:22 -0700734 * 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];
Maurice Chu66f379f2013-11-14 19:08:32 -0800743 ZipFile[] extraZips = new ZipFile[extraSize];
Maurice Chu667f9a82013-10-16 13:12:22 -0700744 DexFile[] extraDexs = new DexFile[extraSize];
Yohann Roussel99581452017-01-02 17:57:27 +0100745 for (ListIterator<? extends File> iterator = additionalClassPathEntries.listIterator();
Maurice Chu667f9a82013-10-16 13:12:22 -0700746 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;
Maurice Chu66f379f2013-11-14 19:08:32 -0800753 extraZips[index] = new ZipFile(additionalEntry);
Maurice Chu667f9a82013-10-16 13:12:22 -0700754 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);
Maurice Chu66f379f2013-11-14 19:08:32 -0800760 expandFieldArray(loader, "mZips", extraZips);
Maurice Chu667f9a82013-10-16 13:12:22 -0700761 expandFieldArray(loader, "mDexs", extraDexs);
762 }
763 }
764
765}