ART services: optimize package - Implement PrimaryDexOptimizer.
This CL contains the basic implementation of app compilation.
Bug: 229268202
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: Ib5940b16c8f6b7f650584d2b770e7fbd40cb75ca
diff --git a/artd/artd.cc b/artd/artd.cc
index 2cdca3c..1cd2655 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -149,18 +149,18 @@
return compiler_filter;
}
-OatFileAssistant::DexOptTrigger DexOptTriggerFromAidl(int8_t aidl_value) {
+OatFileAssistant::DexOptTrigger DexOptTriggerFromAidl(int32_t aidl_value) {
OatFileAssistant::DexOptTrigger trigger{};
- if ((aidl_value & static_cast<int8_t>(DexoptTrigger::COMPILER_FILTER_IS_BETTER)) != 0) {
+ if ((aidl_value & static_cast<int32_t>(DexoptTrigger::COMPILER_FILTER_IS_BETTER)) != 0) {
trigger.targetFilterIsBetter = true;
}
- if ((aidl_value & static_cast<int8_t>(DexoptTrigger::COMPILER_FILTER_IS_SAME)) != 0) {
+ if ((aidl_value & static_cast<int32_t>(DexoptTrigger::COMPILER_FILTER_IS_SAME)) != 0) {
trigger.targetFilterIsSame = true;
}
- if ((aidl_value & static_cast<int8_t>(DexoptTrigger::COMPILER_FILTER_IS_WORSE)) != 0) {
+ if ((aidl_value & static_cast<int32_t>(DexoptTrigger::COMPILER_FILTER_IS_WORSE)) != 0) {
trigger.targetFilterIsWorse = true;
}
- if ((aidl_value & static_cast<int8_t>(DexoptTrigger::PRIMARY_BOOT_IMAGE_BECOMES_USABLE)) != 0) {
+ if ((aidl_value & static_cast<int32_t>(DexoptTrigger::PRIMARY_BOOT_IMAGE_BECOMES_USABLE)) != 0) {
trigger.primaryBootImageBecomesUsable = true;
}
return trigger;
@@ -331,7 +331,7 @@
const std::string& in_instructionSet,
const std::string& in_classLoaderContext,
const std::string& in_compilerFilter,
- int8_t in_dexoptTrigger,
+ int32_t in_dexoptTrigger,
GetDexoptNeededResult* _aidl_return) {
Result<OatFileAssistantContext*> ofa_context = GetOatFileAssistantContext();
if (!ofa_context.ok()) {
diff --git a/artd/artd.h b/artd/artd.h
index 54d6776..18ffa3c 100644
--- a/artd/artd.h
+++ b/artd/artd.h
@@ -58,7 +58,7 @@
const std::string& in_instructionSet,
const std::string& in_classLoaderContext,
const std::string& in_compilerFilter,
- int8_t in_dexoptTrigger,
+ int32_t in_dexoptTrigger,
aidl::com::android::server::art::GetDexoptNeededResult* _aidl_return) override;
ndk::ScopedAStatus dexopt(
diff --git a/artd/binder/com/android/server/art/DexoptTrigger.aidl b/artd/binder/com/android/server/art/DexoptTrigger.aidl
index a160f22..58a9ec8 100644
--- a/artd/binder/com/android/server/art/DexoptTrigger.aidl
+++ b/artd/binder/com/android/server/art/DexoptTrigger.aidl
@@ -25,6 +25,7 @@
*
* @hide
*/
+@Backing(type="int")
enum DexoptTrigger {
COMPILER_FILTER_IS_BETTER = 1 << 0,
COMPILER_FILTER_IS_SAME = 1 << 1,
diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl
index 6750d9c..47c1ade 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -46,7 +46,7 @@
com.android.server.art.GetDexoptNeededResult getDexoptNeeded(
@utf8InCpp String dexFile, @utf8InCpp String instructionSet,
@utf8InCpp String classLoaderContext, @utf8InCpp String compilerFilter,
- byte dexoptTrigger);
+ int dexoptTrigger);
/**
* Dexopts a dex file for the given instruction set. Returns true on success, or false if
diff --git a/libartservice/service/java/com/android/server/art/AidlUtils.java b/libartservice/service/java/com/android/server/art/AidlUtils.java
new file mode 100644
index 0000000..447c3d6
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/AidlUtils.java
@@ -0,0 +1,80 @@
+/*
+ * 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.OutputArtifacts.PermissionSettings;
+import static com.android.server.art.OutputArtifacts.PermissionSettings.SeContext;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/** @hide */
+public final class AidlUtils {
+ private AidlUtils() {}
+
+ @NonNull
+ public static ArtifactsPath buildArtifactsPath(
+ @NonNull String dexPath, @NonNull String isa, boolean isInDalvikCache) {
+ var artifactsPath = new ArtifactsPath();
+ artifactsPath.dexPath = dexPath;
+ artifactsPath.isa = isa;
+ artifactsPath.isInDalvikCache = isInDalvikCache;
+ return artifactsPath;
+ }
+
+ @NonNull
+ public static FsPermission buildFsPermission(
+ int uid, int gid, boolean isOtherReadable, boolean isOtherExecutable) {
+ var fsPermission = new FsPermission();
+ fsPermission.uid = uid;
+ fsPermission.gid = gid;
+ fsPermission.isOtherReadable = isOtherReadable;
+ fsPermission.isOtherExecutable = isOtherExecutable;
+ return fsPermission;
+ }
+
+ @NonNull
+ public static FsPermission buildFsPermission(int uid, int gid, boolean isOtherReadable) {
+ return buildFsPermission(uid, gid, isOtherReadable, false /* isOtherExecutable */);
+ }
+
+ @NonNull
+ public static DexMetadataPath buildDexMetadataPath(@NonNull String dexPath) {
+ var dexMetadataPath = new DexMetadataPath();
+ dexMetadataPath.dexPath = dexPath;
+ return dexMetadataPath;
+ }
+
+ @NonNull
+ public static PermissionSettings buildPermissionSettings(@NonNull FsPermission dirFsPermission,
+ @NonNull FsPermission fileFsPermission, @Nullable SeContext seContext) {
+ var permissionSettings = new PermissionSettings();
+ permissionSettings.dirFsPermission = dirFsPermission;
+ permissionSettings.fileFsPermission = fileFsPermission;
+ permissionSettings.seContext = seContext;
+ return permissionSettings;
+ }
+
+ @NonNull
+ public static OutputArtifacts buildOutputArtifacts(@NonNull String dexPath, @NonNull String isa,
+ boolean isInDalvikCache, @NonNull PermissionSettings permissionSettings) {
+ var outputArtifacts = new OutputArtifacts();
+ outputArtifacts.artifactsPath = buildArtifactsPath(dexPath, isa, isInDalvikCache);
+ outputArtifacts.permissionSettings = permissionSettings;
+ return outputArtifacts;
+ }
+}
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 9b2e9f8..cf083cc 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -142,8 +142,9 @@
continue;
}
for (String isa : Utils.getAllIsas(pkgState)) {
- freedBytes += mInjector.getArtd().deleteArtifacts(
- Utils.buildArtifactsPath(dexInfo.dexPath(), isa, isInDalvikCache));
+ freedBytes +=
+ mInjector.getArtd().deleteArtifacts(AidlUtils.buildArtifactsPath(
+ dexInfo.dexPath(), isa, isInDalvikCache));
}
}
}
@@ -236,7 +237,11 @@
PackageState pkgState = getPackageStateOrThrow(snapshot, packageName);
AndroidPackageApi pkg = getPackageOrThrow(pkgState);
- return mInjector.getDexOptHelper().dexopt(snapshot, pkgState, pkg, options);
+ try {
+ return mInjector.getDexOptHelper().dexopt(snapshot, pkgState, pkg, options);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("An error occurred when calling artd", e);
+ }
}
private PackageState getPackageStateOrThrow(
@@ -252,7 +257,8 @@
private AndroidPackageApi getPackageOrThrow(@NonNull PackageState pkgState) {
AndroidPackageApi pkg = pkgState.getAndroidPackage();
if (pkg == null) {
- throw new IllegalStateException("Unable to get package " + pkgState.getPackageName());
+ throw new IllegalArgumentException(
+ "Unable to get package " + pkgState.getPackageName());
}
return pkg;
}
diff --git a/libartservice/service/java/com/android/server/art/DexOptHelper.java b/libartservice/service/java/com/android/server/art/DexOptHelper.java
index a5cee0b..64060ca 100644
--- a/libartservice/service/java/com/android/server/art/DexOptHelper.java
+++ b/libartservice/service/java/com/android/server/art/DexOptHelper.java
@@ -24,6 +24,7 @@
import android.content.Context;
import android.os.Binder;
import android.os.PowerManager;
+import android.os.RemoteException;
import android.os.WorkSource;
import com.android.internal.annotations.VisibleForTesting;
@@ -71,7 +72,7 @@
@NonNull
public OptimizeResult dexopt(@NonNull PackageDataSnapshot snapshot,
@NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg,
- @NonNull OptimizeOptions options) {
+ @NonNull OptimizeOptions options) throws RemoteException {
List<DexFileOptimizeResult> results = new ArrayList<>();
Supplier<OptimizeResult> createResult = ()
-> new OptimizeResult(pkgState.getPackageName(), options.getCompilerFilter(),
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
index 7315040..1336889 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
@@ -16,17 +16,32 @@
package com.android.server.art;
+import static com.android.server.art.GetDexoptNeededResult.ArtifactsLocation;
+import static com.android.server.art.OutputArtifacts.PermissionSettings;
+import static com.android.server.art.OutputArtifacts.PermissionSettings.SeContext;
+import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo;
import static com.android.server.art.model.OptimizeResult.DexFileOptimizeResult;
+import android.R;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.art.model.OptimizeOptions;
+import com.android.server.art.model.OptimizeResult;
import com.android.server.art.wrapper.AndroidPackageApi;
import com.android.server.art.wrapper.PackageState;
+import dalvik.system.DexFile;
+
+import java.util.ArrayList;
import java.util.List;
/** @hide */
@@ -50,8 +65,237 @@
*/
@NonNull
public List<DexFileOptimizeResult> dexopt(@NonNull PackageState pkgState,
+ @NonNull AndroidPackageApi pkg, @NonNull OptimizeOptions options)
+ throws RemoteException {
+ List<DexFileOptimizeResult> results = new ArrayList<>();
+
+ String targetCompilerFilter = adjustCompilerFilter(
+ pkgState, pkg, options.getCompilerFilter(), options.getReason());
+ if (targetCompilerFilter.equals(OptimizeOptions.COMPILER_FILTER_NOOP)) {
+ return results;
+ }
+
+ boolean isInDalvikCache = Utils.isInDalvikCache(pkgState);
+
+ for (DetailedPrimaryDexInfo dexInfo : PrimaryDexUtils.getDetailedDexInfo(pkgState, pkg)) {
+ try {
+ if (!dexInfo.hasCode()) {
+ continue;
+ }
+
+ // TODO(jiakaiz): Support optimizing a single split.
+
+ String compilerFilter = targetCompilerFilter;
+
+ if (DexFile.isProfileGuidedCompilerFilter(compilerFilter)) {
+ throw new UnsupportedOperationException(
+ "Profile-guided compilation is not implemented");
+ }
+ PermissionSettings permissionSettings =
+ getPermissionSettings(pkgState, pkg, true /* canBePublic */);
+
+ DexoptOptions dexoptOptions = getDexoptOptions(pkgState, pkg, options);
+
+ for (String isa : Utils.getAllIsas(pkgState)) {
+ @OptimizeResult.OptimizeStatus int status = OptimizeResult.OPTIMIZE_SKIPPED;
+ try {
+ GetDexoptNeededResult getDexoptNeededResult = getDexoptNeeded(dexInfo, isa,
+ compilerFilter, options.getShouldDowngrade(), options.getForce());
+
+ if (!getDexoptNeededResult.isDexoptNeeded) {
+ continue;
+ }
+
+ ProfilePath inputProfile = null;
+
+ status = dexoptFile(dexInfo, isa, isInDalvikCache, compilerFilter,
+ inputProfile, getDexoptNeededResult, permissionSettings,
+ options.getPriorityClass(), dexoptOptions);
+ } catch (ServiceSpecificException e) {
+ // Log the error and continue.
+ Log.e(TAG,
+ String.format("Failed to dexopt [packageName = %s, dexPath = %s, "
+ + "isa = %s, classLoaderContext = %s]",
+ pkgState.getPackageName(), dexInfo.dexPath(), isa,
+ dexInfo.classLoaderContext()),
+ e);
+ status = OptimizeResult.OPTIMIZE_FAILED;
+ } finally {
+ results.add(new DexFileOptimizeResult(
+ dexInfo.dexPath(), isa, compilerFilter, status));
+ }
+ }
+ } finally {
+ // TODO(jiakaiz): Cleanup profile.
+ }
+ }
+
+ return results;
+ }
+
+ @NonNull
+ private String adjustCompilerFilter(@NonNull PackageState pkgState,
+ @NonNull AndroidPackageApi pkg, @NonNull String targetCompilerFilter,
+ @NonNull String reason) {
+ if (mInjector.isSystemUiPackage(pkgState.getPackageName())) {
+ String systemUiCompilerFilter = getSystemUiCompilerFilter();
+ if (!systemUiCompilerFilter.isEmpty()) {
+ return systemUiCompilerFilter;
+ }
+ }
+
+ // We force vmSafeMode on debuggable apps as well:
+ // - the runtime ignores their compiled code
+ // - they generally have lots of methods that could make the compiler used run out of
+ // memory (b/130828957)
+ // Note that forcing the compiler filter here applies to all compilations (even if they
+ // are done via adb shell commands). This is okay because the runtime will ignore the
+ // compiled code anyway.
+ if (pkg.isVmSafeMode() || pkg.isDebuggable()) {
+ return DexFile.getSafeModeCompilerFilter(targetCompilerFilter);
+ }
+
+ return targetCompilerFilter;
+ }
+
+ @NonNull
+ private String getSystemUiCompilerFilter() {
+ String compilerFilter = SystemProperties.get("dalvik.vm.systemuicompilerfilter");
+ if (!compilerFilter.isEmpty() && !Utils.isValidArtServiceCompilerFilter(compilerFilter)) {
+ throw new IllegalStateException(
+ "Got invalid compiler filter '" + compilerFilter + "' for System UI");
+ }
+ return compilerFilter;
+ }
+
+ @NonNull
+ PermissionSettings getPermissionSettings(
+ @NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg, boolean canBePublic) {
+ int uid = pkg.getUid();
+ if (uid < 0) {
+ throw new IllegalStateException(
+ "Package '" + pkgState.getPackageName() + "' has invalid app uid");
+ }
+ int sharedGid = UserHandle.getSharedAppGid(uid);
+ if (sharedGid < 0) {
+ throw new IllegalStateException(
+ String.format("Unable to get shared gid for package '%s' (uid: %d)",
+ pkgState.getPackageName(), uid));
+ }
+
+ // The files and directories should belong to the system so that Package Manager can manage
+ // them (e.g., move them around).
+ // We don't need the "read" bit for "others" on the directories because others only need to
+ // access the files in the directories, but they don't need to "ls" the directories.
+ FsPermission dirFsPermission = AidlUtils.buildFsPermission(Process.SYSTEM_UID,
+ Process.SYSTEM_UID, false /* isOtherReadable */, true /* isOtherExecutable */);
+ FsPermission fileFsPermission =
+ AidlUtils.buildFsPermission(Process.SYSTEM_UID, sharedGid, canBePublic);
+ // For primary dex, we can use the default SELinux context.
+ SeContext seContext = null;
+ return AidlUtils.buildPermissionSettings(dirFsPermission, fileFsPermission, seContext);
+ }
+
+ @NonNull
+ private DexoptOptions getDexoptOptions(@NonNull PackageState pkgState,
@NonNull AndroidPackageApi pkg, @NonNull OptimizeOptions options) {
- throw new UnsupportedOperationException();
+ DexoptOptions dexoptOptions = new DexoptOptions();
+ dexoptOptions.compilationReason = options.getReason();
+ dexoptOptions.targetSdkVersion = pkg.getTargetSdkVersion();
+ dexoptOptions.debuggable = pkg.isDebuggable() || isAlwaysDebuggable();
+ dexoptOptions.generateAppImage = false;
+ dexoptOptions.hiddenApiPolicyEnabled = isHiddenApiPolicyEnabled(pkgState, pkg);
+ return dexoptOptions;
+ }
+
+ private boolean isAlwaysDebuggable() {
+ return SystemProperties.getBoolean("dalvik.vm.always_debuggable", false /* def */);
+ }
+
+ private boolean isAppImageEnabled() {
+ return !SystemProperties.get("dalvik.vm.appimageformat").isEmpty();
+ }
+
+ private boolean isHiddenApiPolicyEnabled(
+ @NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg) {
+ if (pkg.isSignedWithPlatformKey()) {
+ return false;
+ }
+ if (pkgState.isSystem() || pkgState.isUpdatedSystemApp()) {
+ // TODO(b/236389629): Check whether the app is in hidden api whitelist.
+ return !pkg.isUsesNonSdkApi();
+ }
+ return true;
+ }
+
+ @NonNull
+ GetDexoptNeededResult getDexoptNeeded(@NonNull DetailedPrimaryDexInfo dexInfo,
+ @NonNull String isa, @NonNull String compilerFilter, boolean shouldDowngrade,
+ boolean force) throws RemoteException {
+ int dexoptTrigger = getDexoptTrigger(shouldDowngrade, force);
+
+ // The result should come from artd even if all the bits of `dexoptTrigger` are set
+ // because the result also contains information about the usable VDEX file.
+ GetDexoptNeededResult result = mInjector.getArtd().getDexoptNeeded(dexInfo.dexPath(), isa,
+ dexInfo.classLoaderContext(), compilerFilter, dexoptTrigger);
+
+ return result;
+ }
+
+ int getDexoptTrigger(boolean shouldDowngrade, boolean force) {
+ if (force) {
+ return DexoptTrigger.COMPILER_FILTER_IS_BETTER | DexoptTrigger.COMPILER_FILTER_IS_SAME
+ | DexoptTrigger.COMPILER_FILTER_IS_WORSE
+ | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+ }
+
+ if (shouldDowngrade) {
+ return DexoptTrigger.COMPILER_FILTER_IS_WORSE;
+ }
+
+ return DexoptTrigger.COMPILER_FILTER_IS_BETTER
+ | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+ }
+
+ private @OptimizeResult.OptimizeStatus int dexoptFile(@NonNull DetailedPrimaryDexInfo dexInfo,
+ @NonNull String isa, boolean isInDalvikCache, @NonNull String compilerFilter,
+ @Nullable ProfilePath profile, @NonNull GetDexoptNeededResult getDexoptNeededResult,
+ @NonNull PermissionSettings permissionSettings, @PriorityClass byte priorityClass,
+ @NonNull DexoptOptions dexoptOptions) throws RemoteException {
+ OutputArtifacts outputArtifacts = AidlUtils.buildOutputArtifacts(
+ dexInfo.dexPath(), isa, isInDalvikCache, permissionSettings);
+
+ VdexPath inputVdex = getInputVdex(getDexoptNeededResult, dexInfo.dexPath(), isa);
+
+ if (!mInjector.getArtd().dexopt(outputArtifacts, dexInfo.dexPath(), isa,
+ dexInfo.classLoaderContext(), compilerFilter, profile, inputVdex, priorityClass,
+ dexoptOptions)) {
+ return OptimizeResult.OPTIMIZE_CANCELLED;
+ }
+
+ return OptimizeResult.OPTIMIZE_PERFORMED;
+ }
+
+ @Nullable
+ private VdexPath getInputVdex(@NonNull GetDexoptNeededResult getDexoptNeededResult,
+ @NonNull String dexPath, @NonNull String isa) {
+ if (!getDexoptNeededResult.isVdexUsable) {
+ return null;
+ }
+ switch (getDexoptNeededResult.artifactsLocation) {
+ case ArtifactsLocation.DALVIK_CACHE:
+ return VdexPath.artifactsPath(
+ AidlUtils.buildArtifactsPath(dexPath, isa, true /* isInDalvikCache */));
+ case ArtifactsLocation.NEXT_TO_DEX:
+ return VdexPath.artifactsPath(
+ AidlUtils.buildArtifactsPath(dexPath, isa, false /* isInDalvikCache */));
+ case ArtifactsLocation.DM:
+ return VdexPath.dexMetadataPath(AidlUtils.buildDexMetadataPath(dexPath));
+ default:
+ // This should never happen as the value is got from artd.
+ throw new IllegalStateException(
+ "Unknown artifacts location " + getDexoptNeededResult.artifactsLocation);
+ }
}
/**
@@ -68,6 +312,13 @@
mContext = context;
}
+ boolean isSystemUiPackage(@NonNull String packageName) {
+ if (mContext == null) {
+ return false;
+ }
+ return packageName.equals(mContext.getString(R.string.config_systemUi));
+ }
+
@NonNull
public IArtd getArtd() {
return Utils.getArtd();
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index b30d208..68cdf07 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -71,16 +71,6 @@
return List.of();
}
- @NonNull
- public static ArtifactsPath buildArtifactsPath(
- @NonNull String dexPath, @NonNull String isa, boolean isInDalvikCache) {
- ArtifactsPath artifactsPath = new ArtifactsPath();
- artifactsPath.dexPath = dexPath;
- artifactsPath.isa = isa;
- artifactsPath.isInDalvikCache = isInDalvikCache;
- return artifactsPath;
- }
-
public static boolean isInDalvikCache(@NonNull PackageState pkg) {
return pkg.isSystem() && !pkg.isUpdatedSystemApp();
}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java b/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java
index 1c3ff6d..66f82a6 100644
--- a/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java
+++ b/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java
@@ -122,4 +122,44 @@
throw new RuntimeException(e);
}
}
+
+ public boolean isVmSafeMode() {
+ try {
+ return (boolean) mPkg.getClass().getMethod("isVmSafeMode").invoke(mPkg);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isDebuggable() {
+ try {
+ return (boolean) mPkg.getClass().getMethod("isDebuggable").invoke(mPkg);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isSignedWithPlatformKey() {
+ try {
+ return (boolean) mPkg.getClass().getMethod("isSignedWithPlatformKey").invoke(mPkg);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isUsesNonSdkApi() {
+ try {
+ return (boolean) mPkg.getClass().getMethod("isUsesNonSdkApi").invoke(mPkg);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int getTargetSdkVersion() {
+ try {
+ return (int) mPkg.getClass().getMethod("getTargetSdkVersion").invoke(mPkg);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index afaa3ee..ded89c9 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -135,7 +135,7 @@
mArtManagerLocal.deleteOptimizedArtifacts(mock(PackageDataSnapshot.class), PKG_NAME);
}
- @Test(expected = IllegalStateException.class)
+ @Test(expected = IllegalArgumentException.class)
public void testDeleteOptimizedArtifactsNoPackage() throws Exception {
when(mPkgState.getAndroidPackage()).thenReturn(null);
@@ -192,7 +192,7 @@
mArtManagerLocal.getOptimizationStatus(mock(PackageDataSnapshot.class), PKG_NAME);
}
- @Test(expected = IllegalStateException.class)
+ @Test(expected = IllegalArgumentException.class)
public void testGetOptimizationStatusNoPackage() throws Exception {
when(mPkgState.getAndroidPackage()).thenReturn(null);
@@ -238,7 +238,7 @@
new OptimizeOptions.Builder("install").build());
}
- @Test(expected = IllegalStateException.class)
+ @Test(expected = IllegalArgumentException.class)
public void testOptimizePackageNoPackage() throws Exception {
when(mPkgState.getAndroidPackage()).thenReturn(null);
diff --git a/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java b/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
index dffbc1f..e68f21b 100644
--- a/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
@@ -94,7 +94,7 @@
}
@Test
- public void testDexopt() {
+ public void testDexopt() throws Exception {
when(mPrimaryDexOptimizer.dexopt(same(mPkgState), same(mPkg), same(mOptions)))
.thenReturn(mPrimaryResults);
@@ -109,7 +109,7 @@
}
@Test
- public void testDexoptNoCode() {
+ public void testDexoptNoCode() throws Exception {
when(mPkg.isHasCode()).thenReturn(false);
OptimizeResult result =
@@ -120,7 +120,7 @@
}
@Test
- public void testDexoptWithAppHibernationManager() {
+ public void testDexoptWithAppHibernationManager() throws Exception {
when(mInjector.getAppHibernationManager()).thenReturn(mAhm);
lenient().when(mAhm.isHibernatingGlobally(PKG_NAME)).thenReturn(false);
lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(true);
@@ -135,7 +135,7 @@
}
@Test
- public void testDexoptIsHibernating() {
+ public void testDexoptIsHibernating() throws Exception {
when(mInjector.getAppHibernationManager()).thenReturn(mAhm);
lenient().when(mAhm.isHibernatingGlobally(PKG_NAME)).thenReturn(true);
lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(true);
@@ -148,7 +148,7 @@
}
@Test
- public void testDexoptIsHibernatingButOatArtifactDeletionDisabled() {
+ public void testDexoptIsHibernatingButOatArtifactDeletionDisabled() throws Exception {
when(mInjector.getAppHibernationManager()).thenReturn(mAhm);
lenient().when(mAhm.isHibernatingGlobally(PKG_NAME)).thenReturn(true);
lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(false);
@@ -163,7 +163,7 @@
}
@Test
- public void testDexoptWithPowerManager() {
+ public void testDexoptWithPowerManager() throws Exception {
var wakeLock = mock(PowerManager.WakeLock.class);
when(mInjector.getPowerManager()).thenReturn(mPowerManager);
when(mPowerManager.newWakeLock(eq(PowerManager.PARTIAL_WAKE_LOCK), any()))
@@ -182,7 +182,7 @@
}
@Test
- public void testDexoptAlwaysReleasesWakeLock() {
+ public void testDexoptAlwaysReleasesWakeLock() throws Exception {
var wakeLock = mock(PowerManager.WakeLock.class);
when(mInjector.getPowerManager()).thenReturn(mPowerManager);
when(mPowerManager.newWakeLock(eq(PowerManager.PARTIAL_WAKE_LOCK), any()))
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java
new file mode 100644
index 0000000..d1eba96
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java
@@ -0,0 +1,299 @@
+/*
+ * 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.AidlUtils.buildFsPermission;
+import static com.android.server.art.AidlUtils.buildOutputArtifacts;
+import static com.android.server.art.AidlUtils.buildPermissionSettings;
+import static com.android.server.art.OutputArtifacts.PermissionSettings;
+import static com.android.server.art.model.OptimizeResult.DexFileOptimizeResult;
+import static com.android.server.art.testing.TestingUtils.deepEq;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.isNull;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.os.Process;
+import android.os.ServiceSpecificException;
+import android.os.SystemProperties;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.art.model.OptimizeOptions;
+import com.android.server.art.model.OptimizeResult;
+import com.android.server.art.testing.OnSuccessRule;
+import com.android.server.art.testing.TestingUtils;
+
+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 java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(Parameterized.class)
+public class PrimaryDexOptimizerParameterizedTest extends PrimaryDexOptimizerTestBase {
+ @Rule
+ public OnSuccessRule onSuccessRule = new OnSuccessRule(() -> {
+ // Don't do this on failure because it will make the failure hard to understand.
+ verifyNoMoreInteractions(mArtd);
+ });
+
+ private OptimizeOptions mOptions;
+
+ @Parameter(0) public Params mParams;
+
+ @Parameters(name = "{0}")
+ public static Iterable<Params> data() {
+ List<Params> list = new ArrayList<>();
+ Params params;
+
+ // Baseline.
+ params = new Params();
+ list.add(params);
+
+ params = new Params();
+ params.mRequestedCompilerFilter = "speed";
+ params.mExpectedCompilerFilter = "speed";
+ list.add(params);
+
+ params = new Params();
+ params.mIsSystem = true;
+ params.mExpectedIsInDalvikCache = true;
+ list.add(params);
+
+ params = new Params();
+ params.mIsSystem = true;
+ params.mIsUpdatedSystemApp = true;
+ list.add(params);
+
+ params = new Params();
+ params.mIsSystem = true;
+ params.mIsUsesNonSdkApi = true;
+ params.mExpectedIsInDalvikCache = true;
+ params.mExpectedIsHiddenApiPolicyEnabled = false;
+ list.add(params);
+
+ params = new Params();
+ params.mIsUpdatedSystemApp = true;
+ params.mIsUsesNonSdkApi = true;
+ params.mExpectedIsHiddenApiPolicyEnabled = false;
+ list.add(params);
+
+ params = new Params();
+ params.mIsSignedWithPlatformKey = true;
+ params.mExpectedIsHiddenApiPolicyEnabled = false;
+ list.add(params);
+
+ params = new Params();
+ params.mIsDebuggable = true;
+ params.mRequestedCompilerFilter = "speed";
+ params.mExpectedCompilerFilter = "verify";
+ params.mExpectedIsDebuggable = true;
+ list.add(params);
+
+ params = new Params();
+ params.mIsVmSafeMode = true;
+ params.mRequestedCompilerFilter = "speed";
+ params.mExpectedCompilerFilter = "verify";
+ list.add(params);
+
+ params = new Params();
+ params.mAlwaysDebuggable = true;
+ params.mExpectedIsDebuggable = true;
+ list.add(params);
+
+ params = new Params();
+ params.mIsSystemUi = true;
+ params.mExpectedCompilerFilter = "speed";
+ list.add(params);
+
+ params = new Params();
+ params.mForce = true;
+ params.mShouldDowngrade = false;
+ params.mExpectedDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
+ | DexoptTrigger.COMPILER_FILTER_IS_SAME | DexoptTrigger.COMPILER_FILTER_IS_WORSE
+ | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+ list.add(params);
+
+ params = new Params();
+ params.mForce = true;
+ params.mShouldDowngrade = true;
+ params.mExpectedDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
+ | DexoptTrigger.COMPILER_FILTER_IS_SAME | DexoptTrigger.COMPILER_FILTER_IS_WORSE
+ | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+ list.add(params);
+
+ params = new Params();
+ params.mShouldDowngrade = true;
+ params.mExpectedDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_WORSE;
+ list.add(params);
+
+ return list;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ lenient().when(mInjector.isSystemUiPackage(any())).thenReturn(mParams.mIsSystemUi);
+
+ lenient()
+ .when(SystemProperties.getBoolean(eq("dalvik.vm.always_debuggable"), anyBoolean()))
+ .thenReturn(mParams.mAlwaysDebuggable);
+
+ lenient().when(mPkg.isVmSafeMode()).thenReturn(mParams.mIsVmSafeMode);
+ lenient().when(mPkg.isDebuggable()).thenReturn(mParams.mIsDebuggable);
+ lenient().when(mPkg.getTargetSdkVersion()).thenReturn(123);
+ lenient().when(mPkg.isSignedWithPlatformKey()).thenReturn(mParams.mIsSignedWithPlatformKey);
+ lenient().when(mPkg.isUsesNonSdkApi()).thenReturn(mParams.mIsUsesNonSdkApi);
+ lenient().when(mPkgState.isSystem()).thenReturn(mParams.mIsSystem);
+ lenient().when(mPkgState.isUpdatedSystemApp()).thenReturn(mParams.mIsUpdatedSystemApp);
+
+ mOptions = new OptimizeOptions.Builder("install")
+ .setCompilerFilter(mParams.mRequestedCompilerFilter)
+ .setPriorityClass(PriorityClass.INTERACTIVE)
+ .setForce(mParams.mForce)
+ .setShouldDowngrade(mParams.mShouldDowngrade)
+ .build();
+ }
+
+ @Test
+ public void testDexopt() throws Exception {
+ PermissionSettings permissionSettings = buildPermissionSettings(
+ buildFsPermission(Process.SYSTEM_UID, Process.SYSTEM_UID,
+ false /* isOtherReadable */, true /* isOtherExecutable */),
+ buildFsPermission(Process.SYSTEM_UID, 52345, true /* isOtherReadable */),
+ null /* seContext */);
+ DexoptOptions dexoptOptions = new DexoptOptions();
+ dexoptOptions.compilationReason = "install";
+ dexoptOptions.targetSdkVersion = 123;
+ dexoptOptions.debuggable = mParams.mExpectedIsDebuggable;
+ dexoptOptions.generateAppImage = false;
+ dexoptOptions.hiddenApiPolicyEnabled = mParams.mExpectedIsHiddenApiPolicyEnabled;
+
+ // The first one is normal.
+ doReturn(dexoptIsNeeded())
+ .when(mArtd)
+ .getDexoptNeeded("/data/app/foo/base.apk", "arm64", "PCL[]",
+ mParams.mExpectedCompilerFilter, mParams.mExpectedDexoptTrigger);
+ doReturn(true).when(mArtd).dexopt(
+ deepEq(buildOutputArtifacts("/data/app/foo/base.apk", "arm64",
+ mParams.mExpectedIsInDalvikCache, permissionSettings)),
+ eq("/data/app/foo/base.apk"), eq("arm64"), eq("PCL[]"),
+ eq(mParams.mExpectedCompilerFilter), isNull() /* profile */,
+ isNull() /* inputVdex */, eq(PriorityClass.INTERACTIVE), deepEq(dexoptOptions));
+
+ // The second one fails on `dexopt`.
+ doReturn(dexoptIsNeeded())
+ .when(mArtd)
+ .getDexoptNeeded("/data/app/foo/base.apk", "arm", "PCL[]",
+ mParams.mExpectedCompilerFilter, mParams.mExpectedDexoptTrigger);
+ doThrow(ServiceSpecificException.class)
+ .when(mArtd)
+ .dexopt(deepEq(buildOutputArtifacts("/data/app/foo/base.apk", "arm",
+ mParams.mExpectedIsInDalvikCache, permissionSettings)),
+ eq("/data/app/foo/base.apk"), eq("arm"), eq("PCL[]"),
+ eq(mParams.mExpectedCompilerFilter), isNull() /* profile */,
+ isNull() /* inputVdex */, eq(PriorityClass.INTERACTIVE),
+ deepEq(dexoptOptions));
+
+ // The third one doesn't need dexopt.
+ doReturn(dexoptIsNotNeeded())
+ .when(mArtd)
+ .getDexoptNeeded("/data/app/foo/split_0.apk", "arm64", "PCL[base.apk]",
+ mParams.mExpectedCompilerFilter, mParams.mExpectedDexoptTrigger);
+
+ // The fourth one is normal.
+ doReturn(dexoptIsNeeded())
+ .when(mArtd)
+ .getDexoptNeeded("/data/app/foo/split_0.apk", "arm", "PCL[base.apk]",
+ mParams.mExpectedCompilerFilter, mParams.mExpectedDexoptTrigger);
+ doReturn(true).when(mArtd).dexopt(
+ deepEq(buildOutputArtifacts("/data/app/foo/split_0.apk", "arm",
+ mParams.mExpectedIsInDalvikCache, permissionSettings)),
+ eq("/data/app/foo/split_0.apk"), eq("arm"), eq("PCL[base.apk]"),
+ eq(mParams.mExpectedCompilerFilter), isNull() /* profile */,
+ isNull() /* inputVdex */, eq(PriorityClass.INTERACTIVE), deepEq(dexoptOptions));
+
+ assertThat(mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptions))
+ .comparingElementsUsing(TestingUtils.<DexFileOptimizeResult>deepEquality())
+ .containsExactly(
+ new DexFileOptimizeResult("/data/app/foo/base.apk", "arm64",
+ mParams.mExpectedCompilerFilter, OptimizeResult.OPTIMIZE_PERFORMED),
+ new DexFileOptimizeResult("/data/app/foo/base.apk", "arm",
+ mParams.mExpectedCompilerFilter, OptimizeResult.OPTIMIZE_FAILED),
+ new DexFileOptimizeResult("/data/app/foo/split_0.apk", "arm64",
+ mParams.mExpectedCompilerFilter, OptimizeResult.OPTIMIZE_SKIPPED),
+ new DexFileOptimizeResult("/data/app/foo/split_0.apk", "arm",
+ mParams.mExpectedCompilerFilter,
+ OptimizeResult.OPTIMIZE_PERFORMED));
+ }
+
+ private static class Params {
+ // Package information.
+ public boolean mIsSystem = false;
+ public boolean mIsUpdatedSystemApp = false;
+ public boolean mIsSignedWithPlatformKey = false;
+ public boolean mIsUsesNonSdkApi = false;
+ public boolean mIsVmSafeMode = false;
+ public boolean mIsDebuggable = false;
+ public boolean mIsSystemUi = false;
+
+ // Options.
+ public String mRequestedCompilerFilter = "verify";
+ public boolean mForce = false;
+ public boolean mShouldDowngrade = false;
+
+ // System properties.
+ public boolean mAlwaysDebuggable = false;
+
+ // Expectations.
+ public String mExpectedCompilerFilter = "verify";
+ public int mExpectedDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
+ | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+ public boolean mExpectedIsInDalvikCache = false;
+ public boolean mExpectedIsDebuggable = false;
+ public boolean mExpectedIsHiddenApiPolicyEnabled = true;
+
+ public String toString() {
+ return String.format("isSystem=%b,isUpdatedSystemApp=%b,isSignedWithPlatformKey=%b,"
+ + "isUsesNonSdkApi=%b,isVmSafeMode=%b,isDebuggable=%b,isSystemUi=%b,"
+ + "requestedCompilerFilter=%s,force=%b,shouldDowngrade=%b,"
+ + "alwaysDebuggable=%b => targetCompilerFilter=%s,"
+ + "expectedDexoptTrigger=%d,expectedIsInDalvikCache=%b,"
+ + "expectedIsDebuggable=%b,expectedIsHiddenApiPolicyEnabled=%b",
+ mIsSystem, mIsUpdatedSystemApp, mIsSignedWithPlatformKey, mIsUsesNonSdkApi,
+ mIsVmSafeMode, mIsDebuggable, mIsSystemUi, mRequestedCompilerFilter, mForce,
+ mShouldDowngrade, mAlwaysDebuggable, mExpectedCompilerFilter,
+ mExpectedDexoptTrigger, mExpectedIsInDalvikCache, mExpectedIsDebuggable,
+ mExpectedIsHiddenApiPolicyEnabled);
+ }
+ }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
new file mode 100644
index 0000000..594d054
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.GetDexoptNeededResult.ArtifactsLocation;
+import static com.android.server.art.testing.TestingUtils.deepEq;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyByte;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.isNull;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.art.model.OptimizeOptions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PrimaryDexOptimizerTest extends PrimaryDexOptimizerTestBase {
+ private OptimizeOptions mOptions;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mOptions = new OptimizeOptions.Builder("install").setCompilerFilter("verify").build();
+ }
+
+ @Test
+ public void testDexoptInputVdex() throws Exception {
+ // null.
+ doReturn(dexoptIsNeeded(ArtifactsLocation.NONE_OR_ERROR))
+ .when(mArtd)
+ .getDexoptNeeded(eq("/data/app/foo/base.apk"), eq("arm64"), any(), any(), anyInt());
+ doReturn(true).when(mArtd).dexopt(any(), eq("/data/app/foo/base.apk"), eq("arm64"), any(),
+ any(), any(), isNull(), anyByte(), any());
+
+ // ArtifactsPath, isInDalvikCache=true.
+ doReturn(dexoptIsNeeded(ArtifactsLocation.DALVIK_CACHE))
+ .when(mArtd)
+ .getDexoptNeeded(eq("/data/app/foo/base.apk"), eq("arm"), any(), any(), anyInt());
+ doReturn(true).when(mArtd).dexopt(any(), eq("/data/app/foo/base.apk"), eq("arm"), any(),
+ any(), any(),
+ deepEq(VdexPath.artifactsPath(AidlUtils.buildArtifactsPath(
+ "/data/app/foo/base.apk", "arm", true /* isInDalvikCache */))),
+ anyByte(), any());
+
+ // ArtifactsPath, isInDalvikCache=false.
+ doReturn(dexoptIsNeeded(ArtifactsLocation.NEXT_TO_DEX))
+ .when(mArtd)
+ .getDexoptNeeded(
+ eq("/data/app/foo/split_0.apk"), eq("arm64"), any(), any(), anyInt());
+ doReturn(true).when(mArtd).dexopt(any(), eq("/data/app/foo/split_0.apk"), eq("arm64"),
+ any(), any(), any(),
+ deepEq(VdexPath.artifactsPath(AidlUtils.buildArtifactsPath(
+ "/data/app/foo/split_0.apk", "arm64", false /* isInDalvikCache */))),
+ anyByte(), any());
+
+ // DexMetadataPath.
+ doReturn(dexoptIsNeeded(ArtifactsLocation.DM))
+ .when(mArtd)
+ .getDexoptNeeded(
+ eq("/data/app/foo/split_0.apk"), eq("arm"), any(), any(), anyInt());
+ doReturn(true).when(mArtd).dexopt(any(), eq("/data/app/foo/split_0.apk"), eq("arm"), any(),
+ any(), any(),
+ deepEq(VdexPath.dexMetadataPath(
+ AidlUtils.buildDexMetadataPath("/data/app/foo/split_0.apk"))),
+ anyByte(), any());
+
+ mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptions);
+ }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
new file mode 100644
index 0000000..92b4cb2
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
@@ -0,0 +1,127 @@
+/*
+ * 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.GetDexoptNeededResult.ArtifactsLocation;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+
+import android.content.pm.ApplicationInfo;
+import android.os.SystemProperties;
+
+import com.android.server.art.testing.StaticMockitoRule;
+import com.android.server.art.wrapper.AndroidPackageApi;
+import com.android.server.art.wrapper.PackageState;
+
+import dalvik.system.PathClassLoader;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+
+public class PrimaryDexOptimizerTestBase {
+ protected static final String PKG_NAME = "com.example.foo";
+
+ @Rule public StaticMockitoRule mockitoRule = new StaticMockitoRule(SystemProperties.class);
+
+ @Mock protected PrimaryDexOptimizer.Injector mInjector;
+ @Mock protected IArtd mArtd;
+ protected PackageState mPkgState;
+ protected AndroidPackageApi mPkg;
+
+ protected PrimaryDexOptimizer mPrimaryDexOptimizer;
+
+ @Before
+ public void setUp() throws Exception {
+ lenient().when(mInjector.getArtd()).thenReturn(mArtd);
+ lenient().when(mInjector.isSystemUiPackage(any())).thenReturn(false);
+
+ lenient()
+ .when(SystemProperties.get("dalvik.vm.systemuicompilerfilter"))
+ .thenReturn("speed");
+ lenient()
+ .when(SystemProperties.getBoolean(eq("dalvik.vm.always_debuggable"), anyBoolean()))
+ .thenReturn(false);
+
+ mPkgState = createPackageState();
+ mPkg = mPkgState.getAndroidPackage();
+
+ mPrimaryDexOptimizer = new PrimaryDexOptimizer(mInjector);
+ }
+
+ private AndroidPackageApi createPackage() {
+ // This package has the base APK and one split APK that has code.
+ AndroidPackageApi pkg = mock(AndroidPackageApi.class);
+ lenient().when(pkg.getBaseApkPath()).thenReturn("/data/app/foo/base.apk");
+ lenient().when(pkg.isHasCode()).thenReturn(true);
+ lenient().when(pkg.getClassLoaderName()).thenReturn(PathClassLoader.class.getName());
+ lenient().when(pkg.getSplitNames()).thenReturn(new String[] {"split_0", "split_1"});
+ lenient()
+ .when(pkg.getSplitCodePaths())
+ .thenReturn(
+ new String[] {"/data/app/foo/split_0.apk", "/data/app/foo/split_1.apk"});
+ lenient()
+ .when(pkg.getSplitFlags())
+ .thenReturn(new int[] {ApplicationInfo.FLAG_HAS_CODE, 0});
+ lenient().when(pkg.getUid()).thenReturn(12345);
+ lenient().when(pkg.isVmSafeMode()).thenReturn(false);
+ lenient().when(pkg.isDebuggable()).thenReturn(false);
+ lenient().when(pkg.getTargetSdkVersion()).thenReturn(123);
+ lenient().when(pkg.isSignedWithPlatformKey()).thenReturn(false);
+ lenient().when(pkg.isUsesNonSdkApi()).thenReturn(false);
+ return pkg;
+ }
+
+ private PackageState createPackageState() {
+ PackageState pkgState = mock(PackageState.class);
+ lenient().when(pkgState.getPackageName()).thenReturn(PKG_NAME);
+ lenient().when(pkgState.getPrimaryCpuAbi()).thenReturn("arm64-v8a");
+ lenient().when(pkgState.getSecondaryCpuAbi()).thenReturn("armeabi-v7a");
+ lenient().when(pkgState.isSystem()).thenReturn(false);
+ lenient().when(pkgState.isUpdatedSystemApp()).thenReturn(false);
+ lenient().when(pkgState.getUsesLibraryInfos()).thenReturn(new ArrayList<>());
+ AndroidPackageApi pkg = createPackage();
+ lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
+ return pkgState;
+ }
+
+ protected GetDexoptNeededResult dexoptIsNotNeeded() {
+ var result = new GetDexoptNeededResult();
+ result.isDexoptNeeded = false;
+ return result;
+ }
+
+ protected GetDexoptNeededResult dexoptIsNeeded() {
+ return dexoptIsNeeded(ArtifactsLocation.NONE_OR_ERROR);
+ }
+
+ protected GetDexoptNeededResult dexoptIsNeeded(@ArtifactsLocation byte location) {
+ var result = new GetDexoptNeededResult();
+ result.isDexoptNeeded = true;
+ result.artifactsLocation = location;
+ if (location != ArtifactsLocation.NONE_OR_ERROR) {
+ result.isVdexUsable = true;
+ }
+ return result;
+ }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java b/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java
new file mode 100644
index 0000000..e7582b1
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java
@@ -0,0 +1,120 @@
+/*
+ * 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.testing;
+
+import static org.mockito.Mockito.argThat;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.google.common.truth.Correspondence;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+
+public final class TestingUtils {
+ private static final String TAG = "TestingUtils";
+
+ private TestingUtils() {}
+
+ /**
+ * Recursively compares two objects using reflection. Returns true if the two objects are equal.
+ * For simplicity, this method only supports types that every field is a primitive type, a
+ * string, or a supported type.
+ */
+ public static boolean deepEquals(
+ @Nullable Object a, @Nullable Object b, @NonNull StringBuilder errorMsg) {
+ try {
+ if (a == null && b == null) {
+ return true;
+ }
+ if (a == null || b == null) {
+ errorMsg.append(String.format("Nullability mismatch: %s != %s",
+ a == null ? "null" : "nonnull", b == null ? "null" : "nonnull"));
+ return false;
+ }
+ if (a.getClass() != b.getClass()) {
+ errorMsg.append(
+ String.format("Type mismatch: %s != %s", a.getClass(), b.getClass()));
+ return false;
+ }
+ if (a.getClass() == String.class) {
+ if (!a.equals(b)) {
+ errorMsg.append(String.format("%s != %s", a, b));
+ }
+ return a.equals(b);
+ }
+ if (a.getClass().isArray()) {
+ throw new UnsupportedOperationException("Array type is not supported");
+ }
+ for (Field field : a.getClass().getDeclaredFields()) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
+ field.setAccessible(true);
+ if (field.getType().isPrimitive()) {
+ if (!field.get(a).equals(field.get(b))) {
+ errorMsg.append(String.format("Field %s mismatch: %s != %s",
+ field.getName(), field.get(a), field.get(b)));
+ return false;
+ }
+ } else if (!deepEquals(field.get(a), field.get(b), errorMsg)) {
+ errorMsg.insert(0, String.format("Field %s mismatch: ", field.getName()));
+ return false;
+ }
+ }
+ return true;
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Same as above, but ignores any error message. */
+ public static boolean deepEquals(@Nullable Object a, @Nullable Object b) {
+ var errorMsgIgnored = new StringBuilder();
+ return deepEquals(a, b, errorMsgIgnored);
+ }
+
+ /**
+ * A Mockito argument matcher that uses {@link #deepEquals} to compare objects and logs any
+ * mismatch.
+ */
+ public static <T> T deepEq(@Nullable T expected) {
+ return argThat(arg -> {
+ var errorMsg = new StringBuilder();
+ boolean result = deepEquals(arg, expected, errorMsg);
+ if (!result) {
+ Log.e(TAG, errorMsg.toString());
+ }
+ return result;
+ });
+ }
+
+ /**
+ * A Truth correspondence that uses {@link #deepEquals} to compare objects and reports any
+ * mismatch.
+ */
+ public static <T> Correspondence<T, T> deepEquality() {
+ return Correspondence.<T, T>from(TestingUtils::deepEquals, "deeply equals")
+ .formattingDiffsUsing((actual, expected) -> {
+ var errorMsg = new StringBuilder();
+ deepEquals(actual, expected, errorMsg);
+ return errorMsg.toString();
+ });
+ }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/testing/TestingUtilsTest.java b/libartservice/service/javatests/com/android/server/art/testing/TestingUtilsTest.java
new file mode 100644
index 0000000..c1fe78b
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/testing/TestingUtilsTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.testing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TestingUtilsTest {
+ @Test
+ public void testDeepEquals() {
+ var a = new Foo();
+ var b = new Foo();
+ assertThat(TestingUtils.deepEquals(a, b)).isTrue();
+ }
+
+ @Test
+ public void testDeepEqualsNull() {
+ assertThat(TestingUtils.deepEquals(null, null)).isTrue();
+ }
+
+ @Test
+ public void testDeepEqualsNullabilityMismatch() {
+ var a = new Foo();
+ assertThat(TestingUtils.deepEquals(a, null)).isFalse();
+ }
+
+ @Test
+ public void testDeepEqualsTypeMismatch() {
+ var a = new Foo();
+ var b = new Bar();
+ assertThat(TestingUtils.deepEquals(a, b)).isFalse();
+ }
+
+ @Test
+ public void testDeepEqualsPrimitiveFieldMismatch() {
+ var a = new Foo();
+ var b = new Foo();
+ b.mA = 11111111;
+ assertThat(TestingUtils.deepEquals(a, b)).isFalse();
+ }
+
+ @Test
+ public void testDeepEqualsStringFieldMismatch() {
+ var a = new Foo();
+ var b = new Foo();
+ b.mB = "def";
+ assertThat(TestingUtils.deepEquals(a, b)).isFalse();
+ }
+
+ @Test
+ public void deepEqualsNestedFieldMismatch() {
+ var a = new Foo();
+ var b = new Foo();
+ b.mC.setB(11111111);
+ assertThat(TestingUtils.deepEquals(a, b)).isFalse();
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testDeepEqualsArrayNotSupported() throws Exception {
+ int[] a = new int[] {1};
+ int[] b = new int[] {2};
+ TestingUtils.deepEquals(a, b);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testDeepEqualsContainerNotSupported() throws Exception {
+ var a = new ArrayList<Integer>();
+ a.add(1);
+ var b = new ArrayList<Integer>();
+ b.add(2);
+ TestingUtils.deepEquals(a, b);
+ }
+}
+
+class Foo {
+ public int mA = 1234567;
+ public String mB = "abc";
+ public Bar mC = new Bar();
+}
+
+class Bar {
+ public static int sA = 10000000;
+ private int mB = 7654321;
+ public void setB(int b) {
+ mB = b;
+ }
+}