Implement notifyDexLoad.

This CL implements notifyDexLoad by adding a class `DexUseManager` to
maintain the information about dex uses and answer queries.

The persistence of the information is not in the scope of this CL.

Bug: 249984283
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: I23f651f013672defb2e61af0a744ff6e6d128bfb
diff --git a/libartservice/service/api/system-server-current.txt b/libartservice/service/api/system-server-current.txt
index 06dd5a4..f2b4819 100644
--- a/libartservice/service/api/system-server-current.txt
+++ b/libartservice/service/api/system-server-current.txt
@@ -9,6 +9,7 @@
     method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String);
     method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, int);
     method public int handleShellCommand(@NonNull android.os.Binder, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull String[]);
+    method public void notifyDexContainersLoaded(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull java.util.Map<java.lang.String,java.lang.String>);
     method @NonNull public com.android.server.art.model.OptimizeResult optimizePackage(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull com.android.server.art.model.OptimizeParams);
     method @NonNull public com.android.server.art.model.OptimizeResult optimizePackage(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull com.android.server.art.model.OptimizeParams, @NonNull android.os.CancellationSignal);
   }
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 2d046b1..7b85131 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -47,6 +47,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * This class provides a system API for functionality provided by the ART module.
@@ -131,8 +132,8 @@
             throw new IllegalArgumentException("Nothing to delete");
         }
 
-        PackageState pkgState = getPackageStateOrThrow(snapshot, packageName);
-        AndroidPackage pkg = getPackageOrThrow(pkgState);
+        PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
+        AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
 
         try {
             long freedBytes = 0;
@@ -191,8 +192,8 @@
             throw new IllegalArgumentException("Nothing to check");
         }
 
-        PackageState pkgState = getPackageStateOrThrow(snapshot, packageName);
-        AndroidPackage pkg = getPackageOrThrow(pkgState);
+        PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
+        AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
 
         try {
             List<DexContainerFileOptimizationStatus> statuses = new ArrayList<>();
@@ -261,8 +262,8 @@
             throw new IllegalArgumentException("Nothing to optimize");
         }
 
-        PackageState pkgState = getPackageStateOrThrow(snapshot, packageName);
-        AndroidPackage pkg = getPackageOrThrow(pkgState);
+        PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
+        AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
 
         try {
             return mInjector.getDexOptHelper().dexopt(snapshot, pkgState, pkg, params,
@@ -272,22 +273,28 @@
         }
     }
 
-    private PackageState getPackageStateOrThrow(
-            @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName) {
-        PackageState pkgState = snapshot.getPackageState(packageName);
-        if (pkgState == null) {
-            throw new IllegalArgumentException("Package not found: " + packageName);
-        }
-        return pkgState;
-    }
-
-    private AndroidPackage getPackageOrThrow(@NonNull PackageState pkgState) {
-        AndroidPackage pkg = pkgState.getAndroidPackage();
-        if (pkg == null) {
-            throw new IllegalArgumentException(
-                    "Unable to get package " + pkgState.getPackageName());
-        }
-        return pkg;
+    /**
+     * Notifies ART Service that a list of dex container files have been loaded.
+     *
+     * ART Service uses this information to:
+     * <ul>
+     *   <li>Determine whether an app is used by another app
+     *   <li>Record which secondary dex container files to optimize and how to optimize them
+     * </ul>
+     *
+     * @param loadingPackageName the name of the package who performs the load. ART Service assumes
+     *         that this argument has been validated that it exists in the snapshot and matches the
+     *         calling UID
+     * @param classLoaderContextByDexContainerFile a map from dex container files' absolute paths to
+     *         the string representations of the class loader contexts used to load them
+     * @throws IllegalArgumentException if {@code classLoaderContextByDexContainerFile} contains
+     *         invalid entries
+     */
+    public void notifyDexContainersLoaded(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
+            @NonNull String loadingPackageName,
+            @NonNull Map<String, String> classLoaderContextByDexContainerFile) {
+        DexUseManager.getInstance().addDexUse(
+                snapshot, loadingPackageName, classLoaderContextByDexContainerFile);
     }
 
     /**
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index 5a3293a..e61b294 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -39,6 +39,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
+import java.util.stream.Collectors;
 
 /**
  * This class handles ART shell commands.
@@ -50,6 +51,7 @@
 
     private final ArtManagerLocal mArtManagerLocal;
     private final PackageManagerLocal mPackageManagerLocal;
+    private final DexUseManager mDexUseManager = DexUseManager.getInstance();
 
     private static Map<String, CancellationSignal> sCancellationSignalMap = new HashMap<>();
 
@@ -144,6 +146,30 @@
                     pw.println("Job cancelled");
                     return 0;
                 }
+                case "dex-use-notify": {
+                    mArtManagerLocal.notifyDexContainersLoaded(snapshot, getNextArgRequired(),
+                            Map.of(getNextArgRequired(), getNextArgRequired()));
+                    return 0;
+                }
+                case "dex-use-get-primary": {
+                    String packageName = getNextArgRequired();
+                    String dexPath = getNextArgRequired();
+                    pw.println("Loaders: "
+                            + mDexUseManager.getPrimaryDexLoaders(packageName, dexPath)
+                                      .stream()
+                                      .map(Object::toString)
+                                      .collect(Collectors.joining(", ")));
+                    pw.println("Is used by other apps: "
+                            + mDexUseManager.isPrimaryDexUsedByOtherApps(packageName, dexPath));
+                    return 0;
+                }
+                case "dex-use-get-secondary": {
+                    for (DexUseManager.SecondaryDexInfo info :
+                            mDexUseManager.getSecondaryDexInfo(getNextArgRequired())) {
+                        pw.println(info);
+                    }
+                    return 0;
+                }
                 default:
                     // Handles empty, help, and invalid commands.
                     return handleDefaultCommands(cmd);
@@ -182,6 +208,15 @@
         pw.println("      -f Force compilation.");
         pw.println("  cancel JOB_ID");
         pw.println("    Cancel a job.");
+        pw.println("  dex-use-notify PACKAGE_NAME DEX_PATH CLASS_LOADER_CONTEXT");
+        pw.println("    Notify that a dex file is loaded with the given class loader context by");
+        pw.println("    the given package.");
+        pw.println("  dex-use-get-primary PACKAGE_NAME DEX_PATH");
+        pw.println("    Print the dex use information about a primary dex file owned by the given");
+        pw.println("    package.");
+        pw.println("  dex-use-get-secondary PACKAGE_NAME");
+        pw.println("    Print the dex use information about all secondary dex files owned by the");
+        pw.println("    given package.");
     }
 
     private void enforceRoot() {
diff --git a/libartservice/service/java/com/android/server/art/DexUseManager.java b/libartservice/service/java/com/android/server/art/DexUseManager.java
new file mode 100644
index 0000000..790cd86
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/DexUseManager.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.art;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.Immutable;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.art.wrapper.Environment;
+import com.android.server.art.wrapper.Process;
+import com.android.server.pm.PackageManagerLocal;
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
+
+import com.google.auto.value.AutoValue;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+/**
+ * A singleton class that maintains the information about dex uses. This class is thread-safe.
+ *
+ * This class collects data sent directly by apps, and hence the data should be trusted as little as
+ * possible.
+ *
+ * @hide
+ */
+public class DexUseManager {
+    private static final String TAG = "DexUseManager";
+
+    @GuardedBy("DexUseManager.class") @Nullable private static DexUseManager sInstance = null;
+
+    @GuardedBy("this") @NonNull private DexUse mDexUse = new DexUse();
+
+    @NonNull
+    public static synchronized DexUseManager getInstance() {
+        if (sInstance == null) {
+            sInstance = new DexUseManager();
+        }
+        return sInstance;
+    }
+
+    /** Returns all entities that load the given primary dex file owned by the given package. */
+    @VisibleForTesting
+    @NonNull
+    public synchronized Set<DexLoader> getPrimaryDexLoaders(
+            @NonNull String packageName, @NonNull String dexPath) {
+        PackageDexUse packageDexUse = mDexUse.mPackageDexUseByOwningPackageName.get(packageName);
+        if (packageDexUse == null) {
+            return Set.of();
+        }
+        PrimaryDexUse primaryDexUse = packageDexUse.mPrimaryDexUseByDexFile.get(dexPath);
+        if (primaryDexUse == null) {
+            return Set.of();
+        }
+        return Set.copyOf(primaryDexUse.mLoaders);
+    }
+
+    /** Returns whether a primary dex file owned by the given package is used by other apps. */
+    public boolean isPrimaryDexUsedByOtherApps(
+            @NonNull String packageName, @NonNull String dexPath) {
+        return isUsedByOtherApps(getPrimaryDexLoaders(packageName, dexPath), packageName);
+    }
+
+    /** Returns information about all secondary dex files owned by the given package. */
+    public synchronized @NonNull List<SecondaryDexInfo> getSecondaryDexInfo(
+            @NonNull String packageName) {
+        PackageDexUse packageDexUse = mDexUse.mPackageDexUseByOwningPackageName.get(packageName);
+        if (packageDexUse == null) {
+            return List.of();
+        }
+        var results = new ArrayList<SecondaryDexInfo>();
+        for (var entry : packageDexUse.mSecondaryDexUseByDexFile.entrySet()) {
+            String dexPath = entry.getKey();
+            SecondaryDexUse secondaryDexUse = entry.getValue();
+            if (secondaryDexUse.mRecordByLoader.isEmpty()) {
+                continue;
+            }
+            List<String> distinctClcList =
+                    secondaryDexUse.mRecordByLoader.values()
+                            .stream()
+                            .map(record -> Utils.assertNonEmpty(record.mClassLoaderContext))
+                            .filter(clc
+                                    -> !clc.equals(
+                                            SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT))
+                            .distinct()
+                            .collect(Collectors.toList());
+            String clc;
+            if (distinctClcList.size() == 0) {
+                clc = SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT;
+            } else if (distinctClcList.size() == 1) {
+                clc = distinctClcList.get(0);
+            } else {
+                // If there are more than one class loader contexts, we can't optimize the dex file.
+                clc = SecondaryDexInfo.VARYING_CLASS_LOADER_CONTEXTS;
+            }
+            // Although we filter out unsupported CLCs above, `distinctAbiNames` and `loaders` still
+            // need to take apps with unsupported CLCs into account because the vdex file is still
+            // usable to them.
+            Set<String> distinctAbiNames =
+                    secondaryDexUse.mRecordByLoader.values()
+                            .stream()
+                            .map(record -> Utils.assertNonEmpty(record.mAbiName))
+                            .collect(Collectors.toSet());
+            Set<DexLoader> loaders = Set.copyOf(secondaryDexUse.mRecordByLoader.keySet());
+            results.add(SecondaryDexInfo.create(dexPath,
+                    Objects.requireNonNull(secondaryDexUse.mUserHandle), clc, distinctAbiNames,
+                    loaders, isUsedByOtherApps(loaders, packageName)));
+        }
+        return Collections.unmodifiableList(results);
+    }
+
+    /**
+     * Handles {@link
+     * ArtManagerLocal#notifyDexContainersLoaded(PackageManagerLocal.FilteredSnapshot, String,
+     * Map<String, String>)}.
+     */
+    public void addDexUse(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
+            @NonNull String loadingPackageName,
+            @NonNull Map<String, String> classLoaderContextByDexContainerFile) {
+        // "android" comes from `SystemServerDexLoadReporter`. ART Services doesn't need to handle
+        // this case because it doesn't compile system server and system server isn't allowed to
+        // load artifacts produced by ART Services.
+        if (loadingPackageName.equals("android")) {
+            return;
+        }
+
+        validateInputs(snapshot, loadingPackageName, classLoaderContextByDexContainerFile);
+
+        // TODO(jiakaiz): Investigate whether it should also be considered as isolated process if
+        // `Process.isSdkSandboxUid` returns true.
+        boolean isolatedProcess = Process.isIsolated(Binder.getCallingUid());
+
+        for (var entry : classLoaderContextByDexContainerFile.entrySet()) {
+            String dexPath = Utils.assertNonEmpty(entry.getKey());
+            String classLoaderContext = Utils.assertNonEmpty(entry.getValue());
+            String owningPackageName = findOwningPackage(snapshot, loadingPackageName, dexPath,
+                    DexUseManager::isOwningPackageForPrimaryDex);
+            if (owningPackageName != null) {
+                addPrimaryDexUse(owningPackageName, dexPath, loadingPackageName, isolatedProcess);
+                continue;
+            }
+            owningPackageName = findOwningPackage(snapshot, loadingPackageName, dexPath,
+                    DexUseManager::isOwningPackageForSecondaryDex);
+            if (owningPackageName != null) {
+                PackageState loadingPkgState =
+                        Utils.getPackageStateOrThrow(snapshot, loadingPackageName);
+                // An app is always launched with its primary ABI.
+                Utils.Abi abi = Utils.getPrimaryAbi(loadingPkgState);
+                addSecondaryDexUse(owningPackageName, dexPath, loadingPackageName, isolatedProcess,
+                        classLoaderContext, abi.name());
+                continue;
+            }
+            // It is expected that a dex file isn't owned by any package. For example, the dex file
+            // could be a shared library jar.
+        }
+    }
+
+    @Nullable
+    private static String findOwningPackage(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
+            @NonNull String loadingPackageName, @NonNull String dexPath,
+            @NonNull BiFunction<PackageState, String, Boolean> predicate) {
+        // Most likely, the package is loading its own dex file, so we check this first as an
+        // optimization.
+        PackageState loadingPkgState = Utils.getPackageStateOrThrow(snapshot, loadingPackageName);
+        if (predicate.apply(loadingPkgState, dexPath)) {
+            return loadingPkgState.getPackageName();
+        }
+
+        // TODO(b/246609797): The API can be improved.
+        var packageStates = new ArrayList<PackageState>();
+        snapshot.forAllPackageStates((pkgState) -> { packageStates.add(pkgState); });
+
+        for (PackageState pkgState : packageStates) {
+            if (predicate.apply(pkgState, dexPath)) {
+                return pkgState.getPackageName();
+            }
+        }
+
+        return null;
+    }
+
+    private static boolean isOwningPackageForPrimaryDex(
+            @NonNull PackageState pkgState, @NonNull String dexPath) {
+        AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
+        return PrimaryDexUtils.getDexInfo(pkg).stream().anyMatch(
+                dexInfo -> dexInfo.dexPath().equals(dexPath));
+    }
+
+    private static boolean isOwningPackageForSecondaryDex(
+            @NonNull PackageState pkgState, @NonNull String dexPath) {
+        String volumeUuid =
+                new com.android.server.art.wrapper.PackageState(pkgState).getVolumeUuid();
+        UserHandle handle = Binder.getCallingUserHandle();
+
+        File ceDir = Environment.getDataUserCePackageDirectory(
+                volumeUuid, handle.getIdentifier(), pkgState.getPackageName());
+        if (Paths.get(dexPath).startsWith(ceDir.toPath())) {
+            return true;
+        }
+
+        File deDir = Environment.getDataUserDePackageDirectory(
+                volumeUuid, handle.getIdentifier(), pkgState.getPackageName());
+        if (Paths.get(dexPath).startsWith(deDir.toPath())) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private synchronized void addPrimaryDexUse(@NonNull String owningPackageName,
+            @NonNull String dexPath, @NonNull String loadingPackageName, boolean isolatedProcess) {
+        mDexUse.mPackageDexUseByOwningPackageName
+                .computeIfAbsent(owningPackageName, k -> new PackageDexUse())
+                .mPrimaryDexUseByDexFile.computeIfAbsent(dexPath, k -> new PrimaryDexUse())
+                .mLoaders.add(DexLoader.create(loadingPackageName, isolatedProcess));
+    }
+
+    private synchronized void addSecondaryDexUse(@NonNull String owningPackageName,
+            @NonNull String dexPath, @NonNull String loadingPackageName, boolean isolatedProcess,
+            @NonNull String classLoaderContext, @NonNull String abiName) {
+        SecondaryDexUse secondaryDexUse =
+                mDexUse.mPackageDexUseByOwningPackageName
+                        .computeIfAbsent(owningPackageName, k -> new PackageDexUse())
+                        .mSecondaryDexUseByDexFile.computeIfAbsent(
+                                dexPath, k -> new SecondaryDexUse());
+        secondaryDexUse.mUserHandle = Binder.getCallingUserHandle();
+        SecondaryDexUseRecord record = secondaryDexUse.mRecordByLoader.computeIfAbsent(
+                DexLoader.create(loadingPackageName, isolatedProcess),
+                k -> new SecondaryDexUseRecord());
+        record.mClassLoaderContext = classLoaderContext;
+        record.mAbiName = abiName;
+    }
+
+    @VisibleForTesting
+    public synchronized void clear() {
+        mDexUse = new DexUse();
+    }
+
+    private static boolean isUsedByOtherApps(
+            @NonNull Set<DexLoader> loaders, @NonNull String owningPackageName) {
+        // If the dex file is loaded by an isolated process of the same app, it can also be
+        // considered as "used by other apps" because isolated processes are sandboxed and can only
+        // read world readable files, so they need the optimized artifacts to be world readable. An
+        // example of such a package is webview.
+        return loaders.stream().anyMatch(loader
+                -> !loader.loadingPackageName().equals(owningPackageName)
+                        || loader.isolatedProcess());
+    }
+
+    private static void validateInputs(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
+            @NonNull String loadingPackageName,
+            @NonNull Map<String, String> classLoaderContextByDexContainerFile) {
+        if (classLoaderContextByDexContainerFile.isEmpty()) {
+            throw new IllegalArgumentException("Nothing to record");
+        }
+
+        for (var entry : classLoaderContextByDexContainerFile.entrySet()) {
+            Utils.assertNonEmpty(entry.getKey());
+            if (!Paths.get(entry.getKey()).isAbsolute()) {
+                throw new IllegalArgumentException(String.format(
+                        "Dex container file path must be absolute, got '%s'", entry.getKey()));
+            }
+            Utils.assertNonEmpty(entry.getValue());
+        }
+
+        // TODO(b/253570365): Make the validation more strict.
+    }
+
+    /**
+     * Detailed information about a secondary dex file (an APK or JAR file that an app adds to its
+     * own data directory and loads dynamically).
+     */
+    @Immutable
+    @AutoValue
+    public abstract static class SecondaryDexInfo {
+        // Special encoding used to denote a foreign ClassLoader was found when trying to encode
+        // class loader contexts for each classpath element in a ClassLoader.
+        // Must be in sync with `kUnsupportedClassLoaderContextEncoding` in
+        // `art/runtime/class_loader_context.h`.
+        public static final String UNSUPPORTED_CLASS_LOADER_CONTEXT =
+                "=UnsupportedClassLoaderContext=";
+
+        // Special encoding used to denote that a dex file is loaded by different packages with
+        // different ClassLoader's. Only for display purpose (e.g., in dumpsys). This value is not
+        // written to the file, and so far only used here.
+        @VisibleForTesting
+        public static final String VARYING_CLASS_LOADER_CONTEXTS = "=VaryingClassLoaderContexts=";
+
+        static SecondaryDexInfo create(@NonNull String dexPath, @NonNull UserHandle userHandle,
+                @Nullable String classLoaderContext, @NonNull Set<String> abiNames,
+                @NonNull Set<DexLoader> loaders, boolean isUsedByOtherApps) {
+            return new AutoValue_DexUseManager_SecondaryDexInfo(dexPath, userHandle,
+                    classLoaderContext, Collections.unmodifiableSet(abiNames),
+                    Collections.unmodifiableSet(loaders), isUsedByOtherApps);
+        }
+
+        /** The absolute path to the dex file within the user's app data directory. */
+        public abstract @NonNull String dexPath();
+
+        /**
+         * The {@link UserHandle} that represents the human user who owns and loads the dex file. A
+         * secondary dex file belongs to a specific human user, and only that user can load it.
+         */
+        public abstract @NonNull UserHandle userHandle();
+
+        /**
+         * A string describing the structure of the class loader that the dex file is loaded with,
+         * or {@link #UNSUPPORTED_CLASS_LOADER_CONTEXT} or {@link #VARYING_CLASS_LOADER_CONTEXTS}.
+         */
+        public abstract @NonNull String classLoaderContext();
+
+        /** The set of ABIs of the dex file is loaded with. */
+        public abstract @NonNull Set<String> abiNames();
+
+        /** The set of entities that load the dex file. */
+        public abstract @NonNull Set<DexLoader> loaders();
+
+        /** Returns whether the dex file is used by apps other than the app that owns it. */
+        public abstract boolean isUsedByOtherApps();
+
+        /**
+         * Returns true if the class loader context is suitable for compilation.
+         */
+        public boolean isClassLoaderContextValid() {
+            return !classLoaderContext().equals(UNSUPPORTED_CLASS_LOADER_CONTEXT)
+                    && !classLoaderContext().equals(VARYING_CLASS_LOADER_CONTEXTS);
+        }
+    }
+
+    private static class DexUse {
+        @NonNull Map<String, PackageDexUse> mPackageDexUseByOwningPackageName = new HashMap<>();
+    }
+
+    private static class PackageDexUse {
+        /**
+         * The keys are absolute paths to primary dex files of the owning package (the base APK and
+         * split APKs).
+         */
+        @NonNull Map<String, PrimaryDexUse> mPrimaryDexUseByDexFile = new HashMap<>();
+
+        /**
+         * The keys are absolute paths to secondary dex files of the owning package (the APKs and
+         * JARs in CE and DE directories).
+         */
+        @NonNull Map<String, SecondaryDexUse> mSecondaryDexUseByDexFile = new HashMap<>();
+    }
+
+    private static class PrimaryDexUse {
+        @NonNull Set<DexLoader> mLoaders = new HashSet<>();
+    }
+
+    private static class SecondaryDexUse {
+        @Nullable UserHandle mUserHandle = null;
+        @NonNull Map<DexLoader, SecondaryDexUseRecord> mRecordByLoader = new HashMap<>();
+    }
+
+    /** Represents an entity that loads a dex file. */
+    @Immutable
+    @AutoValue
+    public abstract static class DexLoader {
+        static DexLoader create(@NonNull String loadingPackageName, boolean isolatedProcess) {
+            return new AutoValue_DexUseManager_DexLoader(loadingPackageName, isolatedProcess);
+        }
+
+        abstract @NonNull String loadingPackageName();
+
+        /** @see Process#isIsolated(int) */
+        abstract boolean isolatedProcess();
+    }
+
+    private static class SecondaryDexUseRecord {
+        // An app constructs their own class loader to load a secondary dex file, so only itself
+        // knows the class loader context. Therefore, we need to record the class loader context
+        // reported by the app.
+        @Nullable String mClassLoaderContext = null;
+        @Nullable String mAbiName = null;
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index 546691a..2bd642a 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -24,13 +24,15 @@
 import android.util.SparseArray;
 
 import com.android.server.art.model.OptimizeParams;
+import com.android.server.pm.PackageManagerLocal;
+import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.PackageState;
 
-import com.google.auto.value.AutoValue;
-
 import dalvik.system.DexFile;
 import dalvik.system.VMRuntime;
 
+import com.google.auto.value.AutoValue;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -63,34 +65,38 @@
     @NonNull
     public static List<Abi> getAllAbis(@NonNull PackageState pkgState) {
         List<Abi> abis = new ArrayList<>();
-        String primaryCpuAbi = pkgState.getPrimaryCpuAbi();
-        if (primaryCpuAbi != null) {
-            String isa = getTranslatedIsa(VMRuntime.getInstructionSet(primaryCpuAbi));
-            abis.add(Abi.create(nativeIsaToAbi(isa), isa, true /* isPrimaryAbi */));
-        }
-        String secondaryCpuAbi = pkgState.getSecondaryCpuAbi();
-        if (secondaryCpuAbi != null) {
-            Utils.check(primaryCpuAbi != null);
-            String isa = getTranslatedIsa(VMRuntime.getInstructionSet(secondaryCpuAbi));
+        abis.add(getPrimaryAbi(pkgState));
+        String pkgPrimaryCpuAbi = pkgState.getPrimaryCpuAbi();
+        String pkgSecondaryCpuAbi = pkgState.getSecondaryCpuAbi();
+        if (pkgSecondaryCpuAbi != null) {
+            Utils.check(pkgState.getPrimaryCpuAbi() != null);
+            String isa = getTranslatedIsa(VMRuntime.getInstructionSet(pkgSecondaryCpuAbi));
             abis.add(Abi.create(nativeIsaToAbi(isa), isa, false /* isPrimaryAbi */));
         }
-        if (abis.isEmpty()) {
-            // This is the most common case. The package manager can't infer the ABIs, probably
-            // because the package doesn't contain any native library. The app is launched with
-            // the device's preferred ABI.
-            String preferredAbi = Constants.getPreferredAbi();
-            abis.add(Abi.create(preferredAbi, VMRuntime.getInstructionSet(preferredAbi),
-                    true /* isPrimaryAbi */));
-        }
         // Primary and secondary ABIs should be guaranteed to have different ISAs.
         if (abis.size() == 2 && abis.get(0).isa().equals(abis.get(1).isa())) {
             throw new IllegalStateException(String.format(
                     "Duplicate ISA: primary ABI '%s' ('%s'), secondary ABI '%s' ('%s')",
-                    primaryCpuAbi, abis.get(0).name(), secondaryCpuAbi, abis.get(1).name()));
+                    pkgPrimaryCpuAbi, abis.get(0).name(), pkgSecondaryCpuAbi, abis.get(1).name()));
         }
         return abis;
     }
 
+    @NonNull
+    public static Abi getPrimaryAbi(@NonNull PackageState pkgState) {
+        String primaryCpuAbi = pkgState.getPrimaryCpuAbi();
+        if (primaryCpuAbi != null) {
+            String isa = getTranslatedIsa(VMRuntime.getInstructionSet(primaryCpuAbi));
+            return Abi.create(nativeIsaToAbi(isa), isa, true /* isPrimaryAbi */);
+        }
+        // This is the most common case. The package manager can't infer the ABIs, probably because
+        // the package doesn't contain any native library. The app is launched with the device's
+        // preferred ABI.
+        String preferredAbi = Constants.getPreferredAbi();
+        return Abi.create(
+                preferredAbi, VMRuntime.getInstructionSet(preferredAbi), true /* isPrimaryAbi */);
+    }
+
     /**
      * If the given ISA isn't native to the device, returns the ISA that the native bridge
      * translates it to. Otherwise, returns the ISA as is. This is the ISA that the app is actually
@@ -157,6 +163,34 @@
         }
     }
 
+    @NonNull
+    public static PackageState getPackageStateOrThrow(
+            @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName) {
+        PackageState pkgState = snapshot.getPackageState(packageName);
+        if (pkgState == null) {
+            throw new IllegalArgumentException("Package not found: " + packageName);
+        }
+        return pkgState;
+    }
+
+    @NonNull
+    public static AndroidPackage getPackageOrThrow(@NonNull PackageState pkgState) {
+        AndroidPackage pkg = pkgState.getAndroidPackage();
+        if (pkg == null) {
+            throw new IllegalArgumentException(
+                    "Unable to get package " + pkgState.getPackageName());
+        }
+        return pkg;
+    }
+
+    @NonNull
+    public static String assertNonEmpty(@Nullable String str) {
+        if (TextUtils.isEmpty(str)) {
+            throw new IllegalArgumentException();
+        }
+        return str;
+    }
+
     @AutoValue
     public abstract static class Abi {
         static @NonNull Abi create(
diff --git a/libartservice/service/java/com/android/server/art/wrapper/Environment.java b/libartservice/service/java/com/android/server/art/wrapper/Environment.java
new file mode 100644
index 0000000..ef024fd
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/Environment.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.art.wrapper;
+
+import java.io.File;
+
+/** @hide */
+public class Environment {
+    public static File getDataUserCePackageDirectory(
+            String volumeUuid, int userId, String packageName) {
+        try {
+            return (File) Class.forName("android.os.Environment")
+                    .getMethod(
+                            "getDataUserCePackageDirectory", String.class, int.class, String.class)
+                    .invoke(null, volumeUuid, userId, packageName);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static File getDataUserDePackageDirectory(
+            String volumeUuid, int userId, String packageName) {
+        try {
+            return (File) Class.forName("android.os.Environment")
+                    .getMethod(
+                            "getDataUserDePackageDirectory", String.class, int.class, String.class)
+                    .invoke(null, volumeUuid, userId, packageName);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/PackageState.java b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
new file mode 100644
index 0000000..fafe8ca
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
@@ -0,0 +1,39 @@
+
+/*
+ * Copyright (C) 2022 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 com.android.server.art.wrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/** @hide */
+public class PackageState {
+    @NonNull private final Object mPkgState;
+
+    public PackageState(@NonNull Object pkgState) {
+        mPkgState = pkgState;
+    }
+
+    @Nullable
+    public String getVolumeUuid() {
+        try {
+            return (String) mPkgState.getClass().getMethod("getVolumeUuid").invoke(mPkgState);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/Process.java b/libartservice/service/java/com/android/server/art/wrapper/Process.java
new file mode 100644
index 0000000..44029e4
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/Process.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.art.wrapper;
+
+/** @hide */
+public class Process {
+    public static boolean isIsolated(int uid) {
+        try {
+            return (boolean) Class.forName("android.os.Process")
+                    .getMethod("isIsolated", int.class)
+                    .invoke(null, uid);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/README.md b/libartservice/service/java/com/android/server/art/wrapper/README.md
index a5e5b51..f3d599f 100644
--- a/libartservice/service/java/com/android/server/art/wrapper/README.md
+++ b/libartservice/service/java/com/android/server/art/wrapper/README.md
@@ -4,3 +4,6 @@
 correspond to system APIs planned to be exposed.
 
 The mappings are:
+- `Environment`: `android.os.Environment`
+- `PackageState`: `com.android.server.pm.pkg.PackageState`
+- `Process`: `android.os.Process`
diff --git a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
new file mode 100644
index 0000000..6c66190
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.art;
+
+import static com.android.server.art.DexUseManager.DexLoader;
+import static com.android.server.art.DexUseManager.SecondaryDexInfo;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.ApplicationInfo;
+import android.os.Binder;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.art.testing.StaticMockitoRule;
+import com.android.server.art.wrapper.Environment;
+import com.android.server.art.wrapper.Process;
+import com.android.server.pm.PackageManagerLocal;
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.AndroidPackageSplit;
+import com.android.server.pm.pkg.PackageState;
+
+import dalvik.system.PathClassLoader;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+@SmallTest
+@RunWith(Parameterized.class)
+public class DexUseManagerTest {
+    private static final String LOADING_PKG_NAME = "com.example.loadingpackage";
+    private static final String OWNING_PKG_NAME = "com.example.owningpackage";
+    private static final String BASE_APK = "/data/app/" + OWNING_PKG_NAME + "/base.apk";
+    private static final String SPLIT_APK = "/data/app/" + OWNING_PKG_NAME + "/split_0.apk";
+
+    @Rule
+    public StaticMockitoRule mockitoRule = new StaticMockitoRule(
+            SystemProperties.class, Constants.class, Process.class);
+
+    @Parameter(0) public String mVolumeUuid;
+
+    private final UserHandle mUserHandle = Binder.getCallingUserHandle();
+
+    @Mock private PackageManagerLocal.FilteredSnapshot mSnapshot;
+    private DexUseManager mDexUseManager = DexUseManager.getInstance();
+    private String mCeDir;
+    private String mDeDir;
+
+    @Parameters(name = "volumeUuid={0}")
+    public static Iterable<? extends Object> data() {
+        List<String> volumeUuids = new ArrayList<>();
+        volumeUuids.add(null);
+        volumeUuids.add("volume-abcd");
+        return volumeUuids;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        // No ISA translation.
+        lenient()
+                .when(SystemProperties.get(argThat(arg -> arg.startsWith("ro.dalvik.vm.isa."))))
+                .thenReturn("");
+
+        lenient().when(Constants.getPreferredAbi()).thenReturn("arm64-v8a");
+        lenient().when(Constants.getNative64BitAbi()).thenReturn("arm64-v8a");
+        lenient().when(Constants.getNative32BitAbi()).thenReturn("armeabi-v7a");
+
+        lenient().when(Process.isIsolated(anyInt())).thenReturn(false);
+
+        PackageState loadingPkgState = createPackageState(LOADING_PKG_NAME, "armeabi-v7a");
+        lenient().when(mSnapshot.getPackageState(eq(LOADING_PKG_NAME))).thenReturn(loadingPkgState);
+        PackageState owningPkgState = createPackageState(OWNING_PKG_NAME, "arm64-v8a");
+        lenient().when(mSnapshot.getPackageState(eq(OWNING_PKG_NAME))).thenReturn(owningPkgState);
+
+        lenient()
+                .doAnswer(invocation -> {
+                    var consumer = invocation.<Consumer<PackageState>>getArgument(0);
+                    consumer.accept(loadingPkgState);
+                    consumer.accept(owningPkgState);
+                    return null;
+                })
+                .when(mSnapshot)
+                .forAllPackageStates(any());
+
+        mCeDir = Environment
+                         .getDataUserCePackageDirectory(mVolumeUuid,
+                                 Binder.getCallingUserHandle().getIdentifier(), OWNING_PKG_NAME)
+                         .toString();
+        mDeDir = Environment
+                         .getDataUserDePackageDirectory(mVolumeUuid,
+                                 Binder.getCallingUserHandle().getIdentifier(), OWNING_PKG_NAME)
+                         .toString();
+
+        mDexUseManager.clear();
+    }
+
+    @Test
+    public void testPrimaryDexOwned() {
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC"));
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, BASE_APK))
+                .containsExactly(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */));
+        assertThat(mDexUseManager.isPrimaryDexUsedByOtherApps(OWNING_PKG_NAME, BASE_APK)).isFalse();
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, SPLIT_APK)).isEmpty();
+        assertThat(mDexUseManager.isPrimaryDexUsedByOtherApps(OWNING_PKG_NAME, SPLIT_APK))
+                .isFalse();
+    }
+
+    @Test
+    public void testPrimaryDexOwnedIsolated() {
+        when(Process.isIsolated(anyInt())).thenReturn(true);
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC"));
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, BASE_APK))
+                .containsExactly(DexLoader.create(OWNING_PKG_NAME, true /* isolatedProcess */));
+        assertThat(mDexUseManager.isPrimaryDexUsedByOtherApps(OWNING_PKG_NAME, BASE_APK)).isTrue();
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, SPLIT_APK)).isEmpty();
+        assertThat(mDexUseManager.isPrimaryDexUsedByOtherApps(OWNING_PKG_NAME, SPLIT_APK))
+                .isFalse();
+    }
+
+    @Test
+    public void testPrimaryDexOwnedSplitIsolated() {
+        when(Process.isIsolated(anyInt())).thenReturn(true);
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(SPLIT_APK, "CLC"));
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, BASE_APK)).isEmpty();
+        assertThat(mDexUseManager.isPrimaryDexUsedByOtherApps(OWNING_PKG_NAME, BASE_APK)).isFalse();
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, SPLIT_APK))
+                .containsExactly(DexLoader.create(OWNING_PKG_NAME, true /* isolatedProcess */));
+        assertThat(mDexUseManager.isPrimaryDexUsedByOtherApps(OWNING_PKG_NAME, SPLIT_APK)).isTrue();
+    }
+
+    @Test
+    public void testPrimaryDexOthers() {
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(BASE_APK, "CLC"));
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, BASE_APK))
+                .containsExactly(DexLoader.create(LOADING_PKG_NAME, false /* isolatedProcess */));
+        assertThat(mDexUseManager.isPrimaryDexUsedByOtherApps(OWNING_PKG_NAME, BASE_APK)).isTrue();
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, SPLIT_APK)).isEmpty();
+        assertThat(mDexUseManager.isPrimaryDexUsedByOtherApps(OWNING_PKG_NAME, SPLIT_APK))
+                .isFalse();
+    }
+
+    /** Checks that it ignores and dedups things correctly. */
+    @Test
+    public void testPrimaryDexMultipleEntries() {
+        // These should be ignored.
+        mDexUseManager.addDexUse(mSnapshot, "android", Map.of(BASE_APK, "CLC"));
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
+                Map.of("/data/app/" + OWNING_PKG_NAME + "/non-existing.apk", "CLC"));
+
+        // Some of these should be deduped.
+        mDexUseManager.addDexUse(
+                mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC", SPLIT_APK, "CLC"));
+        mDexUseManager.addDexUse(
+                mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC", SPLIT_APK, "CLC"));
+
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(BASE_APK, "CLC"));
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(BASE_APK, "CLC"));
+
+        when(Process.isIsolated(anyInt())).thenReturn(true);
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC"));
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC"));
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, BASE_APK))
+                .containsExactly(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */),
+                        DexLoader.create(OWNING_PKG_NAME, true /* isolatedProcess */),
+                        DexLoader.create(LOADING_PKG_NAME, false /* isolatedProcess */));
+
+        assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, SPLIT_APK))
+                .containsExactly(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */));
+    }
+
+    @Test
+    public void testSecondaryDexOwned() {
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+
+        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        assertThat(dexInfoList)
+                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle, "CLC",
+                        Set.of("arm64-v8a"),
+                        Set.of(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */)),
+                        false /* isUsedByOtherApps */));
+        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isTrue();
+    }
+
+    @Test
+    public void testSecondaryDexOwnedIsolated() {
+        when(Process.isIsolated(anyInt())).thenReturn(true);
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mDeDir + "/foo.apk", "CLC"));
+
+        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        assertThat(dexInfoList)
+                .containsExactly(SecondaryDexInfo.create(mDeDir + "/foo.apk", mUserHandle, "CLC",
+                        Set.of("arm64-v8a"),
+                        Set.of(DexLoader.create(OWNING_PKG_NAME, true /* isolatedProcess */)),
+                        true /* isUsedByOtherApps */));
+        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isTrue();
+    }
+
+    @Test
+    public void testSecondaryDexOthers() {
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+
+        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        assertThat(dexInfoList)
+                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle, "CLC",
+                        Set.of("armeabi-v7a"),
+                        Set.of(DexLoader.create(LOADING_PKG_NAME, false /* isolatedProcess */)),
+                        true /* isUsedByOtherApps */));
+        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isTrue();
+    }
+
+    @Test
+    public void testSecondaryDexUnsupportedClc() {
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME,
+                Map.of(mCeDir + "/foo.apk", SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT));
+
+        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        assertThat(dexInfoList)
+                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                        SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT, Set.of("armeabi-v7a"),
+                        Set.of(DexLoader.create(LOADING_PKG_NAME, false /* isolatedProcess */)),
+                        true /* isUsedByOtherApps */));
+        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isFalse();
+    }
+
+    @Test
+    public void testSecondaryDexVariableClc() {
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC2"));
+
+        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        assertThat(dexInfoList)
+                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                        SecondaryDexInfo.VARYING_CLASS_LOADER_CONTEXTS,
+                        Set.of("arm64-v8a", "armeabi-v7a"),
+                        Set.of(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */),
+                                DexLoader.create(LOADING_PKG_NAME, false /* isolatedProcess */)),
+                        true /* isUsedByOtherApps */));
+        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isFalse();
+    }
+
+    /** Checks that it ignores and dedups things correctly. */
+    @Test
+    public void testSecondaryDexMultipleEntries() {
+        // These should be ignored.
+        mDexUseManager.addDexUse(mSnapshot, "android", Map.of(mCeDir + "/foo.apk", "CLC"));
+        mDexUseManager.addDexUse(
+                mSnapshot, OWNING_PKG_NAME, Map.of("/some/non-existing.apk", "CLC"));
+
+        // Some of these should be deduped.
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
+                Map.of(mCeDir + "/foo.apk", "CLC", mCeDir + "/bar.apk", "CLC"));
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
+                Map.of(mCeDir + "/foo.apk", "UpdatedCLC", mCeDir + "/bar.apk", "UpdatedCLC"));
+
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+        mDexUseManager.addDexUse(
+                mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "UpdatedCLC"));
+
+        mDexUseManager.addDexUse(
+                mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/bar.apk", "DifferentCLC"));
+        mDexUseManager.addDexUse(
+                mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/bar.apk", "UpdatedDifferentCLC"));
+
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
+                Map.of(mCeDir + "/baz.apk", SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT));
+
+        when(Process.isIsolated(anyInt())).thenReturn(true);
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
+                Map.of(mCeDir + "/foo.apk", SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT));
+
+        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        assertThat(dexInfoList)
+                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                                         "UpdatedCLC", Set.of("arm64-v8a", "armeabi-v7a"),
+                                         Set.of(DexLoader.create(OWNING_PKG_NAME,
+                                                        false /* isolatedProcess */),
+                                                 DexLoader.create(OWNING_PKG_NAME,
+                                                         true /* isolatedProcess */),
+                                                 DexLoader.create(LOADING_PKG_NAME,
+                                                         false /* isolatedProcess */)),
+                                         true /* isUsedByOtherApps */),
+                        SecondaryDexInfo.create(mCeDir + "/bar.apk", mUserHandle,
+                                SecondaryDexInfo.VARYING_CLASS_LOADER_CONTEXTS,
+                                Set.of("arm64-v8a", "armeabi-v7a"),
+                                Set.of(DexLoader.create(
+                                               OWNING_PKG_NAME, false /* isolatedProcess */),
+                                        DexLoader.create(
+                                                LOADING_PKG_NAME, false /* isolatedProcess */)),
+                                true /* isUsedByOtherApps */),
+                        SecondaryDexInfo.create(mCeDir + "/baz.apk", mUserHandle,
+                                SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT,
+                                Set.of("arm64-v8a"),
+                                Set.of(DexLoader.create(
+                                        OWNING_PKG_NAME, false /* isolatedProcess */)),
+                                false /* isUsedByOtherApps */));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testUnknownPackage() {
+        mDexUseManager.addDexUse(mSnapshot, "bogus", Map.of(BASE_APK, "CLC"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testEmptyMap() {
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testNullKey() {
+        var map = new HashMap<String, String>();
+        map.put(null, "CLC");
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, map);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testNonAbsoluteKey() {
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of("a/b.jar", "CLC"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testNullValue() {
+        var map = new HashMap<String, String>();
+        map.put(mCeDir + "/foo.apk", null);
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, map);
+    }
+
+    private AndroidPackage createPackage(String packageName) {
+        AndroidPackage pkg = mock(AndroidPackage.class);
+
+        var baseSplit = mock(AndroidPackageSplit.class);
+        lenient().when(baseSplit.getPath()).thenReturn("/data/app/" + packageName + "/base.apk");
+        lenient().when(baseSplit.isHasCode()).thenReturn(true);
+        lenient().when(baseSplit.getClassLoaderName()).thenReturn(PathClassLoader.class.getName());
+
+        var split0 = mock(AndroidPackageSplit.class);
+        lenient().when(split0.getName()).thenReturn("split_0");
+        lenient().when(split0.getPath()).thenReturn("/data/app/" + packageName + "/split_0.apk");
+        lenient().when(split0.isHasCode()).thenReturn(true);
+
+        lenient().when(pkg.getSplits()).thenReturn(List.of(baseSplit, split0));
+
+        return pkg;
+    }
+
+    private PackageState createPackageState(String packageName, String primaryAbi) {
+        PackageState pkgState = mock(PackageState.class);
+        lenient().when(pkgState.getPackageName()).thenReturn(packageName);
+        AndroidPackage pkg = createPackage(packageName);
+        lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
+        lenient().when(pkgState.getPrimaryCpuAbi()).thenReturn(primaryAbi);
+        lenient().when(pkgState.getVolumeUuid()).thenReturn(mVolumeUuid);
+        return pkgState;
+    }
+}
diff --git a/runtime/class_loader_context.h b/runtime/class_loader_context.h
index ccc5c73..806ab5e 100644
--- a/runtime/class_loader_context.h
+++ b/runtime/class_loader_context.h
@@ -51,7 +51,10 @@
 
   // Special encoding used to denote a foreign ClassLoader was found when trying to encode class
   // loader contexts for each classpath element in a ClassLoader. See
-  // EncodeClassPathContextsForClassLoader. Keep in sync with PackageDexUsage in the framework.
+  // EncodeClassPathContextsForClassLoader. Keep in sync with PackageDexUsage in the framework
+  // (frameworks/base/services/core/java/com/android/server/pm/dex/PackageDexUsage.java) and
+  // DexUseManager in ART Services
+  // (art/libartservice/service/java/com/android/server/art/DexUseManager.java).
   static constexpr const char* kUnsupportedClassLoaderContextEncoding =
       "=UnsupportedClassLoaderContext=";