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=";