Use the class loader context when optimizing secondary dex files

Record the class loader context for secondary dex loads and pass it to
dexopt during compilation.

The class loader context is passed from libcore every time a
BaseDexClassLoader is created and its recorded in the package dex usage
file.

Note that the context may be:
- unknown: if the dex file was not use after the the upgrade and its
context was not yet updated
- unsupported: if any of the class loaders from the loading context is
unsupported (only PathClassLoader and DelegateLastClassLoader are
supported).
- variable: if it changes over time, form one run to another.

In all the above cases the old compilation behavior is preserved for
now.(i.e. the dex file with be compiled with SKIP_SHARED_LIBRARY_CHECK)

Bug: 38138251
Test: runtest -x
services/tests/servicestests/src/com/android/server/pm/dex/
      adb shell cmd package compile -f -m quicken ^Csecondary-dex
com.google.android.gms

(cherry picked from commit 3bec94d78b0a66c4fa5cebd851ea33bcc51916b0)

Change-Id: Ie8b78c7c0d5de43733b3d116f8dcb3a65324cca8
diff --git a/core/java/android/app/DexLoadReporter.java b/core/java/android/app/DexLoadReporter.java
index 13f288a..371cd12 100644
--- a/core/java/android/app/DexLoadReporter.java
+++ b/core/java/android/app/DexLoadReporter.java
@@ -28,6 +28,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -86,29 +87,50 @@
     }
 
     @Override
-    public void report(List<String> dexPaths) {
-        if (dexPaths.isEmpty()) {
+    public void report(List<BaseDexClassLoader> classLoadersChain, List<String> classPaths) {
+        if (classLoadersChain.size() != classPaths.size()) {
+            Slog.wtf(TAG, "Bad call to DexLoadReporter: argument size mismatch");
             return;
         }
+        if (classPaths.isEmpty()) {
+            Slog.wtf(TAG, "Bad call to DexLoadReporter: empty dex paths");
+            return;
+        }
+
+        // The first element of classPaths is the list of dex files that should be registered.
+        // The classpath is represented as a list of dex files separated by File.pathSeparator.
+        String[] dexPathsForRegistration = classPaths.get(0).split(File.pathSeparator);
+        if (dexPathsForRegistration.length == 0) {
+            // No dex files to register.
+            return;
+        }
+
         // Notify the package manager about the dex loads unconditionally.
         // The load might be for either a primary or secondary dex file.
-        notifyPackageManager(dexPaths);
-        // Check for secondary dex files and register them for profiling if
-        // possible.
-        registerSecondaryDexForProfiling(dexPaths);
+        notifyPackageManager(classLoadersChain, classPaths);
+        // Check for secondary dex files and register them for profiling if possible.
+        // Note that we only register the dex paths belonging to the first class loader.
+        registerSecondaryDexForProfiling(dexPathsForRegistration);
     }
 
-    private void notifyPackageManager(List<String> dexPaths) {
+    private void notifyPackageManager(List<BaseDexClassLoader> classLoadersChain,
+            List<String> classPaths) {
+        // Get the class loader names for the binder call.
+        List<String> classLoadersNames = new ArrayList<>(classPaths.size());
+        for (BaseDexClassLoader bdc : classLoadersChain) {
+            classLoadersNames.add(bdc.getClass().getName());
+        }
         String packageName = ActivityThread.currentPackageName();
         try {
             ActivityThread.getPackageManager().notifyDexLoad(
-                    packageName, dexPaths, VMRuntime.getRuntime().vmInstructionSet());
+                    packageName, classLoadersNames, classPaths,
+                    VMRuntime.getRuntime().vmInstructionSet());
         } catch (RemoteException re) {
             Slog.e(TAG, "Failed to notify PM about dex load for package " + packageName, re);
         }
     }
 
-    private void registerSecondaryDexForProfiling(List<String> dexPaths) {
+    private void registerSecondaryDexForProfiling(String[] dexPaths) {
         if (!SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)) {
             return;
         }
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 9b795aa..b20a56c 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -469,11 +469,19 @@
      * Notify the package manager that a list of dex files have been loaded.
      *
      * @param loadingPackageName the name of the package who performs the load
-     * @param dexPats the list of the dex files paths that have been loaded
+     * @param classLoadersNames the names of the class loaders present in the loading chain. The
+     *    list encodes the class loader chain in the natural order. The first class loader has
+     *    the second one as its parent and so on. The dex files present in the class path of the
+     *    first class loader will be recorded in the usage file.
+     * @param classPaths the class paths corresponding to the class loaders names from
+     *     {@param classLoadersNames}. The the first element corresponds to the first class loader
+     *     and so on. A classpath is represented as a list of dex files separated by
+     *     {@code File.pathSeparator}.
+     *     The dex files found in the first class path will be recorded in the usage file.
      * @param loaderIsa the ISA of the loader process
      */
-    oneway void notifyDexLoad(String loadingPackageName, in List<String> dexPaths,
-            String loaderIsa);
+    oneway void notifyDexLoad(String loadingPackageName, in List<String> classLoadersNames,
+            in List<String> classPaths, String loaderIsa);
 
     /**
      * Register an application dex module with the package manager.
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 41bc7f2..dabd35c 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -33,12 +33,12 @@
 import com.android.server.pm.Installer.InstallerException;
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.dex.DexoptUtils;
+import com.android.server.pm.dex.PackageDexUsage;
 
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 
 import dalvik.system.DexFile;
 
@@ -251,13 +251,12 @@
      * throwing exceptions). Or maybe make a separate call to installd to get DexOptNeeded, though
      * that seems wasteful.
      */
-    public int dexOptSecondaryDexPath(ApplicationInfo info, String path, Set<String> isas,
-            String compilerFilter, boolean isUsedByOtherApps, boolean downgrade) {
+    public int dexOptSecondaryDexPath(ApplicationInfo info, String path,
+            PackageDexUsage.DexUseInfo dexUseInfo, DexoptOptions options) {
         synchronized (mInstallLock) {
             final long acquireTime = acquireWakeLockLI(info.uid);
             try {
-                return dexOptSecondaryDexPathLI(info, path, isas, compilerFilter,
-                        isUsedByOtherApps, downgrade);
+                return dexOptSecondaryDexPathLI(info, path, dexUseInfo, options);
             } finally {
                 releaseWakeLockLI(acquireTime);
             }
@@ -298,9 +297,16 @@
     }
 
     @GuardedBy("mInstallLock")
-    private int dexOptSecondaryDexPathLI(ApplicationInfo info, String path, Set<String> isas,
-            String compilerFilter, boolean isUsedByOtherApps, boolean downgrade) {
-        compilerFilter = getRealCompilerFilter(info, compilerFilter, isUsedByOtherApps);
+    private int dexOptSecondaryDexPathLI(ApplicationInfo info, String path,
+            PackageDexUsage.DexUseInfo dexUseInfo, DexoptOptions options) {
+        if (options.isDexoptOnlySharedDex() && !dexUseInfo.isUsedByOtherApps()) {
+            // We are asked to optimize only the dex files used by other apps and this is not
+            // on of them: skip it.
+            return DEX_OPT_SKIPPED;
+        }
+
+        String compilerFilter = getRealCompilerFilter(info, options.getCompilerFilter(),
+                dexUseInfo.isUsedByOtherApps());
         // Get the dexopt flags after getRealCompilerFilter to make sure we get the correct flags.
         // Secondary dex files are currently not compiled at boot.
         int dexoptFlags = getDexFlags(info, compilerFilter, /* bootComplete */ true)
@@ -317,20 +323,32 @@
             return DEX_OPT_FAILED;
         }
         Log.d(TAG, "Running dexopt on: " + path
-                + " pkg=" + info.packageName + " isa=" + isas
+                + " pkg=" + info.packageName + " isa=" + dexUseInfo.getLoaderIsas()
                 + " dexoptFlags=" + printDexoptFlags(dexoptFlags)
                 + " target-filter=" + compilerFilter);
 
+        String classLoaderContext;
+        if (dexUseInfo.isUnknownClassLoaderContext() ||
+                dexUseInfo.isUnsupportedClassLoaderContext() ||
+                dexUseInfo.isVariableClassLoaderContext()) {
+            // If we have an unknown (not yet set), unsupported (custom class loaders), or a
+            // variable class loader chain, compile without a context and mark the oat file with
+            // SKIP_SHARED_LIBRARY_CHECK. Note that his might lead to a incorrect compilation.
+            // TODO(calin): We should just extract in this case.
+            classLoaderContext = SKIP_SHARED_LIBRARY_CHECK;
+        } else {
+            classLoaderContext = dexUseInfo.getClassLoaderContext();
+        }
         try {
-            for (String isa : isas) {
+            for (String isa : dexUseInfo.getLoaderIsas()) {
                 // Reuse the same dexopt path as for the primary apks. We don't need all the
                 // arguments as some (dexopNeeded and oatDir) will be computed by installd because
                 // system server cannot read untrusted app content.
                 // TODO(calin): maybe add a separate call.
                 mInstaller.dexopt(path, info.uid, info.packageName, isa, /*dexoptNeeded*/ 0,
                         /*oatDir*/ null, dexoptFlags,
-                        compilerFilter, info.volumeUuid, SKIP_SHARED_LIBRARY_CHECK, info.seInfoUser,
-                        downgrade);
+                        compilerFilter, info.volumeUuid, classLoaderContext, info.seInfoUser,
+                        options.isDowngrade());
             }
 
             return DEX_OPT_PERFORMED;
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 98b6949..1068b4a 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -9434,7 +9434,8 @@
     }
 
     @Override
-    public void notifyDexLoad(String loadingPackageName, List<String> dexPaths, String loaderIsa) {
+    public void notifyDexLoad(String loadingPackageName, List<String> classLoaderNames,
+            List<String> classPaths, String loaderIsa) {
         int userId = UserHandle.getCallingUserId();
         ApplicationInfo ai = getApplicationInfo(loadingPackageName, /*flags*/ 0, userId);
         if (ai == null) {
@@ -9442,7 +9443,7 @@
                 + loadingPackageName + ", user=" + userId);
             return;
         }
-        mDexManager.notifyDexLoad(ai, dexPaths, loaderIsa, userId);
+        mDexManager.notifyDexLoad(ai, classLoaderNames, classPaths, loaderIsa, userId);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java
index 3d2d483..79e02b5 100644
--- a/services/core/java/com/android/server/pm/dex/DexManager.java
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -97,29 +97,55 @@
      * return as fast as possible.
      *
      * @param loadingAppInfo the package performing the load
-     * @param dexPaths the list of dex files being loaded
+     * @param classLoadersNames the names of the class loaders present in the loading chain. The
+     *    list encodes the class loader chain in the natural order. The first class loader has
+     *    the second one as its parent and so on. The dex files present in the class path of the
+     *    first class loader will be recorded in the usage file.
+     * @param classPaths the class paths corresponding to the class loaders names from
+     *     {@param classLoadersNames}. The the first element corresponds to the first class loader
+     *     and so on. A classpath is represented as a list of dex files separated by
+     *     {@code File.pathSeparator}.
+     *     The dex files found in the first class path will be recorded in the usage file.
      * @param loaderIsa the ISA of the app loading the dex files
      * @param loaderUserId the user id which runs the code loading the dex files
      */
-    public void notifyDexLoad(ApplicationInfo loadingAppInfo, List<String> dexPaths,
-            String loaderIsa, int loaderUserId) {
+    public void notifyDexLoad(ApplicationInfo loadingAppInfo, List<String> classLoadersNames,
+            List<String> classPaths, String loaderIsa, int loaderUserId) {
         try {
-            notifyDexLoadInternal(loadingAppInfo, dexPaths, loaderIsa, loaderUserId);
+            notifyDexLoadInternal(loadingAppInfo, classLoadersNames, classPaths, loaderIsa,
+                    loaderUserId);
         } catch (Exception e) {
             Slog.w(TAG, "Exception while notifying dex load for package " +
                     loadingAppInfo.packageName, e);
         }
     }
 
-    private void notifyDexLoadInternal(ApplicationInfo loadingAppInfo, List<String> dexPaths,
-            String loaderIsa, int loaderUserId) {
+    private void notifyDexLoadInternal(ApplicationInfo loadingAppInfo,
+            List<String> classLoaderNames, List<String> classPaths, String loaderIsa,
+            int loaderUserId) {
+        if (classLoaderNames.size() != classPaths.size()) {
+            Slog.wtf(TAG, "Bad call to noitfyDexLoad: args have different size");
+            return;
+        }
+        if (classLoaderNames.isEmpty()) {
+            Slog.wtf(TAG, "Bad call to notifyDexLoad: class loaders list is empty");
+            return;
+        }
         if (!PackageManagerServiceUtils.checkISA(loaderIsa)) {
-            Slog.w(TAG, "Loading dex files " + dexPaths + " in unsupported ISA: " +
+            Slog.w(TAG, "Loading dex files " + classPaths + " in unsupported ISA: " +
                     loaderIsa + "?");
             return;
         }
 
-        for (String dexPath : dexPaths) {
+        // The classpath is represented as a list of dex files separated by File.pathSeparator.
+        String[] dexPathsToRegister = classPaths.get(0).split(File.pathSeparator);
+
+        // Encode the class loader contexts for the dexPathsToRegister.
+        String[] classLoaderContexts = DexoptUtils.processContextForDexLoad(
+                classLoaderNames, classPaths);
+
+        int dexPathIndex = 0;
+        for (String dexPath : dexPathsToRegister) {
             // Find the owning package name.
             DexSearchResult searchResult = getDexPackage(loadingAppInfo, dexPath, loaderUserId);
 
@@ -147,24 +173,25 @@
                 // Record dex file usage. If the current usage is a new pattern (e.g. new secondary,
                 // or UsedBytOtherApps), record will return true and we trigger an async write
                 // to disk to make sure we don't loose the data in case of a reboot.
+
+                // A null classLoaderContexts means that there are unsupported class loaders in the
+                // chain.
+                String classLoaderContext = classLoaderContexts == null
+                        ? PackageDexUsage.UNSUPPORTED_CLASS_LOADER_CONTEXT
+                        : classLoaderContexts[dexPathIndex];
                 if (mPackageDexUsage.record(searchResult.mOwningPackageName,
                         dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
-                        loadingAppInfo.packageName)) {
+                        loadingAppInfo.packageName, classLoaderContext)) {
                     mPackageDexUsage.maybeWriteAsync();
                 }
             } else {
-                // This can happen in a few situations:
-                // - bogus dex loads
-                // - recent installs/uninstalls that we didn't detect.
-                // - new installed splits
                 // If we can't find the owner of the dex we simply do not track it. The impact is
                 // that the dex file will not be considered for offline optimizations.
-                // TODO(calin): add hooks for move/uninstall notifications to
-                // capture package moves or obsolete packages.
                 if (DEBUG) {
                     Slog.i(TAG, "Could not find owning package for dex file: " + dexPath);
                 }
             }
+            dexPathIndex++;
         }
     }
 
@@ -328,10 +355,8 @@
         for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) {
             String dexPath = entry.getKey();
             DexUseInfo dexUseInfo = entry.getValue();
-            if (options.isDexoptOnlySharedDex() && !dexUseInfo.isUsedByOtherApps()) {
-                continue;
-            }
-            PackageInfo pkg = null;
+
+            PackageInfo pkg;
             try {
                 pkg = mPackageManager.getPackageInfo(packageName, /*flags*/0,
                     dexUseInfo.getOwnerUserId());
@@ -350,8 +375,7 @@
             }
 
             int result = pdo.dexOptSecondaryDexPath(pkg.applicationInfo, dexPath,
-                    dexUseInfo.getLoaderIsas(), options.getCompilerFilter(),
-                    dexUseInfo.isUsedByOtherApps(), options.isDowngrade());
+                    dexUseInfo, options);
             success = success && (result != PackageDexOptimizer.DEX_OPT_FAILED);
         }
         return success;
@@ -434,6 +458,8 @@
         }
     }
 
+    // TODO(calin): questionable API in the presence of class loaders context. Needs amends as the
+    // compilation happening here will use a pessimistic context.
     public RegisterDexModuleResult registerDexModule(ApplicationInfo info, String dexPath,
             boolean isUsedByOtherApps, int userId) {
         // Find the owning package record.
@@ -452,12 +478,11 @@
 
         // We found the package. Now record the usage for all declared ISAs.
         boolean update = false;
-        Set<String> isas = new HashSet<>();
         for (String isa : getAppDexInstructionSets(info)) {
-            isas.add(isa);
             boolean newUpdate = mPackageDexUsage.record(searchResult.mOwningPackageName,
                     dexPath, userId, isa, isUsedByOtherApps, /*primaryOrSplit*/ false,
-                    searchResult.mOwningPackageName);
+                    searchResult.mOwningPackageName,
+                    PackageDexUsage.UNKNOWN_CLASS_LOADER_CONTEXT);
             update |= newUpdate;
         }
         if (update) {
@@ -467,8 +492,13 @@
         // Try to optimize the package according to the install reason.
         String compilerFilter = PackageManagerServiceCompilerMapping.getCompilerFilterForReason(
                 PackageManagerService.REASON_INSTALL);
-        int result = mPackageDexOptimizer.dexOptSecondaryDexPath(info, dexPath, isas,
-                compilerFilter, isUsedByOtherApps, /* downgrade */ false);
+        DexUseInfo dexUseInfo = mPackageDexUsage.getPackageUseInfo(searchResult.mOwningPackageName)
+                .getDexUseInfoMap().get(dexPath);
+
+        DexoptOptions options = new DexoptOptions(info.packageName, compilerFilter, /*flags*/0);
+
+        int result = mPackageDexOptimizer.dexOptSecondaryDexPath(info, dexPath, dexUseInfo,
+                options);
 
         // If we fail to optimize the package log an error but don't propagate the error
         // back to the app. The app cannot do much about it and the background job
diff --git a/services/core/java/com/android/server/pm/dex/DexoptUtils.java b/services/core/java/com/android/server/pm/dex/DexoptUtils.java
index 18e91df..0196212 100644
--- a/services/core/java/com/android/server/pm/dex/DexoptUtils.java
+++ b/services/core/java/com/android/server/pm/dex/DexoptUtils.java
@@ -23,6 +23,8 @@
 import com.android.internal.os.ClassLoaderFactory;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 public final class DexoptUtils {
@@ -229,7 +231,76 @@
      * dependencies {@see encodeClassLoader} separated by ';'.
      */
     private static String encodeClassLoaderChain(String cl1, String cl2) {
-        return cl1.isEmpty() ? cl2 : (cl1 + ";" + cl2);
+        if (cl1.isEmpty()) return cl2;
+        if (cl2.isEmpty()) return cl1;
+        return cl1 + ";" + cl2;
+    }
+
+    /**
+     * Compute the class loader context for the dex files present in the classpath of the first
+     * class loader from the given list (referred in the code as the {@code loadingClassLoader}).
+     * Each dex files gets its own class loader context in the returned array.
+     *
+     * Example:
+     *    If classLoadersNames = {"dalvik.system.DelegateLastClassLoader",
+     *    "dalvik.system.PathClassLoader"} and classPaths = {"foo.dex:bar.dex", "other.dex"}
+     *    The output will be
+     *    {"DLC[];PCL[other.dex]", "DLC[foo.dex];PCL[other.dex]"}
+     *    with "DLC[];PCL[other.dex]" being the context for "foo.dex"
+     *    and "DLC[foo.dex];PCL[other.dex]" the context for "bar.dex".
+     *
+     * If any of the class loaders names is unsupported the method will return null.
+     *
+     * The argument lists must be non empty and of the same size.
+     *
+     * @param classLoadersNames the names of the class loaders present in the loading chain. The
+     *    list encodes the class loader chain in the natural order. The first class loader has
+     *    the second one as its parent and so on.
+     * @param classPaths the class paths for the elements of {@param classLoadersNames}. The
+     *     the first element corresponds to the first class loader and so on. A classpath is
+     *     represented as a list of dex files separated by {@code File.pathSeparator}.
+     *     The return context will be for the dex files found in the first class path.
+     */
+    /*package*/ static String[] processContextForDexLoad(List<String> classLoadersNames,
+            List<String> classPaths) {
+        if (classLoadersNames.size() != classPaths.size()) {
+            throw new IllegalArgumentException(
+                    "The size of the class loader names and the dex paths do not match.");
+        }
+        if (classLoadersNames.isEmpty()) {
+            throw new IllegalArgumentException("Empty classLoadersNames");
+        }
+
+        // Compute the context for the parent class loaders.
+        String parentContext = "";
+        // We know that these lists are actually ArrayLists so getting the elements by index
+        // is fine (they come over binder). Even if something changes we expect the sizes to be
+        // very small and it shouldn't matter much.
+        for (int i = 1; i < classLoadersNames.size(); i++) {
+            if (!ClassLoaderFactory.isValidClassLoaderName(classLoadersNames.get(i))) {
+                return null;
+            }
+            String classpath = encodeClasspath(classPaths.get(i).split(File.pathSeparator));
+            parentContext = encodeClassLoaderChain(parentContext,
+                    encodeClassLoader(classpath, classLoadersNames.get(i)));
+        }
+
+        // Now compute the class loader context for each dex file from the first classpath.
+        String loadingClassLoader = classLoadersNames.get(0);
+        if (!ClassLoaderFactory.isValidClassLoaderName(loadingClassLoader)) {
+            return null;
+        }
+        String[] loadedDexPaths = classPaths.get(0).split(File.pathSeparator);
+        String[] loadedDexPathsContext = new String[loadedDexPaths.length];
+        String currentLoadedDexPathClasspath = "";
+        for (int i = 0; i < loadedDexPaths.length; i++) {
+            String dexPath = loadedDexPaths[i];
+            String currentContext = encodeClassLoader(
+                    currentLoadedDexPathClasspath, loadingClassLoader);
+            loadedDexPathsContext[i] = encodeClassLoaderChain(currentContext, parentContext);
+            currentLoadedDexPathClasspath = encodeClasspath(currentLoadedDexPathClasspath, dexPath);
+        }
+        return loadedDexPathsContext;
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/dex/PackageDexUsage.java b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
index f7dd174..8819aa6 100644
--- a/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
+++ b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
@@ -26,7 +26,6 @@
 import com.android.server.pm.PackageManagerServiceUtils;
 
 import java.io.BufferedReader;
-import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.InputStreamReader;
@@ -36,7 +35,6 @@
 import java.io.StringWriter;
 import java.io.Writer;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.HashMap;
@@ -55,10 +53,12 @@
 public class PackageDexUsage extends AbstractStatsBase<Void> {
     private final static String TAG = "PackageDexUsage";
 
-    // The last version update: add the list of packages that load the dex files.
-    private final static int PACKAGE_DEX_USAGE_VERSION = 2;
-    // We support VERSION 1 to ensure that the usage list remains valid cross OTAs.
+    // The last version update: add class loader contexts for secondary dex files.
+    private final static int PACKAGE_DEX_USAGE_VERSION = 3;
+    // We support previous version to ensure that the usage list remains valid cross OTAs.
     private final static int PACKAGE_DEX_USAGE_SUPPORTED_VERSION_1 = 1;
+    // Version 2 added the list of packages that load the dex files.
+    private final static int PACKAGE_DEX_USAGE_SUPPORTED_VERSION_2 = 2;
 
     private final static String PACKAGE_DEX_USAGE_VERSION_HEADER =
             "PACKAGE_MANAGER__PACKAGE_DEX_USAGE__";
@@ -66,6 +66,21 @@
     private final static String SPLIT_CHAR = ",";
     private final static String DEX_LINE_CHAR = "#";
     private final static String LOADING_PACKAGE_CHAR = "@";
+
+    // One of the things we record about dex files is the class loader context that was used to
+    // load them. That should be stable but if it changes we don't keep track of variable contexts.
+    // Instead we put a special marker in the dex usage file in order to recognize the case and
+    // skip optimizations on that dex files.
+    /*package*/ static final String VARIABLE_CLASS_LOADER_CONTEXT =
+            "=VariableClassLoaderContext=";
+    // The marker used for unsupported class loader contexts.
+    /*package*/ static final String UNSUPPORTED_CLASS_LOADER_CONTEXT =
+            "=UnsupportedClassLoaderContext=";
+    // The markers used for unknown class loader contexts. This can happen if the dex file was
+    // recorded in a previous version and we didn't have a chance to update its usage.
+    /*package*/ static final String UNKNOWN_CLASS_LOADER_CONTEXT =
+            "=UnknownClassLoaderContext=";
+
     // Map which structures the information we have on a package.
     // Maps package name to package data (which stores info about UsedByOtherApps and
     // secondary dex files.).
@@ -98,10 +113,14 @@
      */
     public boolean record(String owningPackageName, String dexPath, int ownerUserId,
             String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit,
-            String loadingPackageName) {
+            String loadingPackageName, String classLoaderContext) {
         if (!PackageManagerServiceUtils.checkISA(loaderIsa)) {
             throw new IllegalArgumentException("loaderIsa " + loaderIsa + " is unsupported");
         }
+        if (classLoaderContext == null) {
+            throw new IllegalArgumentException("Null classLoaderContext");
+        }
+
         synchronized (mPackageUseInfoMap) {
             PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(owningPackageName);
             if (packageUseInfo == null) {
@@ -117,7 +136,8 @@
                 } else {
                     // For secondary dex files record the loaderISA and the owner. We'll need
                     // to know under which user to compile and for what ISA.
-                    DexUseInfo newData = new DexUseInfo(isUsedByOtherApps, ownerUserId, loaderIsa);
+                    DexUseInfo newData = new DexUseInfo(isUsedByOtherApps, ownerUserId,
+                            classLoaderContext, loaderIsa);
                     packageUseInfo.mDexUseInfoMap.put(dexPath, newData);
                     maybeAddLoadingPackage(owningPackageName, loadingPackageName,
                             newData.mLoadingPackages);
@@ -134,7 +154,7 @@
                     return packageUseInfo.merge(isUsedByOtherApps) || updateLoadingPackages;
                 } else {
                     DexUseInfo newData = new DexUseInfo(
-                            isUsedByOtherApps, ownerUserId, loaderIsa);
+                            isUsedByOtherApps, ownerUserId, classLoaderContext, loaderIsa);
                     boolean updateLoadingPackages = maybeAddLoadingPackage(owningPackageName,
                             loadingPackageName, newData.mLoadingPackages);
 
@@ -249,8 +269,9 @@
                 String dexPath = dEntry.getKey();
                 DexUseInfo dexUseInfo = dEntry.getValue();
                 fpw.println(DEX_LINE_CHAR + dexPath);
-                    fpw.println(LOADING_PACKAGE_CHAR +
-                            String.join(SPLIT_CHAR, dexUseInfo.mLoadingPackages));
+                fpw.println(LOADING_PACKAGE_CHAR +
+                        String.join(SPLIT_CHAR, dexUseInfo.mLoadingPackages));
+                fpw.println(dexUseInfo.getClassLoaderContext());
 
                 fpw.print(String.join(SPLIT_CHAR, Integer.toString(dexUseInfo.mOwnerUserId),
                         writeBoolean(dexUseInfo.mIsUsedByOtherApps)));
@@ -310,8 +331,10 @@
         while ((s = in.readLine()) != null) {
             if (s.startsWith(DEX_LINE_CHAR)) {
                 // This is the start of the the dex lines.
-                // We expect two lines for each dex entry:
+                // We expect 4 lines for each dex entry:
                 // #dexPaths
+                // @loading_package_1,loading_package_2,...
+                // class_loader_context
                 // onwerUserId,isUsedByOtherApps,isa1,isa2
                 if (currentPackage == null) {
                     throw new IllegalStateException(
@@ -323,11 +346,13 @@
 
                 // In version 2 the second line contains the list of packages that loaded the file.
                 List<String> loadingPackages = maybeReadLoadingPackages(in, version);
+                // In version 3 the third line contains the class loader context.
+                String classLoaderContext = maybeReadClassLoaderContext(in, version);
 
                 // Next line is the dex data.
                 s = in.readLine();
                 if (s == null) {
-                    throw new IllegalStateException("Could not find dexUseInfo for line: " + s);
+                    throw new IllegalStateException("Could not find dexUseInfo line");
                 }
 
                 // We expect at least 3 elements (isUsedByOtherApps, userId, isa).
@@ -337,9 +362,9 @@
                 }
                 int ownerUserId = Integer.parseInt(elems[0]);
                 boolean isUsedByOtherApps = readBoolean(elems[1]);
-                DexUseInfo dexUseInfo = new DexUseInfo(isUsedByOtherApps, ownerUserId);
+                DexUseInfo dexUseInfo = new DexUseInfo(isUsedByOtherApps, ownerUserId,
+                        classLoaderContext, /*isa*/ null);
                 dexUseInfo.mLoadingPackages.addAll(loadingPackages);
-
                 for (int i = 2; i < elems.length; i++) {
                     String isa = elems[i];
                     if (supportedIsas.contains(isa)) {
@@ -379,12 +404,30 @@
     }
 
     /**
-     * Reads the list of loading packages from the buffer {@parm in} if
+     * Reads the class loader context encoding from the buffer {@code in} if
      * {@code version} is at least {PACKAGE_DEX_USAGE_VERSION}.
      */
+    private String maybeReadClassLoaderContext(BufferedReader in, int version) throws IOException {
+        String context = null;
+        if (version == PACKAGE_DEX_USAGE_VERSION) {
+            context = in.readLine();
+            if (context == null) {
+                throw new IllegalStateException("Could not find the classLoaderContext line.");
+            }
+        }
+        // The context might be empty if we didn't have the chance to update it after a version
+        // upgrade. In this case return the special marker so that we recognize this is an unknown
+        // context.
+        return context == null ? UNKNOWN_CLASS_LOADER_CONTEXT : context;
+    }
+
+    /**
+     * Reads the list of loading packages from the buffer {@code in} if
+     * {@code version} is at least {PACKAGE_DEX_USAGE_SUPPORTED_VERSION_2}.
+     */
     private List<String> maybeReadLoadingPackages(BufferedReader in, int version)
             throws IOException {
-        if (version == PACKAGE_DEX_USAGE_VERSION) {
+        if (version >= PACKAGE_DEX_USAGE_SUPPORTED_VERSION_2) {
             String line = in.readLine();
             if (line == null) {
                 throw new IllegalStateException("Could not find the loadingPackages line.");
@@ -660,17 +703,20 @@
     public static class DexUseInfo {
         private boolean mIsUsedByOtherApps;
         private final int mOwnerUserId;
+        // The class loader context for the dex file. This encodes the class loader chain
+        // (class loader type + class path) in a format compatible to dex2oat.
+        // See {@code DexoptUtils.processContextForDexLoad}.
+        private String mClassLoaderContext;
+        // The instructions sets of the applications loading the dex file.
         private final Set<String> mLoaderIsas;
         // Packages who load this dex file.
         private final Set<String> mLoadingPackages;
 
-        public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId) {
-            this(isUsedByOtherApps, ownerUserId, null);
-        }
-
-        public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId, String loaderIsa) {
+        public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId, String classLoaderContext,
+                String loaderIsa) {
             mIsUsedByOtherApps = isUsedByOtherApps;
             mOwnerUserId = ownerUserId;
+            mClassLoaderContext = classLoaderContext;
             mLoaderIsas = new HashSet<>();
             if (loaderIsa != null) {
                 mLoaderIsas.add(loaderIsa);
@@ -682,6 +728,7 @@
         public DexUseInfo(DexUseInfo other) {
             mIsUsedByOtherApps = other.mIsUsedByOtherApps;
             mOwnerUserId = other.mOwnerUserId;
+            mClassLoaderContext = other.mClassLoaderContext;
             mLoaderIsas = new HashSet<>(other.mLoaderIsas);
             mLoadingPackages = new HashSet<>(other.mLoadingPackages);
         }
@@ -691,8 +738,24 @@
             mIsUsedByOtherApps = mIsUsedByOtherApps || dexUseInfo.mIsUsedByOtherApps;
             boolean updateIsas = mLoaderIsas.addAll(dexUseInfo.mLoaderIsas);
             boolean updateLoadingPackages = mLoadingPackages.addAll(dexUseInfo.mLoadingPackages);
-            return updateIsas || (oldIsUsedByOtherApps != mIsUsedByOtherApps) ||
-                    updateLoadingPackages;
+
+            String oldClassLoaderContext = mClassLoaderContext;
+            if (UNKNOWN_CLASS_LOADER_CONTEXT.equals(mClassLoaderContext)) {
+                // Can happen if we read a previous version.
+                mClassLoaderContext = dexUseInfo.mClassLoaderContext;
+            } else if (UNSUPPORTED_CLASS_LOADER_CONTEXT.equals(dexUseInfo.mClassLoaderContext)) {
+                // We detected an unsupported context.
+                mClassLoaderContext = UNSUPPORTED_CLASS_LOADER_CONTEXT;
+            } else if (!UNSUPPORTED_CLASS_LOADER_CONTEXT.equals(mClassLoaderContext) &&
+                    !Objects.equal(mClassLoaderContext, dexUseInfo.mClassLoaderContext)) {
+                // We detected a context change.
+                mClassLoaderContext = VARIABLE_CLASS_LOADER_CONTEXT;
+            }
+
+            return updateIsas ||
+                    (oldIsUsedByOtherApps != mIsUsedByOtherApps) ||
+                    updateLoadingPackages
+                    || !Objects.equal(oldClassLoaderContext, mClassLoaderContext);
         }
 
         public boolean isUsedByOtherApps() {
@@ -710,5 +773,21 @@
         public Set<String> getLoadingPackages() {
             return mLoadingPackages;
         }
+
+        public String getClassLoaderContext() { return mClassLoaderContext; }
+
+        public boolean isUnsupportedClassLoaderContext() {
+            return UNSUPPORTED_CLASS_LOADER_CONTEXT.equals(mClassLoaderContext);
+        }
+
+        public boolean isUnknownClassLoaderContext() {
+            // The class loader context may be unknown if we loaded the data from a previous version
+            // which didn't save the context.
+            return UNKNOWN_CLASS_LOADER_CONTEXT.equals(mClassLoaderContext);
+        }
+
+        public boolean isVariableClassLoaderContext() {
+            return VARIABLE_CLASS_LOADER_CONTEXT.equals(mClassLoaderContext);
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
index afc0f67..e2dfb29 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
@@ -23,10 +23,14 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.PathClassLoader;
 import dalvik.system.VMRuntime;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -48,6 +52,10 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class DexManagerTests {
+    private static final String PATH_CLASS_LOADER_NAME = PathClassLoader.class.getName();
+    private static final String DELEGATE_LAST_CLASS_LOADER_NAME =
+            DelegateLastClassLoader.class.getName();
+
     private DexManager mDexManager;
 
     private TestData mFooUser0;
@@ -56,6 +64,9 @@
     private TestData mInvalidIsa;
     private TestData mDoesNotExist;
 
+    private TestData mBarUser0UnsupportedClassLoader;
+    private TestData mBarUser0DelegateLastClassLoader;
+
     private int mUser0;
     private int mUser1;
 
@@ -68,12 +79,17 @@
         String foo = "foo";
         String bar = "bar";
 
-        mFooUser0 = new TestData(foo, isa, mUser0);
-        mBarUser0 = new TestData(bar, isa, mUser0);
-        mBarUser1 = new TestData(bar, isa, mUser1);
+        mFooUser0 = new TestData(foo, isa, mUser0, PATH_CLASS_LOADER_NAME);
+        mBarUser0 = new TestData(bar, isa, mUser0, PATH_CLASS_LOADER_NAME);
+        mBarUser1 = new TestData(bar, isa, mUser1, PATH_CLASS_LOADER_NAME);
         mInvalidIsa = new TestData("INVALID", "INVALID_ISA", mUser0);
         mDoesNotExist = new TestData("DOES.NOT.EXIST", isa, mUser1);
 
+        mBarUser0UnsupportedClassLoader = new TestData(bar, isa, mUser0,
+                "unsupported.class_loader");
+        mBarUser0DelegateLastClassLoader = new TestData(bar, isa, mUser0,
+                DELEGATE_LAST_CLASS_LOADER_NAME);
+
         mDexManager = new DexManager(null, null, null, null);
 
         // Foo and Bar are available to user0.
@@ -373,8 +389,82 @@
         assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
     }
 
+    @Test
+    public void testNotifyUnsupportedClassLoader() {
+        List<String> secondaries = mBarUser0UnsupportedClassLoader.getSecondaryDexPaths();
+        notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0);
+
+        PackageUseInfo pui = getPackageUseInfo(mBarUser0UnsupportedClassLoader);
+        assertNotNull(pui);
+        assertFalse(pui.isUsedByOtherApps());
+        assertEquals(secondaries.size(), pui.getDexUseInfoMap().size());
+        // We expect that all the contexts are unsupported.
+        String[] expectedContexts =
+                Collections.nCopies(secondaries.size(),
+                        PackageDexUsage.UNSUPPORTED_CLASS_LOADER_CONTEXT).toArray(new String[0]);
+        assertSecondaryUse(mBarUser0UnsupportedClassLoader, pui, secondaries,
+                /*isUsedByOtherApps*/false, mUser0, expectedContexts);
+    }
+
+    @Test
+    public void testNotifyVariableClassLoader() {
+        // Record bar secondaries with the default PathClassLoader.
+        List<String> secondaries = mBarUser0.getSecondaryDexPaths();
+
+        notifyDexLoad(mBarUser0, secondaries, mUser0);
+        PackageUseInfo pui = getPackageUseInfo(mBarUser0);
+        assertNotNull(pui);
+        assertFalse(pui.isUsedByOtherApps());
+        assertEquals(secondaries.size(), pui.getDexUseInfoMap().size());
+        assertSecondaryUse(mFooUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0);
+
+        // Record bar secondaries again with a different class loader. This will change the context.
+        notifyDexLoad(mBarUser0DelegateLastClassLoader, secondaries, mUser0);
+
+        pui = getPackageUseInfo(mBarUser0);
+        assertNotNull(pui);
+        assertFalse(pui.isUsedByOtherApps());
+        assertEquals(secondaries.size(), pui.getDexUseInfoMap().size());
+        // We expect that all the contexts to be changed to variable now.
+        String[] expectedContexts =
+                Collections.nCopies(secondaries.size(),
+                        PackageDexUsage.VARIABLE_CLASS_LOADER_CONTEXT).toArray(new String[0]);
+        assertSecondaryUse(mFooUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0,
+                expectedContexts);
+    }
+
+    @Test
+    public void testNotifyUnsupportedClassLoaderDoesNotChange() {
+        List<String> secondaries = mBarUser0UnsupportedClassLoader.getSecondaryDexPaths();
+        notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0);
+
+        PackageUseInfo pui = getPackageUseInfo(mBarUser0UnsupportedClassLoader);
+        assertNotNull(pui);
+        assertFalse(pui.isUsedByOtherApps());
+        assertEquals(secondaries.size(), pui.getDexUseInfoMap().size());
+        // We expect that all the contexts are unsupported.
+        String[] expectedContexts =
+                Collections.nCopies(secondaries.size(),
+                        PackageDexUsage.UNSUPPORTED_CLASS_LOADER_CONTEXT).toArray(new String[0]);
+        assertSecondaryUse(mBarUser0UnsupportedClassLoader, pui, secondaries,
+                /*isUsedByOtherApps*/false, mUser0, expectedContexts);
+
+        // Record bar secondaries again with a different class loader. This will change the context.
+        // However, because the context was already marked as unsupported we should not chage it.
+        notifyDexLoad(mBarUser0DelegateLastClassLoader, secondaries, mUser0);
+        pui = getPackageUseInfo(mBarUser0UnsupportedClassLoader);
+        assertSecondaryUse(mBarUser0UnsupportedClassLoader, pui, secondaries,
+                /*isUsedByOtherApps*/false, mUser0, expectedContexts);
+
+    }
+
+
     private void assertSecondaryUse(TestData testData, PackageUseInfo pui,
-            List<String> secondaries, boolean isUsedByOtherApps, int ownerUserId) {
+            List<String> secondaries, boolean isUsedByOtherApps, int ownerUserId,
+            String[] expectedContexts) {
+        assertNotNull(expectedContexts);
+        assertEquals(expectedContexts.length, secondaries.size());
+        int index = 0;
         for (String dex : secondaries) {
             DexUseInfo dui = pui.getDexUseInfoMap().get(dex);
             assertNotNull(dui);
@@ -382,11 +472,29 @@
             assertEquals(ownerUserId, dui.getOwnerUserId());
             assertEquals(1, dui.getLoaderIsas().size());
             assertTrue(dui.getLoaderIsas().contains(testData.mLoaderIsa));
+            assertEquals(expectedContexts[index++], dui.getClassLoaderContext());
         }
     }
+    private void assertSecondaryUse(TestData testData, PackageUseInfo pui,
+            List<String> secondaries, boolean isUsedByOtherApps, int ownerUserId) {
+        String[] expectedContexts = DexoptUtils.processContextForDexLoad(
+                Arrays.asList(testData.mClassLoader),
+                Arrays.asList(String.join(File.pathSeparator, secondaries)));
+        assertSecondaryUse(testData, pui, secondaries, isUsedByOtherApps, ownerUserId,
+                expectedContexts);
+    }
 
     private void notifyDexLoad(TestData testData, List<String> dexPaths, int loaderUserId) {
-        mDexManager.notifyDexLoad(testData.mPackageInfo.applicationInfo, dexPaths,
+        // By default, assume a single class loader in the chain.
+        // This makes writing tests much easier.
+        List<String> classLoaders = Arrays.asList(testData.mClassLoader);
+        List<String> classPaths = Arrays.asList(String.join(File.pathSeparator, dexPaths));
+        notifyDexLoad(testData, classLoaders, classPaths, loaderUserId);
+    }
+
+    private void notifyDexLoad(TestData testData, List<String> classLoader, List<String> classPaths,
+            int loaderUserId) {
+        mDexManager.notifyDexLoad(testData.mPackageInfo.applicationInfo, classLoader, classPaths,
                 testData.mLoaderIsa, loaderUserId);
     }
 
@@ -416,10 +524,16 @@
     private static class TestData {
         private final PackageInfo mPackageInfo;
         private final String mLoaderIsa;
+        private final String mClassLoader;
 
-        private TestData(String  packageName, String loaderIsa, int userId) {
+        private TestData(String packageName, String loaderIsa, int userId, String classLoader) {
             mPackageInfo = getMockPackageInfo(packageName, userId);
             mLoaderIsa = loaderIsa;
+            mClassLoader = classLoader;
+        }
+
+        private TestData(String packageName, String loaderIsa, int userId) {
+            this(packageName, loaderIsa, userId, PATH_CLASS_LOADER_NAME);
         }
 
         private String getPackageName() {
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java
index 1b6aeb1..2c56a82 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java
@@ -17,6 +17,9 @@
 package com.android.server.pm.dex;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import android.content.pm.ApplicationInfo;
 import android.support.test.filters.SmallTest;
@@ -30,6 +33,11 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class DexoptUtilsTest {
@@ -212,4 +220,68 @@
         assertEquals(1, contexts.length);
         assertEquals("DLC[]", contexts[0]);
     }
+
+    @Test
+    public void testProcessContextForDexLoad() {
+        List<String> classLoaders = Arrays.asList(
+                DELEGATE_LAST_CLASS_LOADER_NAME,
+                PATH_CLASS_LOADER_NAME,
+                PATH_CLASS_LOADER_NAME);
+        List<String> classPaths = Arrays.asList(
+                String.join(File.pathSeparator, "foo.dex", "bar.dex"),
+                String.join(File.pathSeparator, "parent1.dex"),
+                String.join(File.pathSeparator, "parent2.dex", "parent3.dex"));
+        String[] context = DexoptUtils.processContextForDexLoad(classLoaders, classPaths);
+        assertNotNull(context);
+        assertEquals(2, context.length);
+        assertEquals("DLC[];PCL[parent1.dex];PCL[parent2.dex:parent3.dex]", context[0]);
+        assertEquals("DLC[foo.dex];PCL[parent1.dex];PCL[parent2.dex:parent3.dex]", context[1]);
+    }
+
+    @Test
+    public void testProcessContextForDexLoadSingleElement() {
+        List<String> classLoaders = Arrays.asList(PATH_CLASS_LOADER_NAME);
+        List<String> classPaths = Arrays.asList(
+                String.join(File.pathSeparator, "foo.dex", "bar.dex", "zoo.dex"));
+        String[] context = DexoptUtils.processContextForDexLoad(classLoaders, classPaths);
+        assertNotNull(context);
+        assertEquals(3, context.length);
+        assertEquals("PCL[]", context[0]);
+        assertEquals("PCL[foo.dex]", context[1]);
+        assertEquals("PCL[foo.dex:bar.dex]", context[2]);
+    }
+
+    @Test
+    public void testProcessContextForDexLoadUnsupported() {
+        List<String> classLoaders = Arrays.asList(
+                DELEGATE_LAST_CLASS_LOADER_NAME,
+                "unsupported.class.loader");
+        List<String> classPaths = Arrays.asList(
+                String.join(File.pathSeparator, "foo.dex", "bar.dex"),
+                String.join(File.pathSeparator, "parent1.dex"));
+        String[] context = DexoptUtils.processContextForDexLoad(classLoaders, classPaths);
+        assertNull(context);
+    }
+
+    @Test
+    public void testProcessContextForDexLoadIllegalCallEmptyList() {
+        boolean gotException = false;
+        try {
+            DexoptUtils.processContextForDexLoad(Collections.emptyList(), Collections.emptyList());
+        } catch (IllegalArgumentException ignore) {
+            gotException = true;
+        }
+        assertTrue(gotException);
+    }
+
+    @Test
+    public void testProcessContextForDexLoadIllegalCallDifferentSize() {
+        boolean gotException = false;
+        try {
+            DexoptUtils.processContextForDexLoad(Collections.emptyList(), Arrays.asList("a"));
+        } catch (IllegalArgumentException ignore) {
+            gotException = true;
+        }
+        assertTrue(gotException);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
index e1ef41e..3fc12b4 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
@@ -359,7 +359,7 @@
     public void testRecordDexFileUsersNotTheOwningPackage() {
         PackageDexUsage packageDexUsageRecordUsers = new PackageDexUsage();
         Set<String> users = new HashSet<>(Arrays.asList(
-                new String[] {mFooBaseUser0.mPackageName,}));
+                new String[] {mFooBaseUser0.mPackageName}));
         Set<String> usersExtra = new HashSet<>(Arrays.asList(
                 new String[] {"another.package.2", "another.package.3"}));
 
@@ -375,6 +375,89 @@
                 mFooSecondary1User0);
     }
 
+    @Test
+    public void testRecordClassLoaderContextVariableContext() {
+        // Record a secondary dex file.
+        assertTrue(record(mFooSecondary1User0));
+        // Now update its context.
+        TestData fooSecondary1User0NewContext = mFooSecondary1User0.updateClassLoaderContext(
+                "PCL[new_context.dex]");
+        assertTrue(record(fooSecondary1User0NewContext));
+
+        // Not check that the context was switch to variable.
+        TestData expectedContext = mFooSecondary1User0.updateClassLoaderContext(
+                PackageDexUsage.VARIABLE_CLASS_LOADER_CONTEXT);
+
+        assertPackageDexUsage(null, expectedContext);
+        writeAndReadBack();
+        assertPackageDexUsage(null, expectedContext);
+    }
+
+    @Test
+    public void testRecordClassLoaderContextUnsupportedContext() {
+        // Record a secondary dex file.
+        assertTrue(record(mFooSecondary1User0));
+        // Now update its context.
+        TestData unsupportedContext = mFooSecondary1User0.updateClassLoaderContext(
+                PackageDexUsage.UNSUPPORTED_CLASS_LOADER_CONTEXT);
+        assertTrue(record(unsupportedContext));
+
+        assertPackageDexUsage(null, unsupportedContext);
+        writeAndReadBack();
+        assertPackageDexUsage(null, unsupportedContext);
+    }
+
+    @Test
+    public void testRecordClassLoaderContextTransitionFromUnknown() {
+        // Record a secondary dex file.
+        TestData unknownContext = mFooSecondary1User0.updateClassLoaderContext(
+                PackageDexUsage.UNKNOWN_CLASS_LOADER_CONTEXT);
+        assertTrue(record(unknownContext));
+
+        assertPackageDexUsage(null, unknownContext);
+        writeAndReadBack();
+        assertPackageDexUsage(null, unknownContext);
+
+        // Now update the secondary dex record with a class loader context. This simulates the
+        // version 2 to version 3 upgrade.
+
+        assertTrue(record(mFooSecondary1User0));
+
+        assertPackageDexUsage(null, mFooSecondary1User0);
+        writeAndReadBack();
+        assertPackageDexUsage(null, mFooSecondary1User0);
+    }
+
+
+    @Test
+    public void testDexUsageClassLoaderContext() {
+        final boolean isUsedByOtherApps = false;
+        final int userId = 0;
+        PackageDexUsage.DexUseInfo validContext = new DexUseInfo(isUsedByOtherApps, userId,
+                "valid_context", "arm");
+        assertFalse(validContext.isUnknownClassLoaderContext());
+        assertFalse(validContext.isUnsupportedClassLoaderContext());
+        assertFalse(validContext.isVariableClassLoaderContext());
+
+        PackageDexUsage.DexUseInfo unsupportedContext = new DexUseInfo(isUsedByOtherApps, userId,
+                PackageDexUsage.UNSUPPORTED_CLASS_LOADER_CONTEXT, "arm");
+        assertFalse(unsupportedContext.isUnknownClassLoaderContext());
+        assertTrue(unsupportedContext.isUnsupportedClassLoaderContext());
+        assertFalse(unsupportedContext.isVariableClassLoaderContext());
+
+        PackageDexUsage.DexUseInfo variableContext = new DexUseInfo(isUsedByOtherApps, userId,
+                PackageDexUsage.VARIABLE_CLASS_LOADER_CONTEXT, "arm");
+        assertFalse(variableContext.isUnknownClassLoaderContext());
+        assertFalse(variableContext.isUnsupportedClassLoaderContext());
+        assertTrue(variableContext.isVariableClassLoaderContext());
+
+        PackageDexUsage.DexUseInfo unknownContext = new DexUseInfo(isUsedByOtherApps, userId,
+                PackageDexUsage.UNKNOWN_CLASS_LOADER_CONTEXT, "arm");
+        assertTrue(unknownContext.isUnknownClassLoaderContext());
+        assertFalse(unknownContext.isUnsupportedClassLoaderContext());
+        assertFalse(unknownContext.isVariableClassLoaderContext());
+    }
+
     private void assertPackageDexUsage(TestData primary, TestData... secondaries) {
         assertPackageDexUsage(mPackageDexUsage, null, primary, secondaries);
     }
@@ -382,7 +465,7 @@
     private void assertPackageDexUsage(PackageDexUsage packageDexUsage, Set<String> users,
             TestData primary, TestData... secondaries) {
         String packageName = primary == null ? secondaries[0].mPackageName : primary.mPackageName;
-        boolean primaryUsedByOtherApps = primary == null ? false : primary.mUsedByOtherApps;
+        boolean primaryUsedByOtherApps = primary != null && primary.mUsedByOtherApps;
         PackageUseInfo pInfo = packageDexUsage.getPackageUseInfo(packageName);
 
         // Check package use info
@@ -406,13 +489,15 @@
             if (users != null) {
                  assertEquals(dInfo.getLoadingPackages(), users);
             }
+
+            assertEquals(testData.mClassLoaderContext, dInfo.getClassLoaderContext());
         }
     }
 
     private boolean record(TestData testData) {
         return mPackageDexUsage.record(testData.mPackageName, testData.mDexFile,
                testData.mOwnerUserId, testData.mLoaderIsa, testData.mUsedByOtherApps,
-               testData.mPrimaryOrSplit, testData.mUsedBy);
+               testData.mPrimaryOrSplit, testData.mUsedBy, testData.mClassLoaderContext);
     }
 
     private boolean record(PackageDexUsage packageDexUsage, TestData testData, Set<String> users) {
@@ -420,7 +505,7 @@
         for (String user : users) {
             result = result && packageDexUsage.record(testData.mPackageName, testData.mDexFile,
                     testData.mOwnerUserId, testData.mLoaderIsa, testData.mUsedByOtherApps,
-                    testData.mPrimaryOrSplit, user);
+                    testData.mPrimaryOrSplit, user, testData.mClassLoaderContext);
         }
         return result;
     }
@@ -451,9 +536,16 @@
         private final boolean mUsedByOtherApps;
         private final boolean mPrimaryOrSplit;
         private final String mUsedBy;
+        private final String mClassLoaderContext;
 
         private TestData(String packageName, String dexFile, int ownerUserId,
-                 String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit, String usedBy) {
+                String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit, String usedBy) {
+            this(packageName, dexFile, ownerUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
+                    usedBy, "DefaultClassLoaderContextFor_" + dexFile);
+        }
+        private TestData(String packageName, String dexFile, int ownerUserId,
+                String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit, String usedBy,
+                String classLoaderContext) {
             mPackageName = packageName;
             mDexFile = dexFile;
             mOwnerUserId = ownerUserId;
@@ -461,7 +553,12 @@
             mUsedByOtherApps = isUsedByOtherApps;
             mPrimaryOrSplit = primaryOrSplit;
             mUsedBy = usedBy;
+            mClassLoaderContext = classLoaderContext;
         }
 
+        private TestData updateClassLoaderContext(String newContext) {
+            return new TestData(mPackageName, mDexFile, mOwnerUserId, mLoaderIsa, mUsedByOtherApps,
+                    mPrimaryOrSplit, mUsedBy, newContext);
+        }
     }
 }