content: /*
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.support.multidex;
  
  import android.app.Application;
  import android.app.Instrumentation;
  import android.content.Context;
  import android.content.pm.ApplicationInfo;
  import android.os.Build;
  import android.util.Log;
  
  import dalvik.system.DexFile;
  
  import java.io.File;
  import java.io.IOException;
  import java.lang.reflect.Array;
  import java.lang.reflect.Field;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashSet;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Set;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import java.util.zip.ZipFile;
  
  /**
   * MultiDex patches {@link Context#getClassLoader() the application context class
   * loader} in order to load classes from more than one dex file. The primary
   * {@code classes.dex} must contain the classes necessary for calling this
   * class methods. Secondary dex files named classes2.dex, classes3.dex... found
   * in the application apk will be added to the classloader after first call to
   * {@link #install(Context)}.
   * <p>
   * <p/>
   * This library provides compatibility for platforms with API level 4 through 20. This library does
   * nothing on newer versions of the platform which provide built-in support for secondary dex files.
   */
  public final class MultiDex {
  
      static final String TAG = "MultiDex";
  
      private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
  
      private static final String CODE_CACHE_NAME = "code_cache";
  
      private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "secondary-dexes";
  
      private static final int MAX_SUPPORTED_SDK_VERSION = 20;
  
      private static final int MIN_SDK_VERSION = 4;
  
      private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
  
      private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
  
      private static final String NO_KEY_PREFIX = "";
  
      private static final Set<File> installedApk = new HashSet<File>();
  
      public static final boolean IS_VM_MULTIDEX_CAPABLE =
              isVMMultidexCapable(System.getProperty("java.vm.version"));
  
      private MultiDex() {
      }
  
      /**
       * Patches the application context class loader by appending extra dex files
       * loaded from the application apk. This method should be called in the
       * attachBaseContext of your {@link Application}, see
       * {@link MultiDexApplication} for more explanation and an example.
       *
       * @param context application context.
       * @throws RuntimeException if an error occurred preventing the classloader
       *                          extension.
       */
      public static void install(Context context) {
          Log.i(TAG, "Installing application");
          if (IS_VM_MULTIDEX_CAPABLE) {
              Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
              return;
          }
  
          if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
              throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
                      + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
          }
  
          try {
              ApplicationInfo applicationInfo = getApplicationInfo(context);
              if (applicationInfo == null) {
                  Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
                          + " MultiDex support library is disabled.");
                  return;
              }
  
              doInstallation(context,
                      new File(applicationInfo.sourceDir),
                      new File(applicationInfo.dataDir),
                      CODE_CACHE_SECONDARY_FOLDER_NAME,
                      NO_KEY_PREFIX);
  
          } catch (Exception e) {
              Log.e(TAG, "MultiDex installation failure", e);
              throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
          }
          Log.i(TAG, "install done");
      }
  
      /**
       * Patches the instrumentation context class loader by appending extra dex files
       * loaded from the instrumentation apk and the application apk. This method should be called in
       * the onCreate of your {@link Instrumentation}, see
       * {@link com.android.test.runner.MultiDexTestRunner} for an example.
       *
       * @param instrumentationContext instrumentation context.
       * @param targetContext          target application context.
       * @throws RuntimeException if an error occurred preventing the classloader
       *                          extension.
       */
      public static void installInstrumentation(Context instrumentationContext,
                                                Context targetContext) {
          Log.i(TAG, "Installing instrumentation");
  
          if (IS_VM_MULTIDEX_CAPABLE) {
              Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
              return;
          }
  
          if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
              throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
                      + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
          }
          try {
  
              ApplicationInfo instrumentationInfo = getApplicationInfo(instrumentationContext);
              if (instrumentationInfo == null) {
                  Log.i(TAG, "No ApplicationInfo available for instrumentation, i.e. running on a"
                          + " test Context: MultiDex support library is disabled.");
                  return;
              }
  
              ApplicationInfo applicationInfo = getApplicationInfo(targetContext);
              if (applicationInfo == null) {
                  Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
                          + " MultiDex support library is disabled.");
                  return;
              }
  
              String instrumentationPrefix = instrumentationContext.getPackageName() + ".";
  
              File dataDir = new File(applicationInfo.dataDir);
  
              doInstallation(targetContext,
                      new File(instrumentationInfo.sourceDir),
                      dataDir,
                      instrumentationPrefix + CODE_CACHE_SECONDARY_FOLDER_NAME,
                      instrumentationPrefix);
  
              doInstallation(targetContext,
                      new File(applicationInfo.sourceDir),
                      dataDir,
                      CODE_CACHE_SECONDARY_FOLDER_NAME,
                      NO_KEY_PREFIX);
          } catch (Exception e) {
              Log.e(TAG, "MultiDex installation failure", e);
              throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
          }
          Log.i(TAG, "Installation done");
      }
  
      /**
       * @param mainContext         context used to get filesDir, to save preference and to get the
       *                            classloader to patch.
       * @param sourceApk           Apk file.
       * @param dataDir             data directory to use for code cache simulation.
       * @param secondaryFolderName name of the folder for storing extractions.
       * @param prefsKeyPrefix      prefix of all stored preference keys.
       */
      private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
                                         String secondaryFolderName, String prefsKeyPrefix) throws IOException,
              IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
              InvocationTargetException, NoSuchMethodException {
          synchronized (installedApk) {
              if (installedApk.contains(sourceApk)) {
                  return;
              }
              installedApk.add(sourceApk);
  
              if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
                  Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
                          + Build.VERSION.SDK_INT + ": SDK version higher than "
                          + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
                          + "runtime with built-in multidex capabilty but it's not the "
                          + "case here: java.vm.version=\""
                          + System.getProperty("java.vm.version") + "\"");
              }
  
              /* The patched class loader is expected to be a descendant of
               * dalvik.system.BaseDexClassLoader. We modify its
               * dalvik.system.DexPathList pathList field to append additional DEX
               * file entries.
               */
              ClassLoader loader;
              try {
                  loader = mainContext.getClassLoader();
              } catch (RuntimeException e) {
                  /* Ignore those exceptions so that we don't break tests relying on Context like
                   * a android.test.mock.MockContext or a android.content.ContextWrapper with a
                   * null base Context.
                   */
                  Log.w(TAG, "Failure while trying to obtain Context class loader. " +
                          "Must be running in test mode. Skip patching.", e);
                  return;
              }
              if (loader == null) {
                  // Note, the context class loader is null when running Robolectric tests.
                  Log.e(TAG,
                          "Context class loader is null. Must be running in test mode. "
                                  + "Skip patching.");
                  return;
              }
  
              try {
                  clearOldDexDir(mainContext);
              } catch (Throwable t) {
                  Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                          + "continuing without cleaning.", t);
              }
  
              File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
              List<? extends File> files =
                      MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);
              installSecondaryDexes(loader, dexDir, files);
          }
      }
  
      private static ApplicationInfo getApplicationInfo(Context context) {
          try {
              /* Due to package install races it is possible for a process to be started from an old
               * apk even though that apk has been replaced. Querying for ApplicationInfo by package
               * name may return information for the new apk, leading to a runtime with the old main
               * dex file and new secondary dex files. This leads to various problems like
               * ClassNotFoundExceptions. Using context.getApplicationInfo() should result in the
               * process having a consistent view of the world (even if it is of the old world). The
               * package install races are eventually resolved and old processes are killed.
               */
              return context.getApplicationInfo();
          } catch (RuntimeException e) {
              /* Ignore those exceptions so that we don't break tests relying on Context like
               * a android.test.mock.MockContext or a android.content.ContextWrapper with a null
               * base Context.
               */
              Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " +
                      "Must be running in test mode. Skip patching.", e);
              return null;
          }
      }
  
      /**
       * Identifies if the current VM has a native support for multidex, meaning there is no need for
       * additional installation by this library.
       *
       * @return true if the VM handles multidex
       */
      /* package visible for test */
      static boolean isVMMultidexCapable(String versionString) {
          boolean isMultidexCapable = false;
          if (versionString != null) {
              Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
              if (matcher.matches()) {
                  try {
                      int major = Integer.parseInt(matcher.group(1));
                      int minor = Integer.parseInt(matcher.group(2));
                      isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
                              || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
                              && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
                  } catch (NumberFormatException e) {
                      // let isMultidexCapable be false
                  }
              }
          }
          Log.i(TAG, "VM with version " + versionString +
                  (isMultidexCapable ?
                          " has multidex support" :
                          " does not have multidex support"));
          return isMultidexCapable;
      }
  
      private static void installSecondaryDexes(ClassLoader loader, File dexDir,
                                                List<? extends File> files)
              throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
              InvocationTargetException, NoSuchMethodException, IOException {
          if (!files.isEmpty()) {
              if (Build.VERSION.SDK_INT >= 19) {
                  V19.install(loader, files, dexDir);
              } else if (Build.VERSION.SDK_INT >= 14) {
                  V14.install(loader, files, dexDir);
              } else {
                  V4.install(loader, files);
              }
          }
      }
  
      /**
       * Locates a given field anywhere in the class inheritance hierarchy.
       *
       * @param instance an object to search the field into.
       * @param name     field name
       * @return a field object
       * @throws NoSuchFieldException if the field cannot be located
       */
      private static Field findField(Object instance, String name) throws NoSuchFieldException {
          for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
              try {
                  Field field = clazz.getDeclaredField(name);
  
  
                  if (!field.isAccessible()) {
                      field.setAccessible(true);
                  }
  
                  return field;
              } catch (NoSuchFieldException e) {
                  // ignore and search next
              }
          }
  
          throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
      }
  
      /**
       * Locates a given method anywhere in the class inheritance hierarchy.
       *
       * @param instance       an object to search the method into.
       * @param name           method name
       * @param parameterTypes method parameter types
       * @return a method object
       * @throws NoSuchMethodException if the method cannot be located
       */
      private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
              throws NoSuchMethodException {
          for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
              try {
                  Method method = clazz.getDeclaredMethod(name, parameterTypes);
  
  
                  if (!method.isAccessible()) {
                      method.setAccessible(true);
                  }
  
                  return method;
              } catch (NoSuchMethodException e) {
                  // ignore and search next
              }
          }
  
          throw new NoSuchMethodException("Method " + name + " with parameters " +
                  Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
      }
  
      /**
       * Replace the value of a field containing a non null array, by a new array containing the
       * elements of the original array plus the elements of extraElements.
       *
       * @param instance      the instance whose field is to be modified.
       * @param fieldName     the field to modify.
       * @param extraElements elements to append at the end of the array.
       */
      private static void expandFieldArray(Object instance, String fieldName,
                                           Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
              IllegalAccessException {
          Field jlrField = findField(instance, fieldName);
          Object[] original = (Object[]) jlrField.get(instance);
          Object[] combined = (Object[]) Array.newInstance(
                  original.getClass().getComponentType(), original.length + extraElements.length);
          System.arraycopy(original, 0, combined, 0, original.length);
          System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
          jlrField.set(instance, combined);
      }
  
      private static void clearOldDexDir(Context context) throws Exception {
          File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME);
          if (dexDir.isDirectory()) {
              Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
              File[] files = dexDir.listFiles();
              if (files == null) {
                  Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
                  return;
              }
              for (File oldFile : files) {
                  Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size "
                          + oldFile.length());
                  if (!oldFile.delete()) {
                      Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
                  } else {
                      Log.i(TAG, "Deleted old file " + oldFile.getPath());
                  }
              }
              if (!dexDir.delete()) {
                  Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath());
              } else {
                  Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath());
              }
          }
      }
  
      private static File getDexDir(Context context, File dataDir, String secondaryFolderName)
              throws IOException {
          File cache = new File(dataDir, CODE_CACHE_NAME);
          try {
              mkdirChecked(cache);
          } catch (IOException e) {
              /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless
               * files on disk if the device ever updates to android 5+. But since this seems to
               * happen only on some devices running android 2, this should cause no pollution.
               */
              cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
              mkdirChecked(cache);
          }
          File dexDir = new File(cache, secondaryFolderName);
          mkdirChecked(dexDir);
          return dexDir;
      }
  
      private static void mkdirChecked(File dir) throws IOException {
          dir.mkdir();
          if (!dir.isDirectory()) {
              File parent = dir.getParentFile();
              if (parent == null) {
                  Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null.");
              } else {
                  Log.e(TAG, "Failed to create dir " + dir.getPath() +
                          ". parent file is a dir " + parent.isDirectory() +
                          ", a file " + parent.isFile() +
                          ", exists " + parent.exists() +
                          ", readable " + parent.canRead() +
                          ", writable " + parent.canWrite());
              }
              throw new IOException("Failed to create directory " + dir.getPath());
          }
      }
  
      /**
       * Installer for platform versions 19.
       */
      private static final class V19 {
  
          private static void install(ClassLoader loader,
                                      List<? extends File> additionalClassPathEntries,
                                      File optimizedDirectory)
                  throws IllegalArgumentException, IllegalAccessException,
                  NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
              /* The patched class loader is expected to be a descendant of
               * dalvik.system.BaseDexClassLoader. We modify its
               * dalvik.system.DexPathList pathList field to append additional DEX
               * file entries.
               */
              Field pathListField = findField(loader, "pathList");
              Object dexPathList = pathListField.get(loader);
              ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
              expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                      new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                      suppressedExceptions));
              if (suppressedExceptions.size() > 0) {
                  for (IOException e : suppressedExceptions) {
                      Log.w(TAG, "Exception in makeDexElement", e);
                  }
                  Field suppressedExceptionsField =
                          findField(dexPathList, "dexElementsSuppressedExceptions");
                  IOException[] dexElementsSuppressedExceptions =
                          (IOException[]) suppressedExceptionsField.get(dexPathList);
  
                  if (dexElementsSuppressedExceptions == null) {
                      dexElementsSuppressedExceptions =
                              suppressedExceptions.toArray(
                                      new IOException[suppressedExceptions.size()]);
                  } else {
                      IOException[] combined =
                              new IOException[suppressedExceptions.size() +
                                      dexElementsSuppressedExceptions.length];
                      suppressedExceptions.toArray(combined);
                      System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
                              suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                      dexElementsSuppressedExceptions = combined;
                  }
  
                  suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
              }
          }
  
          /**
           * A wrapper around
           * {@code private static final dalvik.system.DexPathList#makeDexElements}.
           */
          private static Object[] makeDexElements(
                  Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                  ArrayList<IOException> suppressedExceptions)
                  throws IllegalAccessException, InvocationTargetException,
                  NoSuchMethodException {
              Method makeDexElements =
                      findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                              ArrayList.class);
  
              IDexElementsMethodInvoker invoker = new DexElementsMethodInvokerV19(dexPathList, optimizedDirectory, suppressedExceptions, makeDexElements);
              DexElementsMaker dexElementsMaker = new DexElementsMaker(files, invoker);
  
              return dexElementsMaker.make();
          }
      }
  
      /**
       * Installer for platform versions 14, 15, 16, 17 and 18.
       */
      private static final class V14 {
  
          private static void install(ClassLoader loader,
                                      List<? extends File> additionalClassPathEntries,
                                      File optimizedDirectory)
                  throws IllegalArgumentException, IllegalAccessException,
                  NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
              /* The patched class loader is expected to be a descendant of
               * dalvik.system.BaseDexClassLoader. We modify its
               * dalvik.system.DexPathList pathList field to append additional DEX
               * file entries.
               */
              Field pathListField = findField(loader, "pathList");
              Object dexPathList = pathListField.get(loader);
              expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                      new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
          }
  
          /**
           * A wrapper around
           * {@code private static final dalvik.system.DexPathList#makeDexElements}.
           */
          private static Object[] makeDexElements(
                  Object dexPathList, ArrayList<File> files, File optimizedDirectory)
                  throws IllegalAccessException, InvocationTargetException,
                  NoSuchMethodException {
              Method makeDexElements =
                      findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
  
              IDexElementsMethodInvoker invoker = new DexElementsMethodInvokerV14(dexPathList, optimizedDirectory, makeDexElements);
              DexElementsMaker dexElementsMaker = new DexElementsMaker(files, invoker);
  
              return dexElementsMaker.make();
          }
      }
  
      /**
       * Installer for platform versions 4 to 13.
       */
      private static final class V4 {
          private static void install(ClassLoader loader,
                                      List<? extends File> additionalClassPathEntries)
                  throws IllegalArgumentException, IllegalAccessException,
                  NoSuchFieldException, IOException {
              /* The patched class loader is expected to be a descendant of
               * dalvik.system.DexClassLoader. We modify its
               * fields mPaths, mFiles, mZips and mDexs to append additional DEX
               * file entries.
               */
              int extraSize = additionalClassPathEntries.size();
  
              Field pathField = findField(loader, "path");
  
              StringBuilder path = new StringBuilder((String) pathField.get(loader));
              String[] extraPaths = new String[extraSize];
              File[] extraFiles = new File[extraSize];
              ZipFile[] extraZips = new ZipFile[extraSize];
              DexFile[] extraDexs = new DexFile[extraSize];
              for (ListIterator<? extends File> iterator = additionalClassPathEntries.listIterator();
                   iterator.hasNext(); ) {
                  File additionalEntry = iterator.next();
                  String entryPath = additionalEntry.getAbsolutePath();
                  path.append(':').append(entryPath);
                  int index = iterator.previousIndex();
                  extraPaths[index] = entryPath;
                  extraFiles[index] = additionalEntry;
                  extraZips[index] = new ZipFile(additionalEntry);
                  extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
              }
  
              pathField.set(loader, path.toString());
              expandFieldArray(loader, "mPaths", extraPaths);
              expandFieldArray(loader, "mFiles", extraFiles);
              expandFieldArray(loader, "mZips", extraZips);
              expandFieldArray(loader, "mDexs", extraDexs);
          }
      }
  
  }
  