ART services: optimize package - Implement profile-guided compilation.

This CL contains the implementation of profile-guided compilation using
existing profiles: reference profiles, prebuilt profiles, and dm files.
Merging profiles is not in the scope of this CL.

Bug: 229268202
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: I0984e8fc0c3475d05251f9521223ac80d6b769cd
diff --git a/artd/binder/com/android/server/art/ProfilePath.aidl b/artd/binder/com/android/server/art/ProfilePath.aidl
index 3846b06..e12d94a 100644
--- a/artd/binder/com/android/server/art/ProfilePath.aidl
+++ b/artd/binder/com/android/server/art/ProfilePath.aidl
@@ -44,9 +44,14 @@
         @utf8InCpp String id;
     }
 
-    /** Represents a profile built in the system image. */
+    /**
+     * Represents a profile next to a dex file. This is usually a prebuilt profile in the system
+     * image, but it can also be a profile that package manager can potentially put along with the
+     * APK during installation. The latter one is not officially supported by package manager, but
+     * OEMs can customize package manager to support that.
+     */
     parcelable PrebuiltProfilePath {
-        /** The path to the dex file that the prebuilt profile is next to. */
+        /** The path to the dex file that the profile is next to. */
         @utf8InCpp String dexPath;
     }
 }
diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp
index 200f3c9..7753144 100644
--- a/libartservice/service/Android.bp
+++ b/libartservice/service/Android.bp
@@ -81,11 +81,17 @@
     srcs: [
         "java/**/*.java",
     ],
+    libs: [
+        "auto_value_annotations",
+    ],
     static_libs: [
         "artd-aidl-java",
         "modules-utils-shell-command-handler",
     ],
-    plugins: ["java_api_finder"],
+    plugins: [
+        "auto_value_plugin",
+        "java_api_finder",
+    ],
     jarjar_rules: "jarjar-rules.txt",
 }
 
diff --git a/libartservice/service/java/com/android/server/art/AidlUtils.java b/libartservice/service/java/com/android/server/art/AidlUtils.java
index 447c3d6..b28e1ff 100644
--- a/libartservice/service/java/com/android/server/art/AidlUtils.java
+++ b/libartservice/service/java/com/android/server/art/AidlUtils.java
@@ -18,6 +18,9 @@
 
 import static com.android.server.art.OutputArtifacts.PermissionSettings;
 import static com.android.server.art.OutputArtifacts.PermissionSettings.SeContext;
+import static com.android.server.art.ProfilePath.PrebuiltProfilePath;
+import static com.android.server.art.ProfilePath.RefProfilePath;
+import static com.android.server.art.ProfilePath.TmpRefProfilePath;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -77,4 +80,42 @@
         outputArtifacts.permissionSettings = permissionSettings;
         return outputArtifacts;
     }
+
+    @NonNull
+    public static RefProfilePath buildRefProfilePath(
+            @NonNull String packageName, @NonNull String profileName) {
+        var refProfilePath = new RefProfilePath();
+        refProfilePath.packageName = packageName;
+        refProfilePath.profileName = profileName;
+        return refProfilePath;
+    }
+
+    @NonNull
+    public static ProfilePath buildProfilePathForRef(
+            @NonNull String packageName, @NonNull String profileName) {
+        return ProfilePath.refProfilePath(buildRefProfilePath(packageName, profileName));
+    }
+
+    @NonNull
+    public static ProfilePath buildProfilePathForPrebuilt(@NonNull String dexPath) {
+        var prebuiltProfilePath = new PrebuiltProfilePath();
+        prebuiltProfilePath.dexPath = dexPath;
+        return ProfilePath.prebuiltProfilePath(prebuiltProfilePath);
+    }
+
+    @NonNull
+    public static ProfilePath buildProfilePathForDm(@NonNull String dexPath) {
+        return ProfilePath.dexMetadataPath(buildDexMetadataPath(dexPath));
+    }
+
+    @NonNull
+    public static OutputProfile buildOutputProfile(@NonNull String packageName,
+            @NonNull String profileName, int uid, int gid, boolean isPublic) {
+        var outputProfile = new OutputProfile();
+        outputProfile.profilePath = new TmpRefProfilePath();
+        outputProfile.profilePath.refProfilePath = buildRefProfilePath(packageName, profileName);
+        outputProfile.profilePath.id = ""; // Will be filled by artd.
+        outputProfile.fsPermission = buildFsPermission(uid, gid, isPublic);
+        return outputProfile;
+    }
 }
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
index 7d51df2..a63e4e6 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
@@ -20,6 +20,9 @@
 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.ProfilePath.RefProfilePath;
+import static com.android.server.art.ProfilePath.TmpRefProfilePath;
+import static com.android.server.art.model.ArtFlags.OptimizeFlags;
 import static com.android.server.art.model.OptimizeResult.DexFileOptimizeResult;
 
 import android.R;
@@ -31,6 +34,7 @@
 import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -40,6 +44,8 @@
 import com.android.server.art.wrapper.AndroidPackageApi;
 import com.android.server.art.wrapper.PackageState;
 
+import com.google.auto.value.AutoValue;
+
 import dalvik.system.DexFile;
 
 import java.util.ArrayList;
@@ -69,6 +75,18 @@
             @NonNull AndroidPackageApi pkg, @NonNull OptimizeParams params) throws RemoteException {
         List<DexFileOptimizeResult> results = new ArrayList<>();
 
+        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));
+        }
+
         String targetCompilerFilter =
                 adjustCompilerFilter(pkgState, pkg, params.getCompilerFilter(), params.getReason());
         if (targetCompilerFilter.equals(OptimizeParams.COMPILER_FILTER_NOOP)) {
@@ -78,6 +96,8 @@
         boolean isInDalvikCache = Utils.isInDalvikCache(pkgState);
 
         for (DetailedPrimaryDexInfo dexInfo : PrimaryDexUtils.getDetailedDexInfo(pkgState, pkg)) {
+            OutputProfile profile = null;
+            boolean succeeded = true;
             try {
                 if (!dexInfo.hasCode()) {
                     continue;
@@ -87,32 +107,69 @@
 
                 String compilerFilter = targetCompilerFilter;
 
+                boolean needsToBeShared = isSharedLibrary(pkg)
+                        || mInjector.isUsedByOtherApps(pkgState.getPackageName());
+                // If true, implies that the profile has changed since the last compilation.
+                boolean profileMerged = false;
                 if (DexFile.isProfileGuidedCompilerFilter(compilerFilter)) {
-                    throw new UnsupportedOperationException(
-                            "Profile-guided compilation is not implemented");
+                    if (needsToBeShared) {
+                        profile = initReferenceProfile(pkgState, dexInfo, uid, sharedGid);
+                    } else {
+                        profile = copyOrInitReferenceProfile(pkgState, dexInfo, uid, sharedGid);
+                        // TODO(jiakaiz): Merge profiles.
+                    }
+                    if (profile == null) {
+                        // A profile guided optimization with no profile is essentially 'verify',
+                        // and dex2oat already makes this transformation. However, we need to
+                        // explicitly make this transformation here to guide the later decisions
+                        // such as whether the artifacts can be public and whether dexopt is needed.
+                        compilerFilter = needsToBeShared
+                                ? ReasonMapping.getCompilerFilterForShared()
+                                : "verify";
+                    }
                 }
-                PermissionSettings permissionSettings =
-                        getPermissionSettings(pkgState, pkg, true /* canBePublic */);
+                boolean isProfileGuidedCompilerFilter =
+                        DexFile.isProfileGuidedCompilerFilter(compilerFilter);
+                assert isProfileGuidedCompilerFilter == (profile != null);
 
-                DexoptOptions dexoptOptions = getDexoptOptions(pkgState, pkg, params);
+                boolean canBePublic =
+                        !isProfileGuidedCompilerFilter || profile.fsPermission.isOtherReadable;
+                assert Utils.implies(needsToBeShared, canBePublic);
+                PermissionSettings permissionSettings =
+                        getPermissionSettings(sharedGid, canBePublic);
+
+                DexoptOptions dexoptOptions =
+                        getDexoptOptions(pkgState, pkg, params, isProfileGuidedCompilerFilter);
 
                 for (String isa : Utils.getAllIsas(pkgState)) {
                     @OptimizeResult.OptimizeStatus int status = OptimizeResult.OPTIMIZE_SKIPPED;
                     try {
+                        DexoptTarget target = DexoptTarget.builder()
+                                                      .setDexInfo(dexInfo)
+                                                      .setIsa(isa)
+                                                      .setIsInDalvikCache(isInDalvikCache)
+                                                      .setCompilerFilter(compilerFilter)
+                                                      .build();
+                        GetDexoptNeededOptions options =
+                                GetDexoptNeededOptions.builder()
+                                        .setProfileMerged(profileMerged)
+                                        .setFlags(params.getFlags())
+                                        .setNeedsToBePublic(needsToBeShared)
+                                        .build();
+
                         GetDexoptNeededResult getDexoptNeededResult =
-                                getDexoptNeeded(dexInfo, isa, compilerFilter,
-                                        (params.getFlags() & ArtFlags.FLAG_SHOULD_DOWNGRADE) != 0,
-                                        (params.getFlags() & ArtFlags.FLAG_FORCE) != 0);
+                                getDexoptNeeded(target, options);
 
                         if (!getDexoptNeededResult.isDexoptNeeded) {
                             continue;
                         }
 
-                        ProfilePath inputProfile = null;
+                        ProfilePath inputProfile = profile != null
+                                ? ProfilePath.tmpRefProfilePath(profile.profilePath)
+                                : null;
 
-                        status = dexoptFile(dexInfo, isa, isInDalvikCache, compilerFilter,
-                                inputProfile, getDexoptNeededResult, permissionSettings,
-                                params.getPriorityClass(), dexoptOptions);
+                        status = dexoptFile(target, inputProfile, getDexoptNeededResult,
+                                permissionSettings, params.getPriorityClass(), dexoptOptions);
                     } catch (ServiceSpecificException e) {
                         // Log the error and continue.
                         Log.e(TAG,
@@ -125,10 +182,25 @@
                     } finally {
                         results.add(new DexFileOptimizeResult(
                                 dexInfo.dexPath(), isa, compilerFilter, status));
+                        if (status != OptimizeResult.OPTIMIZE_SKIPPED
+                                && status != OptimizeResult.OPTIMIZE_PERFORMED) {
+                            succeeded = false;
+                        }
                     }
                 }
+
+                if (profile != null && succeeded) {
+                    // Commit the profile only if dexopt succeeds.
+                    if (commitProfileChanges(profile)) {
+                        profile = null;
+                    }
+                    // TODO(jiakaiz): If profileMerged is true, clear current profiles.
+                }
             } finally {
-                // TODO(jiakaiz): Cleanup profile.
+                if (profile != null) {
+                    mInjector.getArtd().deleteProfile(
+                            ProfilePath.tmpRefProfilePath(profile.profilePath));
+                }
             }
         }
 
@@ -170,21 +242,96 @@
         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));
+    boolean isSharedLibrary(@NonNull AndroidPackageApi pkg) {
+        // TODO(b/242688548): Package manager should provide a better API for this.
+        return !TextUtils.isEmpty(pkg.getSdkLibName())
+                || !TextUtils.isEmpty(pkg.getStaticSharedLibName())
+                || !pkg.getLibraryNames().isEmpty();
+    }
+
+    /**
+     * Returns a reference profile initialized from a prebuilt profile or a DM profile if exists, or
+     * null otherwise.
+     */
+    @Nullable
+    private OutputProfile initReferenceProfile(@NonNull PackageState pkgState,
+            @NonNull DetailedPrimaryDexInfo dexInfo, int uid, int gid) throws RemoteException {
+        String profileName = getProfileName(dexInfo.splitName());
+        OutputProfile output = AidlUtils.buildOutputProfile(
+                pkgState.getPackageName(), profileName, uid, gid, true /* isPublic */);
+
+        ProfilePath prebuiltProfile = AidlUtils.buildProfilePathForPrebuilt(dexInfo.dexPath());
+        try {
+            // If the APK is really a prebuilt one, rewriting the profile is unnecessary because the
+            // dex location is known at build time and is correctly set in the profile header.
+            // However, the APK can also be an installed one, in which case partners may place a
+            // profile file next to the APK at install time. Rewriting the profile in the latter
+            // case is necessary.
+            if (mInjector.getArtd().copyAndRewriteProfile(
+                        prebuiltProfile, output, dexInfo.dexPath())) {
+                return output;
+            }
+        } catch (ServiceSpecificException e) {
+            Log.e(TAG,
+                    String.format(
+                            "Failed to use prebuilt profile [packageName = %s, profileName = %s]",
+                            pkgState.getPackageName(), profileName),
+                    e);
         }
 
+        ProfilePath dmProfile = AidlUtils.buildProfilePathForDm(dexInfo.dexPath());
+        try {
+            if (mInjector.getArtd().copyAndRewriteProfile(dmProfile, output, dexInfo.dexPath())) {
+                return output;
+            }
+        } catch (ServiceSpecificException e) {
+            Log.e(TAG,
+                    String.format("Failed to use profile in dex metadata file "
+                                    + "[packageName = %s, profileName = %s]",
+                            pkgState.getPackageName(), profileName),
+                    e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Copies the existing reference profile if exists, or initializes a reference profile
+     * otherwise.
+     */
+    @Nullable
+    private OutputProfile copyOrInitReferenceProfile(@NonNull PackageState pkgState,
+            @NonNull DetailedPrimaryDexInfo dexInfo, int uid, int gid) throws RemoteException {
+        String profileName = getProfileName(dexInfo.splitName());
+        ProfilePath refProfile =
+                AidlUtils.buildProfilePathForRef(pkgState.getPackageName(), profileName);
+        try {
+            if (mInjector.getArtd().isProfileUsable(refProfile, dexInfo.dexPath())) {
+                boolean isOtherReadable = mInjector.getArtd().getProfileVisibility(refProfile)
+                        == FileVisibility.OTHER_READABLE;
+                OutputProfile output = AidlUtils.buildOutputProfile(pkgState.getPackageName(),
+                        getProfileName(dexInfo.splitName()), uid, gid, isOtherReadable);
+                mInjector.getArtd().copyProfile(refProfile, output);
+                return output;
+            }
+        } catch (ServiceSpecificException e) {
+            Log.e(TAG,
+                    String.format("Failed to use the existing reference profile "
+                                    + "[packageName = %s, profileName = %s]",
+                            pkgState.getPackageName(), profileName),
+                    e);
+        }
+
+        return initReferenceProfile(pkgState, dexInfo, uid, gid);
+    }
+
+    @NonNull
+    public String getProfileName(@Nullable String splitName) {
+        return splitName == null ? "primary" : splitName + ".split";
+    }
+
+    @NonNull
+    PermissionSettings getPermissionSettings(int sharedGid, boolean canBePublic) {
         // 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
@@ -200,12 +347,18 @@
 
     @NonNull
     private DexoptOptions getDexoptOptions(@NonNull PackageState pkgState,
-            @NonNull AndroidPackageApi pkg, @NonNull OptimizeParams params) {
+            @NonNull AndroidPackageApi pkg, @NonNull OptimizeParams params,
+            boolean isProfileGuidedFilter) {
         DexoptOptions dexoptOptions = new DexoptOptions();
         dexoptOptions.compilationReason = params.getReason();
         dexoptOptions.targetSdkVersion = pkg.getTargetSdkVersion();
         dexoptOptions.debuggable = pkg.isDebuggable() || isAlwaysDebuggable();
-        dexoptOptions.generateAppImage = false;
+        // Generating a meaningful app image needs a profile to determine what to include in the
+        // image. Otherwise, the app image will be nearly empty.
+        // Additionally, disable app images if the app requests for the splits to be loaded in
+        // isolation because app images are unsupported for multiple class loaders (b/72696798).
+        dexoptOptions.generateAppImage = isProfileGuidedFilter
+                && !PrimaryDexUtils.isIsolatedSplitLoading(pkg) && isAppImageEnabled();
         dexoptOptions.hiddenApiPolicyEnabled = isHiddenApiPolicyEnabled(pkgState, pkg);
         return dexoptOptions;
     }
@@ -231,47 +384,66 @@
     }
 
     @NonNull
-    GetDexoptNeededResult getDexoptNeeded(@NonNull DetailedPrimaryDexInfo dexInfo,
-            @NonNull String isa, @NonNull String compilerFilter, boolean shouldDowngrade,
-            boolean force) throws RemoteException {
-        int dexoptTrigger = getDexoptTrigger(shouldDowngrade, force);
+    GetDexoptNeededResult getDexoptNeeded(@NonNull DexoptTarget target,
+            @NonNull GetDexoptNeededOptions options) throws RemoteException {
+        int dexoptTrigger = getDexoptTrigger(target, options);
 
         // 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);
+        GetDexoptNeededResult result = mInjector.getArtd().getDexoptNeeded(
+                target.dexInfo().dexPath(), target.isa(), target.dexInfo().classLoaderContext(),
+                target.compilerFilter(), dexoptTrigger);
 
         return result;
     }
 
-    int getDexoptTrigger(boolean shouldDowngrade, boolean force) {
-        if (force) {
+    int getDexoptTrigger(@NonNull DexoptTarget target, @NonNull GetDexoptNeededOptions options)
+            throws RemoteException {
+        if ((options.flags() & ArtFlags.FLAG_FORCE) != 0) {
             return DexoptTrigger.COMPILER_FILTER_IS_BETTER | DexoptTrigger.COMPILER_FILTER_IS_SAME
                     | DexoptTrigger.COMPILER_FILTER_IS_WORSE
                     | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
         }
 
-        if (shouldDowngrade) {
+        if ((options.flags() & ArtFlags.FLAG_SHOULD_DOWNGRADE) != 0) {
             return DexoptTrigger.COMPILER_FILTER_IS_WORSE;
         }
 
-        return DexoptTrigger.COMPILER_FILTER_IS_BETTER
+        int dexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
                 | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+        if (options.profileMerged()) {
+            dexoptTrigger |= DexoptTrigger.COMPILER_FILTER_IS_SAME;
+        }
+
+        ArtifactsPath existingArtifactsPath = AidlUtils.buildArtifactsPath(
+                target.dexInfo().dexPath(), target.isa(), target.isInDalvikCache());
+
+        if (options.needsToBePublic()
+                && mInjector.getArtd().getArtifactsVisibility(existingArtifactsPath)
+                        == FileVisibility.NOT_OTHER_READABLE) {
+            // Typically, this happens after an app starts being used by other apps.
+            // This case should be the same as force as we have no choice but to trigger a new
+            // dexopt.
+            dexoptTrigger |=
+                    DexoptTrigger.COMPILER_FILTER_IS_SAME | DexoptTrigger.COMPILER_FILTER_IS_WORSE;
+        }
+
+        return dexoptTrigger;
     }
 
-    private @OptimizeResult.OptimizeStatus int dexoptFile(@NonNull DetailedPrimaryDexInfo dexInfo,
-            @NonNull String isa, boolean isInDalvikCache, @NonNull String compilerFilter,
+    private @OptimizeResult.OptimizeStatus int dexoptFile(@NonNull DexoptTarget target,
             @Nullable ProfilePath profile, @NonNull GetDexoptNeededResult getDexoptNeededResult,
             @NonNull PermissionSettings permissionSettings, @PriorityClass int priorityClass,
             @NonNull DexoptOptions dexoptOptions) throws RemoteException {
-        OutputArtifacts outputArtifacts = AidlUtils.buildOutputArtifacts(
-                dexInfo.dexPath(), isa, isInDalvikCache, permissionSettings);
+        OutputArtifacts outputArtifacts = AidlUtils.buildOutputArtifacts(target.dexInfo().dexPath(),
+                target.isa(), target.isInDalvikCache(), permissionSettings);
 
-        VdexPath inputVdex = getInputVdex(getDexoptNeededResult, dexInfo.dexPath(), isa);
+        VdexPath inputVdex =
+                getInputVdex(getDexoptNeededResult, target.dexInfo().dexPath(), target.isa());
 
-        if (!mInjector.getArtd().dexopt(outputArtifacts, dexInfo.dexPath(), isa,
-                    dexInfo.classLoaderContext(), compilerFilter, profile, inputVdex, priorityClass,
-                    dexoptOptions)) {
+        if (!mInjector.getArtd().dexopt(outputArtifacts, target.dexInfo().dexPath(), target.isa(),
+                    target.dexInfo().classLoaderContext(), target.compilerFilter(), profile,
+                    inputVdex, priorityClass, dexoptOptions)) {
             return OptimizeResult.OPTIMIZE_CANCELLED;
         }
 
@@ -300,6 +472,61 @@
         }
     }
 
+    boolean commitProfileChanges(@NonNull OutputProfile profile) throws RemoteException {
+        try {
+            mInjector.getArtd().commitTmpProfile(profile.profilePath);
+            return true;
+        } catch (ServiceSpecificException e) {
+            RefProfilePath refProfilePath = profile.profilePath.refProfilePath;
+            Log.e(TAG,
+                    String.format(
+                            "Failed to commit profile changes [packageName = %s, profileName = %s]",
+                            refProfilePath.packageName, refProfilePath.profileName),
+                    e);
+            return false;
+        }
+    }
+
+    @AutoValue
+    abstract static class DexoptTarget {
+        abstract @NonNull DetailedPrimaryDexInfo dexInfo();
+        abstract @NonNull String isa();
+        abstract boolean isInDalvikCache();
+        abstract @NonNull String compilerFilter();
+
+        static Builder builder() {
+            return new AutoValue_PrimaryDexOptimizer_DexoptTarget.Builder();
+        }
+
+        @AutoValue.Builder
+        abstract static class Builder {
+            abstract Builder setDexInfo(@NonNull DetailedPrimaryDexInfo value);
+            abstract Builder setIsa(@NonNull String value);
+            abstract Builder setIsInDalvikCache(boolean value);
+            abstract Builder setCompilerFilter(@NonNull String value);
+            abstract DexoptTarget build();
+        }
+    }
+
+    @AutoValue
+    abstract static class GetDexoptNeededOptions {
+        abstract @OptimizeFlags int flags();
+        abstract boolean profileMerged();
+        abstract boolean needsToBePublic();
+
+        static Builder builder() {
+            return new AutoValue_PrimaryDexOptimizer_GetDexoptNeededOptions.Builder();
+        }
+
+        @AutoValue.Builder
+        abstract static class Builder {
+            abstract Builder setFlags(@OptimizeFlags int value);
+            abstract Builder setProfileMerged(boolean value);
+            abstract Builder setNeedsToBePublic(boolean value);
+            abstract GetDexoptNeededOptions build();
+        }
+    }
+
     /**
      * Injector pattern for testing purpose.
      *
@@ -317,6 +544,11 @@
             return packageName.equals(mContext.getString(R.string.config_systemUi));
         }
 
+        boolean isUsedByOtherApps(@NonNull String packageName) {
+            // TODO(jiakaiz): Get the real value.
+            return false;
+        }
+
         @NonNull
         public IArtd getArtd() {
             return Utils.getArtd();
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
index d6b8a59..81ec165 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
@@ -107,8 +107,7 @@
 
         String[] splitClassLoaderNames = pkg.getSplitClassLoaderNames();
         SparseArray<int[]> splitDependencies = pkg.getSplitDependencies();
-        boolean isIsolatedSplitLoading =
-                pkg.isIsolatedSplitLoading() && !Utils.isEmpty(splitDependencies);
+        boolean isIsolatedSplitLoading = isIsolatedSplitLoading(pkg);
 
         for (int i = 1; i < dexInfos.size(); i++) {
             assert dexInfos.get(i).mSplitIndex == i - 1;
@@ -277,6 +276,10 @@
                 .collect(Collectors.joining("#", "{", "}"));
     }
 
+    public static boolean isIsolatedSplitLoading(@NonNull AndroidPackageApi pkg) {
+        return pkg.isIsolatedSplitLoading() && !Utils.isEmpty(pkg.getSplitDependencies());
+    }
+
     /** Basic information about a primary dex file (either the base APK or a split APK). */
     @Immutable
     public static class PrimaryDexInfo {
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index 9e44760..8c1cfbe 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -91,4 +91,8 @@
         }
         return artd;
     }
+
+    public static boolean implies(boolean cond1, boolean cond2) {
+        return cond1 ? cond2 : true;
+    }
 }
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 a6431b0..fac534a 100644
--- a/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java
+++ b/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java
@@ -20,6 +20,8 @@
 import android.annotation.Nullable;
 import android.util.SparseArray;
 
+import java.util.List;
+
 /** @hide */
 public class AndroidPackageApi {
     private final Object mPkg;
@@ -160,4 +162,31 @@
             throw new RuntimeException(e);
         }
     }
+
+    @Nullable
+    public String getSdkLibName() {
+        try {
+            return (String) mPkg.getClass().getMethod("getSdkLibName").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public String getStaticSharedLibName() {
+        try {
+            return (String) mPkg.getClass().getMethod("getStaticSharedLibName").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public List<String> getLibraryNames() {
+        try {
+            return (List<String>) mPkg.getClass().getMethod("getLibraryNames").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java
index c7c779e..43aac8d 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java
@@ -192,7 +192,7 @@
         PermissionSettings permissionSettings = buildPermissionSettings(
                 buildFsPermission(Process.SYSTEM_UID, Process.SYSTEM_UID,
                         false /* isOtherReadable */, true /* isOtherExecutable */),
-                buildFsPermission(Process.SYSTEM_UID, 52345, true /* isOtherReadable */),
+                buildFsPermission(Process.SYSTEM_UID, SHARED_GID, true /* isOtherReadable */),
                 null /* seContext */);
         DexoptOptions dexoptOptions = new DexoptOptions();
         dexoptOptions.compilationReason = "install";
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
index 8db3032..03151e7 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
@@ -19,31 +19,83 @@
 import static com.android.server.art.GetDexoptNeededResult.ArtifactsLocation;
 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.anyInt;
+import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.isNull;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.ServiceSpecificException;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.art.model.OptimizeParams;
+import com.android.server.art.testing.TestingUtils;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+
+import java.util.ArrayList;
+import java.util.List;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class PrimaryDexOptimizerTest extends PrimaryDexOptimizerTestBase {
-    private OptimizeParams mOptimizeParams;
+    private final OptimizeParams mOptimizeParams =
+            new OptimizeParams.Builder("install").setCompilerFilter("speed-profile").build();
+
+    private final String mDexPath = "/data/app/foo/base.apk";
+    private final ProfilePath mRefProfile = AidlUtils.buildProfilePathForRef(PKG_NAME, "primary");
+    private final ProfilePath mPrebuiltProfile = AidlUtils.buildProfilePathForPrebuilt(mDexPath);
+    private final ProfilePath mDmProfile = AidlUtils.buildProfilePathForDm(mDexPath);
+    private final OutputProfile mPublicOutputProfile = AidlUtils.buildOutputProfile(
+            PKG_NAME, "primary", UID, SHARED_GID, true /* isOtherReadable */);
+    private final OutputProfile mPrivateOutputProfile = AidlUtils.buildOutputProfile(
+            PKG_NAME, "primary", UID, SHARED_GID, false /* isOtherReadable */);
+
+    private final String mSplit0DexPath = "/data/app/foo/split_0.apk";
+    private final ProfilePath mSplit0RefProfile =
+            AidlUtils.buildProfilePathForRef(PKG_NAME, "split_0.split");
+    private final OutputProfile mSplit0PrivateOutputProfile = AidlUtils.buildOutputProfile(
+            PKG_NAME, "split_0.split", UID, SHARED_GID, false /* isOtherReadable */);
+
+    private final int mDefaultDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
+            | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+    private final int mForceDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
+            | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE
+            | DexoptTrigger.COMPILER_FILTER_IS_SAME | DexoptTrigger.COMPILER_FILTER_IS_WORSE;
+
+    private List<ProfilePath> mUsedProfiles;
 
     @Before
     public void setUp() throws Exception {
         super.setUp();
 
-        mOptimizeParams = new OptimizeParams.Builder("install").setCompilerFilter("verify").build();
+        // By default, none of the profiles are usable.
+        lenient().when(mArtd.isProfileUsable(any(), any())).thenReturn(false);
+        lenient().when(mArtd.copyAndRewriteProfile(any(), any(), any())).thenReturn(false);
+
+        // Dexopt is by default needed and successful.
+        lenient()
+                .when(mArtd.getDexoptNeeded(any(), any(), any(), any(), anyInt()))
+                .thenReturn(dexoptIsNeeded());
+        lenient()
+                .when(mArtd.dexopt(
+                        any(), any(), any(), any(), any(), any(), any(), anyInt(), any()))
+                .thenReturn(true);
+
+        mUsedProfiles = new ArrayList<>();
     }
 
     @Test
@@ -51,42 +103,284 @@
         // 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(), anyInt(), any());
+                .getDexoptNeeded(eq(mDexPath), eq("arm64"), any(), any(), anyInt());
+        doReturn(true).when(mArtd).dexopt(
+                any(), eq(mDexPath), eq("arm64"), any(), any(), any(), isNull(), anyInt(), 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 */))),
+                .getDexoptNeeded(eq(mDexPath), eq("arm"), any(), any(), anyInt());
+        doReturn(true).when(mArtd).dexopt(any(), eq(mDexPath), eq("arm"), any(), any(), any(),
+                deepEq(VdexPath.artifactsPath(
+                        AidlUtils.buildArtifactsPath(mDexPath, "arm", true /* isInDalvikCache */))),
                 anyInt(), 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(),
+                .getDexoptNeeded(eq(mSplit0DexPath), eq("arm64"), any(), any(), anyInt());
+        doReturn(true).when(mArtd).dexopt(any(), eq(mSplit0DexPath), eq("arm64"), any(), any(),
+                any(),
                 deepEq(VdexPath.artifactsPath(AidlUtils.buildArtifactsPath(
-                        "/data/app/foo/split_0.apk", "arm64", false /* isInDalvikCache */))),
+                        mSplit0DexPath, "arm64", false /* isInDalvikCache */))),
                 anyInt(), 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"))),
+                .getDexoptNeeded(eq(mSplit0DexPath), eq("arm"), any(), any(), anyInt());
+        doReturn(true).when(mArtd).dexopt(any(), eq(mSplit0DexPath), eq("arm"), any(), any(), any(),
+                deepEq(VdexPath.dexMetadataPath(AidlUtils.buildDexMetadataPath(mSplit0DexPath))),
                 anyInt(), any());
 
         mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams);
     }
+
+    @Test
+    public void testDexoptUsesRefProfile() throws Exception {
+        makeProfileUsable(mRefProfile);
+        when(mArtd.getProfileVisibility(deepEq(mRefProfile)))
+                .thenReturn(FileVisibility.NOT_OTHER_READABLE);
+
+        // Other profiles are also usable, but they shouldn't be used.
+        makeProfileUsable(mPrebuiltProfile);
+        makeProfileUsable(mDmProfile);
+
+        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams);
+
+        InOrder inOrder = inOrder(mArtd);
+
+        inOrder.verify(mArtd).copyProfile(deepEq(mRefProfile), deepEq(mPrivateOutputProfile));
+
+        inOrder.verify(mArtd).getDexoptNeeded(
+                eq(mDexPath), eq("arm64"), any(), eq("speed-profile"), eq(mDefaultDexoptTrigger));
+        checkDexoptWithPrivateProfile(
+                inOrder.verify(mArtd), mDexPath, "arm64", mPrivateOutputProfile);
+
+        inOrder.verify(mArtd).getDexoptNeeded(
+                eq(mDexPath), eq("arm"), any(), eq("speed-profile"), eq(mDefaultDexoptTrigger));
+        checkDexoptWithPrivateProfile(
+                inOrder.verify(mArtd), mDexPath, "arm", mPrivateOutputProfile);
+
+        inOrder.verify(mArtd).commitTmpProfile(deepEq(mPrivateOutputProfile.profilePath));
+
+        // There is no profile for split 0, so it should fall back to "verify".
+        inOrder.verify(mArtd).getDexoptNeeded(
+                eq(mSplit0DexPath), eq("arm64"), any(), eq("verify"), eq(mDefaultDexoptTrigger));
+        checkDexoptWithNoProfile(inOrder.verify(mArtd), mSplit0DexPath, "arm64", "verify");
+
+        inOrder.verify(mArtd).getDexoptNeeded(
+                eq(mSplit0DexPath), eq("arm"), any(), eq("verify"), eq(mDefaultDexoptTrigger));
+        checkDexoptWithNoProfile(inOrder.verify(mArtd), mSplit0DexPath, "arm", "verify");
+
+        verifyProfileNotUsed(mPrebuiltProfile);
+        verifyProfileNotUsed(mDmProfile);
+    }
+
+    @Test
+    public void testDexoptUsesPublicRefProfile() throws Exception {
+        // The ref profile is usable and public.
+        makeProfileUsable(mRefProfile);
+        when(mArtd.getProfileVisibility(deepEq(mRefProfile)))
+                .thenReturn(FileVisibility.OTHER_READABLE);
+
+        // Other profiles are also usable, but they shouldn't be used.
+        makeProfileUsable(mPrebuiltProfile);
+        makeProfileUsable(mDmProfile);
+
+        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams);
+
+        verify(mArtd).copyProfile(deepEq(mRefProfile), deepEq(mPublicOutputProfile));
+
+        checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm64", mPublicOutputProfile);
+        checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm", mPublicOutputProfile);
+
+        verifyProfileNotUsed(mPrebuiltProfile);
+        verifyProfileNotUsed(mDmProfile);
+    }
+
+    @Test
+    public void testDexoptUsesPrebuiltProfile() throws Exception {
+        makeProfileNotUsable(mRefProfile);
+        makeProfileUsable(mPrebuiltProfile);
+        makeProfileUsable(mDmProfile);
+
+        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams);
+
+        verify(mArtd).copyAndRewriteProfile(
+                deepEq(mPrebuiltProfile), deepEq(mPublicOutputProfile), eq(mDexPath));
+
+        checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm64", mPublicOutputProfile);
+        checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm", mPublicOutputProfile);
+
+        verifyProfileNotUsed(mRefProfile);
+        verifyProfileNotUsed(mDmProfile);
+    }
+
+    @Test
+    public void testDexoptUsesDmProfile() throws Exception {
+        makeProfileNotUsable(mRefProfile);
+        makeProfileNotUsable(mPrebuiltProfile);
+        makeProfileUsable(mDmProfile);
+
+        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams);
+
+        verify(mArtd).copyAndRewriteProfile(
+                deepEq(mDmProfile), deepEq(mPublicOutputProfile), eq(mDexPath));
+
+        checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm64", mPublicOutputProfile);
+        checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm", mPublicOutputProfile);
+
+        verifyProfileNotUsed(mRefProfile);
+        verifyProfileNotUsed(mPrebuiltProfile);
+    }
+
+    @Test
+    public void testDexoptDeletesProfileOnFailure() throws Exception {
+        makeProfileUsable(mRefProfile);
+        when(mArtd.getProfileVisibility(deepEq(mRefProfile)))
+                .thenReturn(FileVisibility.NOT_OTHER_READABLE);
+
+        when(mArtd.dexopt(any(), eq(mDexPath), any(), any(), any(), any(), any(), anyInt(), any()))
+                .thenThrow(ServiceSpecificException.class);
+
+        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams);
+
+        verify(mArtd).deleteProfile(
+                deepEq(ProfilePath.tmpRefProfilePath(mPrivateOutputProfile.profilePath)));
+        verify(mArtd, never()).commitTmpProfile(deepEq(mPrivateOutputProfile.profilePath));
+    }
+
+    @Test
+    public void testDexoptNeedsToBeShared() throws Exception {
+        when(mInjector.isUsedByOtherApps(PKG_NAME)).thenReturn(true);
+
+        // The ref profile is usable but shouldn't be used.
+        makeProfileUsable(mRefProfile);
+
+        makeProfileNotUsable(mPrebuiltProfile);
+        makeProfileUsable(mDmProfile);
+
+        // The existing artifacts are private.
+        when(mArtd.getArtifactsVisibility(
+                     argThat(artifactsPath -> artifactsPath.dexPath == mDexPath)))
+                .thenReturn(FileVisibility.NOT_OTHER_READABLE);
+
+        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams);
+
+        verify(mArtd).copyAndRewriteProfile(
+                deepEq(mDmProfile), deepEq(mPublicOutputProfile), eq(mDexPath));
+
+        // It should re-compile anyway.
+        verify(mArtd).getDexoptNeeded(
+                eq(mDexPath), eq("arm64"), any(), eq("speed-profile"), eq(mForceDexoptTrigger));
+        checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm64", mPublicOutputProfile);
+
+        verify(mArtd).getDexoptNeeded(
+                eq(mDexPath), eq("arm"), any(), eq("speed-profile"), eq(mForceDexoptTrigger));
+        checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm", mPublicOutputProfile);
+
+        checkDexoptWithNoProfile(verify(mArtd), mSplit0DexPath, "arm64", "speed");
+        checkDexoptWithNoProfile(verify(mArtd), mSplit0DexPath, "arm", "speed");
+
+        verifyProfileNotUsed(mRefProfile);
+        verifyProfileNotUsed(mPrebuiltProfile);
+    }
+
+    @Test
+    public void testDexoptNeedsToBeSharedArtifactsArePublic() throws Exception {
+        // Same setup as above, but the existing artifacts are public.
+        when(mInjector.isUsedByOtherApps(PKG_NAME)).thenReturn(true);
+        makeProfileUsable(mRefProfile);
+        makeProfileNotUsable(mPrebuiltProfile);
+        makeProfileUsable(mDmProfile);
+        when(mArtd.getArtifactsVisibility(
+                     argThat(artifactsPath -> artifactsPath.dexPath == mDexPath)))
+                .thenReturn(FileVisibility.OTHER_READABLE);
+
+        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams);
+
+        // It should use the default dexopt trigger.
+        verify(mArtd).getDexoptNeeded(
+                eq(mDexPath), eq("arm64"), any(), eq("speed-profile"), eq(mDefaultDexoptTrigger));
+        verify(mArtd).getDexoptNeeded(
+                eq(mDexPath), eq("arm"), any(), eq("speed-profile"), eq(mDefaultDexoptTrigger));
+    }
+
+    @Test
+    public void testDexoptUsesProfileForSplit() throws Exception {
+        makeProfileUsable(mSplit0RefProfile);
+        when(mArtd.getProfileVisibility(deepEq(mSplit0RefProfile)))
+                .thenReturn(FileVisibility.NOT_OTHER_READABLE);
+
+        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams);
+
+        verify(mArtd).copyProfile(deepEq(mSplit0RefProfile), deepEq(mSplit0PrivateOutputProfile));
+
+        verify(mArtd).getDexoptNeeded(eq(mSplit0DexPath), eq("arm64"), any(), eq("speed-profile"),
+                eq(mDefaultDexoptTrigger));
+        checkDexoptWithPrivateProfile(
+                verify(mArtd), mSplit0DexPath, "arm64", mSplit0PrivateOutputProfile);
+
+        verify(mArtd).getDexoptNeeded(eq(mSplit0DexPath), eq("arm"), any(), eq("speed-profile"),
+                eq(mDefaultDexoptTrigger));
+        checkDexoptWithPrivateProfile(
+                verify(mArtd), mSplit0DexPath, "arm", mSplit0PrivateOutputProfile);
+    }
+
+    private void checkDexoptWithPublicProfile(
+            IArtd artd, String dexPath, String isa, OutputProfile profile) throws Exception {
+        artd.dexopt(
+                argThat(artifacts
+                        -> artifacts.permissionSettings.fileFsPermission.isOtherReadable == true),
+                eq(dexPath), eq(isa), any(), eq("speed-profile"),
+                deepEq(ProfilePath.tmpRefProfilePath(profile.profilePath)), any(), anyInt(),
+                argThat(dexoptOptions -> dexoptOptions.generateAppImage == true));
+    }
+
+    private void checkDexoptWithPrivateProfile(
+            IArtd artd, String dexPath, String isa, OutputProfile profile) throws Exception {
+        artd.dexopt(
+                argThat(artifacts
+                        -> artifacts.permissionSettings.fileFsPermission.isOtherReadable == false),
+                eq(dexPath), eq(isa), any(), eq("speed-profile"),
+                deepEq(ProfilePath.tmpRefProfilePath(profile.profilePath)), any(), anyInt(),
+                argThat(dexoptOptions -> dexoptOptions.generateAppImage == true));
+    }
+
+    private void checkDexoptWithNoProfile(
+            IArtd artd, String dexPath, String isa, String compilerFilter) throws Exception {
+        artd.dexopt(
+                argThat(artifacts
+                        -> artifacts.permissionSettings.fileFsPermission.isOtherReadable == true),
+                eq(dexPath), eq(isa), any(), eq(compilerFilter), isNull(), any(), anyInt(),
+                argThat(dexoptOptions -> dexoptOptions.generateAppImage == false));
+    }
+
+    private void verifyProfileNotUsed(ProfilePath profile) throws Exception {
+        assertThat(mUsedProfiles)
+                .comparingElementsUsing(TestingUtils.<ProfilePath>deepEquality())
+                .doesNotContain(profile);
+    }
+
+    private void makeProfileUsable(ProfilePath profile) throws Exception {
+        lenient().when(mArtd.isProfileUsable(deepEq(profile), any())).thenAnswer(invocation -> {
+            mUsedProfiles.add(invocation.<ProfilePath>getArgument(0));
+            return true;
+        });
+        lenient()
+                .when(mArtd.copyAndRewriteProfile(deepEq(profile), any(), any()))
+                .thenAnswer(invocation -> {
+                    mUsedProfiles.add(invocation.<ProfilePath>getArgument(0));
+                    return true;
+                });
+    }
+
+    private void makeProfileNotUsable(ProfilePath profile) throws Exception {
+        lenient().when(mArtd.isProfileUsable(deepEq(profile), any())).thenReturn(false);
+        lenient()
+                .when(mArtd.copyAndRewriteProfile(deepEq(profile), any(), any()))
+                .thenReturn(false);
+    }
 }
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
index 92b4cb2..2f2c33f 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
@@ -26,6 +26,7 @@
 
 import android.content.pm.ApplicationInfo;
 import android.os.SystemProperties;
+import android.os.UserHandle;
 
 import com.android.server.art.testing.StaticMockitoRule;
 import com.android.server.art.wrapper.AndroidPackageApi;
@@ -41,6 +42,8 @@
 
 public class PrimaryDexOptimizerTestBase {
     protected static final String PKG_NAME = "com.example.foo";
+    protected static final int UID = 12345;
+    protected static final int SHARED_GID = UserHandle.getSharedAppGid(UID);
 
     @Rule public StaticMockitoRule mockitoRule = new StaticMockitoRule(SystemProperties.class);
 
@@ -55,6 +58,7 @@
     public void setUp() throws Exception {
         lenient().when(mInjector.getArtd()).thenReturn(mArtd);
         lenient().when(mInjector.isSystemUiPackage(any())).thenReturn(false);
+        lenient().when(mInjector.isUsedByOtherApps(any())).thenReturn(false);
 
         lenient()
                 .when(SystemProperties.get("dalvik.vm.systemuicompilerfilter"))
@@ -62,6 +66,8 @@
         lenient()
                 .when(SystemProperties.getBoolean(eq("dalvik.vm.always_debuggable"), anyBoolean()))
                 .thenReturn(false);
+        lenient().when(SystemProperties.get("dalvik.vm.appimageformat")).thenReturn("lz4");
+        lenient().when(SystemProperties.get("pm.dexopt.shared")).thenReturn("speed");
 
         mPkgState = createPackageState();
         mPkg = mPkgState.getAndroidPackage();
@@ -83,12 +89,15 @@
         lenient()
                 .when(pkg.getSplitFlags())
                 .thenReturn(new int[] {ApplicationInfo.FLAG_HAS_CODE, 0});
-        lenient().when(pkg.getUid()).thenReturn(12345);
+        lenient().when(pkg.getUid()).thenReturn(UID);
         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);
+        lenient().when(pkg.getSdkLibName()).thenReturn(null);
+        lenient().when(pkg.getStaticSharedLibName()).thenReturn(null);
+        lenient().when(pkg.getLibraryNames()).thenReturn(new ArrayList<>());
         return pkg;
     }
 
diff --git a/libartservice/service/javatests/com/android/server/art/UtilsTest.java b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
index da39eec..a167919 100644
--- a/libartservice/service/javatests/com/android/server/art/UtilsTest.java
+++ b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
@@ -64,4 +64,12 @@
     public void testArrayIsEmptyFalse() {
         assertThat(Utils.isEmpty(new int[] {1})).isFalse();
     }
+
+    @Test
+    public void testImplies() {
+        assertThat(Utils.implies(false, false)).isTrue();
+        assertThat(Utils.implies(false, true)).isTrue();
+        assertThat(Utils.implies(true, false)).isFalse();
+        assertThat(Utils.implies(true, true)).isTrue();
+    }
 }