Refactor PrimaryDexOptimizer to prepare for secondary dex compilation.

This CL extracts common dex compilation logic into a base class
DexOptimizer, and only leave primary-dex-specific logic in
PrimaryDexOptimizer.

This is a no-op change. It only moves code around.

Bug: 249984283
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: Ifca8ef1f8079afb4d8ce0826f0f9e4c21c3ce1d8
diff --git a/libartservice/service/java/com/android/server/art/DexOptHelper.java b/libartservice/service/java/com/android/server/art/DexOptHelper.java
index b3aa9d5..c87d71b 100644
--- a/libartservice/service/java/com/android/server/art/DexOptHelper.java
+++ b/libartservice/service/java/com/android/server/art/DexOptHelper.java
@@ -100,8 +100,9 @@
             wakeLock.acquire(WAKE_LOCK_TIMEOUT_MS);
 
             if ((params.getFlags() & ArtFlags.FLAG_FOR_PRIMARY_DEX) != 0) {
-                results.addAll(mInjector.getPrimaryDexOptimizer().dexopt(
-                        pkgState, pkg, params, cancellationSignal));
+                results.addAll(
+                        mInjector.getPrimaryDexOptimizer(pkgState, pkg, params, cancellationSignal)
+                                .dexopt());
                 if (hasCancelledResult.get()) {
                     return createResult.get();
                 }
@@ -158,8 +159,10 @@
         }
 
         @NonNull
-        PrimaryDexOptimizer getPrimaryDexOptimizer() {
-            return new PrimaryDexOptimizer(mContext);
+        PrimaryDexOptimizer getPrimaryDexOptimizer(@NonNull PackageState pkgState,
+                @NonNull AndroidPackage pkg, @NonNull OptimizeParams params,
+                @NonNull CancellationSignal cancellationSignal) {
+            return new PrimaryDexOptimizer(mContext, pkgState, pkg, params, cancellationSignal);
         }
 
         @NonNull
diff --git a/libartservice/service/java/com/android/server/art/DexOptimizer.java b/libartservice/service/java/com/android/server/art/DexOptimizer.java
new file mode 100644
index 0000000..078f698
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/DexOptimizer.java
@@ -0,0 +1,582 @@
+/*
+ * 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.OutputArtifacts.PermissionSettings;
+import static com.android.server.art.ProfilePath.TmpProfilePath;
+import static com.android.server.art.Utils.Abi;
+import static com.android.server.art.model.ArtFlags.OptimizeFlags;
+import static com.android.server.art.model.OptimizeResult.DexContainerFileOptimizeResult;
+
+import android.R;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemProperties;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.DetailedDexInfo;
+import com.android.server.art.model.OptimizeParams;
+import com.android.server.art.model.OptimizeResult;
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
+
+import dalvik.system.DexFile;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** @hide */
+public abstract class DexOptimizer<DexInfoType extends DetailedDexInfo> {
+    private static final String TAG = "DexOptimizer";
+
+    @NonNull protected final Injector mInjector;
+    @NonNull protected final PackageState mPkgState;
+    /** This is always {@code mPkgState.getAndroidPackage()} and guaranteed to be non-null. */
+    @NonNull protected final AndroidPackage mPkg;
+    @NonNull protected final OptimizeParams mParams;
+    @NonNull protected final CancellationSignal mCancellationSignal;
+
+    protected DexOptimizer(@NonNull Injector injector, @NonNull PackageState pkgState,
+            @NonNull AndroidPackage pkg, @NonNull OptimizeParams params,
+            @NonNull CancellationSignal cancellationSignal) {
+        mInjector = injector;
+        mPkgState = pkgState;
+        mPkg = pkg;
+        mParams = params;
+        mCancellationSignal = cancellationSignal;
+        if (pkgState.getAppId() < 0) {
+            throw new IllegalStateException(
+                    "Package '" + pkgState.getPackageName() + "' has invalid app ID");
+        }
+    }
+
+    /**
+     * DO NOT use this method directly. Use {@link
+     * ArtManagerLocal#optimizePackage(PackageManagerLocal.FilteredSnapshot, String,
+     * OptimizeParams)}.
+     */
+    @NonNull
+    public final List<DexContainerFileOptimizeResult> dexopt() throws RemoteException {
+        List<DexContainerFileOptimizeResult> results = new ArrayList<>();
+
+        String targetCompilerFilter = adjustCompilerFilter(mParams.getCompilerFilter());
+        if (targetCompilerFilter.equals(OptimizeParams.COMPILER_FILTER_NOOP)) {
+            return results;
+        }
+
+        for (DexInfoType dexInfo : getDexInfoList()) {
+            ProfilePath profile = null;
+            boolean succeeded = true;
+            try {
+                if (!isOptimizable(dexInfo)) {
+                    continue;
+                }
+
+                String compilerFilter = targetCompilerFilter;
+
+                boolean needsToBeShared = needsToBeShared(dexInfo);
+                boolean isOtherReadable = true;
+                // If true, implies that the profile has changed since the last compilation.
+                boolean profileMerged = false;
+                if (DexFile.isProfileGuidedCompilerFilter(compilerFilter)) {
+                    if (needsToBeShared) {
+                        profile = initReferenceProfile(dexInfo);
+                    } else {
+                        Pair<ProfilePath, Boolean> pair = getOrInitReferenceProfile(dexInfo);
+                        if (pair != null) {
+                            profile = pair.first;
+                            isOtherReadable = pair.second;
+                        }
+                        ProfilePath mergedProfile = mergeProfiles(dexInfo, profile);
+                        if (mergedProfile != null) {
+                            if (profile != null && profile.getTag() == ProfilePath.tmpProfilePath) {
+                                mInjector.getArtd().deleteProfile(profile);
+                            }
+                            profile = mergedProfile;
+                            isOtherReadable = false;
+                            profileMerged = true;
+                        }
+                    }
+                    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";
+                    }
+                }
+                boolean isProfileGuidedCompilerFilter =
+                        DexFile.isProfileGuidedCompilerFilter(compilerFilter);
+                Utils.check(isProfileGuidedCompilerFilter == (profile != null));
+
+                boolean canBePublic = !isProfileGuidedCompilerFilter || isOtherReadable;
+                Utils.check(Utils.implies(needsToBeShared, canBePublic));
+                PermissionSettings permissionSettings = getPermissionSettings(dexInfo, canBePublic);
+
+                DexoptOptions dexoptOptions = getDexoptOptions(isProfileGuidedCompilerFilter);
+
+                for (Abi abi : getAllAbis(dexInfo)) {
+                    @OptimizeResult.OptimizeStatus int status = OptimizeResult.OPTIMIZE_SKIPPED;
+                    long wallTimeMs = 0;
+                    long cpuTimeMs = 0;
+                    try {
+                        DexoptTarget target = DexoptTarget.builder()
+                                                      .setDexInfo(dexInfo)
+                                                      .setIsa(abi.isa())
+                                                      .setIsInDalvikCache(isInDalvikCache())
+                                                      .setCompilerFilter(compilerFilter)
+                                                      .build();
+                        GetDexoptNeededOptions options =
+                                GetDexoptNeededOptions.builder()
+                                        .setProfileMerged(profileMerged)
+                                        .setFlags(mParams.getFlags())
+                                        .setNeedsToBePublic(needsToBeShared)
+                                        .build();
+
+                        GetDexoptNeededResult getDexoptNeededResult =
+                                getDexoptNeeded(target, options);
+
+                        if (!getDexoptNeededResult.isDexoptNeeded) {
+                            continue;
+                        }
+
+                        IArtdCancellationSignal artdCancellationSignal =
+                                mInjector.getArtd().createCancellationSignal();
+                        mCancellationSignal.setOnCancelListener(() -> {
+                            try {
+                                artdCancellationSignal.cancel();
+                            } catch (RemoteException e) {
+                                Log.e(TAG, "An error occurred when sending a cancellation signal",
+                                        e);
+                            }
+                        });
+
+                        DexoptResult dexoptResult = dexoptFile(target, profile,
+                                getDexoptNeededResult, permissionSettings,
+                                mParams.getPriorityClass(), dexoptOptions, artdCancellationSignal);
+                        status = dexoptResult.cancelled ? OptimizeResult.OPTIMIZE_CANCELLED
+                                                        : OptimizeResult.OPTIMIZE_PERFORMED;
+                        wallTimeMs = dexoptResult.wallTimeMs;
+                        cpuTimeMs = dexoptResult.cpuTimeMs;
+
+                        if (status == OptimizeResult.OPTIMIZE_CANCELLED) {
+                            return results;
+                        }
+                    } catch (ServiceSpecificException e) {
+                        // Log the error and continue.
+                        Log.e(TAG,
+                                String.format("Failed to dexopt [packageName = %s, dexPath = %s, "
+                                                + "isa = %s, classLoaderContext = %s]",
+                                        mPkgState.getPackageName(), dexInfo.dexPath(), abi.isa(),
+                                        dexInfo.classLoaderContext()),
+                                e);
+                        status = OptimizeResult.OPTIMIZE_FAILED;
+                    } finally {
+                        results.add(new DexContainerFileOptimizeResult(dexInfo.dexPath(),
+                                abi.isPrimaryAbi(), abi.name(), compilerFilter, status, wallTimeMs,
+                                cpuTimeMs));
+                        if (status != OptimizeResult.OPTIMIZE_SKIPPED
+                                && status != OptimizeResult.OPTIMIZE_PERFORMED) {
+                            succeeded = false;
+                        }
+                        // Make sure artd does not leak even if the caller holds
+                        // `mCancellationSignal` forever.
+                        mCancellationSignal.setOnCancelListener(null);
+                    }
+                }
+
+                if (profile != null && succeeded) {
+                    if (profile.getTag() == ProfilePath.tmpProfilePath) {
+                        // Commit the profile only if dexopt succeeds.
+                        if (commitProfileChanges(profile.getTmpProfilePath())) {
+                            profile = null;
+                        }
+                    }
+                    if (profileMerged) {
+                        // Note that this is just an optimization, to reduce the amount of data that
+                        // the runtime writes on every profile save. The profile merge result on the
+                        // next run won't change regardless of whether the cleanup is done or not
+                        // because profman only looks at the diff.
+                        // A caveat is that it may delete more than what has been merged, if the
+                        // runtime writes additional entries between the merge and the cleanup, but
+                        // this is fine because the runtime writes all JITed classes and methods on
+                        // every save and the additional entries will likely be written back on the
+                        // next save.
+                        cleanupCurProfiles(dexInfo);
+                    }
+                }
+            } finally {
+                if (profile != null && profile.getTag() == ProfilePath.tmpProfilePath) {
+                    mInjector.getArtd().deleteProfile(profile);
+                }
+            }
+        }
+
+        return results;
+    }
+
+    @NonNull
+    private String adjustCompilerFilter(@NonNull String targetCompilerFilter) {
+        if (mInjector.isSystemUiPackage(mPkgState.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 (mPkg.isVmSafeMode() || mPkg.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;
+    }
+
+    /**
+     * Gets the existing reference profile if exists, or initializes a reference profile from an
+     * external profile.
+     *
+     * @return A pair where the first element is the found or initialized profile, and the second
+     *         element is true if the profile is readable by others. Or null if there is no
+     *         reference profile or external profile to use.
+     */
+    @Nullable
+    private Pair<ProfilePath, Boolean> getOrInitReferenceProfile(@NonNull DexInfoType dexInfo)
+            throws RemoteException {
+        ProfilePath refProfile = buildRefProfilePath(dexInfo);
+        try {
+            if (mInjector.getArtd().isProfileUsable(refProfile, dexInfo.dexPath())) {
+                boolean isOtherReadable = mInjector.getArtd().getProfileVisibility(refProfile)
+                        == FileVisibility.OTHER_READABLE;
+                return Pair.create(refProfile, isOtherReadable);
+            }
+        } catch (ServiceSpecificException e) {
+            Log.e(TAG,
+                    "Failed to use the existing reference profile "
+                            + AidlUtils.toString(refProfile),
+                    e);
+        }
+
+        ProfilePath initializedProfile = initReferenceProfile(dexInfo);
+        return initializedProfile != null ? Pair.create(initializedProfile, true) : null;
+    }
+
+    @NonNull
+    private DexoptOptions getDexoptOptions(boolean isProfileGuidedFilter) {
+        DexoptOptions dexoptOptions = new DexoptOptions();
+        dexoptOptions.compilationReason = mParams.getReason();
+        dexoptOptions.targetSdkVersion = mPkg.getTargetSdkVersion();
+        dexoptOptions.debuggable = mPkg.isDebuggable() || isAlwaysDebuggable();
+        // Generating a meaningful app image needs a profile to determine what to include in the
+        // image. Otherwise, the app image will be nearly empty.
+        dexoptOptions.generateAppImage =
+                isProfileGuidedFilter && isAppImageAllowed() && isAppImageEnabled();
+        dexoptOptions.hiddenApiPolicyEnabled = isHiddenApiPolicyEnabled();
+        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() {
+        if (mPkg.isSignedWithPlatformKey()) {
+            return false;
+        }
+        if (mPkgState.isSystem() || mPkgState.isUpdatedSystemApp()) {
+            // TODO(b/236389629): Check whether the app is in hidden api whitelist.
+            return !mPkg.isUsesNonSdkApi();
+        }
+        return true;
+    }
+
+    @NonNull
+    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(
+                target.dexInfo().dexPath(), target.isa(), target.dexInfo().classLoaderContext(),
+                target.compilerFilter(), dexoptTrigger);
+
+        return result;
+    }
+
+    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 ((options.flags() & ArtFlags.FLAG_SHOULD_DOWNGRADE) != 0) {
+            return DexoptTrigger.COMPILER_FILTER_IS_WORSE;
+        }
+
+        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 DexoptResult dexoptFile(@NonNull DexoptTarget target, @Nullable ProfilePath profile,
+            @NonNull GetDexoptNeededResult getDexoptNeededResult,
+            @NonNull PermissionSettings permissionSettings, @PriorityClass int priorityClass,
+            @NonNull DexoptOptions dexoptOptions, IArtdCancellationSignal artdCancellationSignal)
+            throws RemoteException {
+        OutputArtifacts outputArtifacts = AidlUtils.buildOutputArtifacts(target.dexInfo().dexPath(),
+                target.isa(), target.isInDalvikCache(), permissionSettings);
+
+        VdexPath inputVdex =
+                getInputVdex(getDexoptNeededResult, target.dexInfo().dexPath(), target.isa());
+
+        return mInjector.getArtd().dexopt(outputArtifacts, target.dexInfo().dexPath(), target.isa(),
+                target.dexInfo().classLoaderContext(), target.compilerFilter(), profile, inputVdex,
+                priorityClass, dexoptOptions, artdCancellationSignal);
+    }
+
+    @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);
+        }
+    }
+
+    private boolean commitProfileChanges(@NonNull TmpProfilePath profile) throws RemoteException {
+        try {
+            mInjector.getArtd().commitTmpProfile(profile);
+            return true;
+        } catch (ServiceSpecificException e) {
+            Log.e(TAG, "Failed to commit profile changes " + AidlUtils.toString(profile.finalPath),
+                    e);
+            return false;
+        }
+    }
+
+    @Nullable
+    private ProfilePath mergeProfiles(@NonNull DexInfoType dexInfo,
+            @Nullable ProfilePath referenceProfile) throws RemoteException {
+        OutputProfile output = buildOutputProfile(dexInfo, false /* isPublic */);
+
+        try {
+            if (mInjector.getArtd().mergeProfiles(
+                        getCurProfiles(dexInfo), referenceProfile, output, dexInfo.dexPath())) {
+                return ProfilePath.tmpProfilePath(output.profilePath);
+            }
+        } catch (ServiceSpecificException e) {
+            Log.e(TAG,
+                    "Failed to merge profiles " + AidlUtils.toString(output.profilePath.finalPath),
+                    e);
+        }
+
+        return null;
+    }
+
+    private void cleanupCurProfiles(@NonNull DexInfoType dexInfo) throws RemoteException {
+        for (ProfilePath profile : getCurProfiles(dexInfo)) {
+            mInjector.getArtd().deleteProfile(profile);
+        }
+    }
+
+    // Methods to be implemented by child classes.
+
+    /** Returns true if the artifacts should be written to the global dalvik-cache directory. */
+    protected abstract boolean isInDalvikCache();
+
+    /** Returns information about all dex files. */
+    @NonNull protected abstract List<DexInfoType> getDexInfoList();
+
+    /** Returns true if the given dex file should be optimized. */
+    protected abstract boolean isOptimizable(@NonNull DexInfoType dexInfo) throws RemoteException;
+
+    /** Returns true if the artifacts should be shared with other apps. */
+    protected abstract boolean needsToBeShared(@NonNull DexInfoType dexInfo);
+
+    /**
+     * Returns a reference profile initialized from an external profile (e.g., a DM profile) if
+     * one exists, or null otherwise.
+     */
+    @Nullable
+    protected abstract ProfilePath initReferenceProfile(@NonNull DexInfoType dexInfo)
+            throws RemoteException;
+
+    /** Returns the permission settings to use for the artifacts of the given dex file. */
+    @NonNull
+    protected abstract PermissionSettings getPermissionSettings(
+            @NonNull DexInfoType dexInfo, boolean canBePublic);
+
+    /** Returns all ABIs that the given dex file should be compiled for. */
+    @NonNull protected abstract List<Abi> getAllAbis(@NonNull DexInfoType dexInfo);
+
+    /** Returns the path to the reference profile of the given dex file. */
+    @NonNull protected abstract ProfilePath buildRefProfilePath(@NonNull DexInfoType dexInfo);
+
+    /** Returns true if app image (--app-image-fd) is allowed. */
+    protected abstract boolean isAppImageAllowed();
+
+    /**
+     * Returns the data structure that represents the temporary profile to use during processing.
+     */
+    @NonNull
+    protected abstract OutputProfile buildOutputProfile(
+            @NonNull DexInfoType dexInfo, boolean isPublic);
+
+    /** Returns the paths to the current profiles of the given dex file. */
+    @NonNull protected abstract List<ProfilePath> getCurProfiles(@NonNull DexInfoType dexInfo);
+
+    @AutoValue
+    abstract static class DexoptTarget {
+        abstract @NonNull DetailedDexInfo dexInfo();
+        abstract @NonNull String isa();
+        abstract boolean isInDalvikCache();
+        abstract @NonNull String compilerFilter();
+
+        static Builder builder() {
+            return new AutoValue_DexOptimizer_DexoptTarget.Builder();
+        }
+
+        @AutoValue.Builder
+        abstract static class Builder {
+            abstract Builder setDexInfo(@NonNull DetailedDexInfo 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_DexOptimizer_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.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public static class Injector {
+        @NonNull private final Context mContext;
+
+        Injector(@NonNull Context context) {
+            mContext = context;
+        }
+
+        boolean isSystemUiPackage(@NonNull String packageName) {
+            return packageName.equals(mContext.getString(R.string.config_systemUi));
+        }
+
+        @NonNull
+        UserManager getUserManager() {
+            return Objects.requireNonNull(mContext.getSystemService(UserManager.class));
+        }
+
+        @NonNull
+        DexUseManager getDexUseManager() {
+            return DexUseManager.getInstance();
+        }
+
+        @NonNull
+        IArtd getArtd() {
+            return Utils.getArtd();
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/DexUseManager.java b/libartservice/service/java/com/android/server/art/DexUseManager.java
index 5c023e1..9cbca54 100644
--- a/libartservice/service/java/com/android/server/art/DexUseManager.java
+++ b/libartservice/service/java/com/android/server/art/DexUseManager.java
@@ -25,6 +25,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.Immutable;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.art.model.DetailedDexInfo;
 import com.android.server.art.proto.DexUseProto;
 import com.android.server.art.proto.Int32Value;
 import com.android.server.art.proto.PackageDexUseProto;
@@ -343,7 +344,7 @@
      */
     @Immutable
     @AutoValue
-    public abstract static class SecondaryDexInfo {
+    public abstract static class SecondaryDexInfo implements DetailedDexInfo {
         // Special encoding used to denote a foreign ClassLoader was found when trying to encode
         // class loader contexts for each classpath element in a ClassLoader.
         // Must be in sync with `kUnsupportedClassLoaderContextEncoding` in
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
index ef7d659..ea1b20a 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
@@ -16,17 +16,11 @@
 
 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.ProfilePath.TmpProfilePath;
-import static com.android.server.art.ProfilePath.WritableProfilePath;
 import static com.android.server.art.Utils.Abi;
-import static com.android.server.art.model.ArtFlags.OptimizeFlags;
-import static com.android.server.art.model.OptimizeResult.DexContainerFileOptimizeResult;
 
-import android.R;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -34,15 +28,11 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
-import android.os.SystemProperties;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.text.TextUtils;
 import android.util.Log;
-import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.OptimizeParams;
 import com.android.server.art.model.OptimizeResult;
 import com.android.server.pm.PackageManagerLocal;
@@ -58,264 +48,60 @@
 import java.util.List;
 
 /** @hide */
-public class PrimaryDexOptimizer {
+public class PrimaryDexOptimizer extends DexOptimizer<DetailedPrimaryDexInfo> {
     private static final String TAG = "PrimaryDexOptimizer";
 
-    @NonNull private final Injector mInjector;
+    private final int mSharedGid;
 
-    public PrimaryDexOptimizer(@NonNull Context context) {
-        this(new Injector(context));
+    public PrimaryDexOptimizer(@NonNull Context context, @NonNull PackageState pkgState,
+            @NonNull AndroidPackage pkg, @NonNull OptimizeParams params,
+            @NonNull CancellationSignal cancellationSignal) {
+        this(new Injector(context), pkgState, pkg, params, cancellationSignal);
     }
 
     @VisibleForTesting
-    public PrimaryDexOptimizer(@NonNull Injector injector) {
-        mInjector = injector;
-    }
-
-    /**
-     * DO NOT use this method directly. Use {@link
-     * ArtManagerLocal#optimizePackage(PackageManagerLocal.FilteredSnapshot, String,
-     * OptimizeParams)}.
-     */
-    @NonNull
-    public List<DexContainerFileOptimizeResult> dexopt(@NonNull PackageState pkgState,
+    public PrimaryDexOptimizer(@NonNull Injector injector, @NonNull PackageState pkgState,
             @NonNull AndroidPackage pkg, @NonNull OptimizeParams params,
-            @NonNull CancellationSignal cancellationSignal) throws RemoteException {
-        List<DexContainerFileOptimizeResult> results = new ArrayList<>();
+            @NonNull CancellationSignal cancellationSignal) {
+        super(injector, pkgState, pkg, params, cancellationSignal);
 
-        int appId = pkgState.getAppId();
-        if (appId < 0) {
-            throw new IllegalStateException(
-                    "Package '" + pkgState.getPackageName() + "' has invalid app ID");
-        }
-        int sharedGid = UserHandle.getSharedAppGid(appId);
-        if (sharedGid < 0) {
+        mSharedGid = UserHandle.getSharedAppGid(pkgState.getAppId());
+        if (mSharedGid < 0) {
             throw new IllegalStateException(
                     String.format("Unable to get shared gid for package '%s' (app ID: %d)",
-                            pkgState.getPackageName(), appId));
+                            pkgState.getPackageName(), pkgState.getAppId()));
         }
-
-        String targetCompilerFilter =
-                adjustCompilerFilter(pkgState, pkg, params.getCompilerFilter(), params.getReason());
-        if (targetCompilerFilter.equals(OptimizeParams.COMPILER_FILTER_NOOP)) {
-            return results;
-        }
-
-        boolean isInDalvikCache = Utils.isInDalvikCache(pkgState);
-
-        for (DetailedPrimaryDexInfo dexInfo : PrimaryDexUtils.getDetailedDexInfo(pkgState, pkg)) {
-            ProfilePath profile = null;
-            boolean succeeded = true;
-            try {
-                if (!dexInfo.hasCode()) {
-                    continue;
-                }
-
-                // TODO(jiakaiz): Support optimizing a single split.
-
-                String compilerFilter = targetCompilerFilter;
-
-                boolean needsToBeShared = isSharedLibrary(pkg)
-                        || mInjector.getDexUseManager().isPrimaryDexUsedByOtherApps(
-                                pkgState.getPackageName(), dexInfo.dexPath());
-                boolean isOtherReadable = true;
-                // If true, implies that the profile has changed since the last compilation.
-                boolean profileMerged = false;
-                if (DexFile.isProfileGuidedCompilerFilter(compilerFilter)) {
-                    if (needsToBeShared) {
-                        profile = initReferenceProfile(pkgState, dexInfo, appId, sharedGid);
-                    } else {
-                        Pair<ProfilePath, Boolean> pair =
-                                getOrInitReferenceProfile(pkgState, dexInfo, appId, sharedGid);
-                        if (pair != null) {
-                            profile = pair.first;
-                            isOtherReadable = pair.second;
-                        }
-                        ProfilePath mergedProfile =
-                                mergeProfiles(pkgState, dexInfo, appId, sharedGid, profile);
-                        if (mergedProfile != null) {
-                            if (profile != null && profile.getTag() == ProfilePath.tmpProfilePath) {
-                                mInjector.getArtd().deleteProfile(profile);
-                            }
-                            profile = mergedProfile;
-                            isOtherReadable = false;
-                            profileMerged = true;
-                        }
-                    }
-                    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";
-                    }
-                }
-                boolean isProfileGuidedCompilerFilter =
-                        DexFile.isProfileGuidedCompilerFilter(compilerFilter);
-                Utils.check(isProfileGuidedCompilerFilter == (profile != null));
-
-                boolean canBePublic = !isProfileGuidedCompilerFilter || isOtherReadable;
-                Utils.check(Utils.implies(needsToBeShared, canBePublic));
-                PermissionSettings permissionSettings =
-                        getPermissionSettings(sharedGid, canBePublic);
-
-                DexoptOptions dexoptOptions =
-                        getDexoptOptions(pkgState, pkg, params, isProfileGuidedCompilerFilter);
-
-                for (Abi abi : Utils.getAllAbis(pkgState)) {
-                    @OptimizeResult.OptimizeStatus int status = OptimizeResult.OPTIMIZE_SKIPPED;
-                    long wallTimeMs = 0;
-                    long cpuTimeMs = 0;
-                    try {
-                        DexoptTarget target = DexoptTarget.builder()
-                                                      .setDexInfo(dexInfo)
-                                                      .setIsa(abi.isa())
-                                                      .setIsInDalvikCache(isInDalvikCache)
-                                                      .setCompilerFilter(compilerFilter)
-                                                      .build();
-                        GetDexoptNeededOptions options =
-                                GetDexoptNeededOptions.builder()
-                                        .setProfileMerged(profileMerged)
-                                        .setFlags(params.getFlags())
-                                        .setNeedsToBePublic(needsToBeShared)
-                                        .build();
-
-                        GetDexoptNeededResult getDexoptNeededResult =
-                                getDexoptNeeded(target, options);
-
-                        if (!getDexoptNeededResult.isDexoptNeeded) {
-                            continue;
-                        }
-
-                        IArtdCancellationSignal artdCancellationSignal =
-                                mInjector.getArtd().createCancellationSignal();
-                        cancellationSignal.setOnCancelListener(() -> {
-                            try {
-                                artdCancellationSignal.cancel();
-                            } catch (RemoteException e) {
-                                Log.e(TAG, "An error occurred when sending a cancellation signal",
-                                        e);
-                            }
-                        });
-
-                        DexoptResult dexoptResult = dexoptFile(target, profile,
-                                getDexoptNeededResult, permissionSettings,
-                                params.getPriorityClass(), dexoptOptions, artdCancellationSignal);
-                        status = dexoptResult.cancelled ? OptimizeResult.OPTIMIZE_CANCELLED
-                                                        : OptimizeResult.OPTIMIZE_PERFORMED;
-                        wallTimeMs = dexoptResult.wallTimeMs;
-                        cpuTimeMs = dexoptResult.cpuTimeMs;
-
-                        if (status == OptimizeResult.OPTIMIZE_CANCELLED) {
-                            return results;
-                        }
-                    } 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(), abi.isa(),
-                                        dexInfo.classLoaderContext()),
-                                e);
-                        status = OptimizeResult.OPTIMIZE_FAILED;
-                    } finally {
-                        results.add(new DexContainerFileOptimizeResult(dexInfo.dexPath(),
-                                abi.isPrimaryAbi(), abi.name(), compilerFilter, status, wallTimeMs,
-                                cpuTimeMs));
-                        if (status != OptimizeResult.OPTIMIZE_SKIPPED
-                                && status != OptimizeResult.OPTIMIZE_PERFORMED) {
-                            succeeded = false;
-                        }
-                        // Make sure artd does not leak even if the caller holds
-                        // `cancellationSignal` forever.
-                        cancellationSignal.setOnCancelListener(null);
-                    }
-                }
-
-                if (profile != null && succeeded) {
-                    if (profile.getTag() == ProfilePath.tmpProfilePath) {
-                        // Commit the profile only if dexopt succeeds.
-                        if (commitProfileChanges(profile.getTmpProfilePath())) {
-                            profile = null;
-                        }
-                    }
-                    if (profileMerged) {
-                        // Note that this is just an optimization, to reduce the amount of data that
-                        // the runtime writes on every profile save. The profile merge result on the
-                        // next run won't change regardless of whether the cleanup is done or not
-                        // because profman only looks at the diff.
-                        // A caveat is that it may delete more than what has been merged, if the
-                        // runtime writes additional entries between the merge and the cleanup, but
-                        // this is fine because the runtime writes all JITed classes and methods on
-                        // every save and the additional entries will likely be written back on the
-                        // next save.
-                        cleanupCurProfiles(pkgState, dexInfo);
-                    }
-                }
-            } finally {
-                if (profile != null && profile.getTag() == ProfilePath.tmpProfilePath) {
-                    mInjector.getArtd().deleteProfile(profile);
-                }
-            }
-        }
-
-        return results;
     }
 
+    @Override
+    protected boolean isInDalvikCache() {
+        return Utils.isInDalvikCache(mPkgState);
+    }
+
+    @Override
     @NonNull
-    private String adjustCompilerFilter(@NonNull PackageState pkgState,
-            @NonNull AndroidPackage 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;
+    protected List<DetailedPrimaryDexInfo> getDexInfoList() {
+        return PrimaryDexUtils.getDetailedDexInfo(mPkgState, mPkg);
     }
 
-    @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;
+    @Override
+    protected boolean isOptimizable(@NonNull DetailedPrimaryDexInfo dexInfo) {
+        // TODO(jiakaiz): Support optimizing a single split.
+        return dexInfo.hasCode();
     }
 
-    boolean isSharedLibrary(@NonNull AndroidPackage pkg) {
-        // TODO(b/242688548): Package manager should provide a better API for this.
-        return !TextUtils.isEmpty(pkg.getSdkLibraryName())
-                || !TextUtils.isEmpty(pkg.getStaticSharedLibraryName())
-                || !pkg.getLibraryNames().isEmpty();
+    @Override
+    protected boolean needsToBeShared(@NonNull DetailedPrimaryDexInfo dexInfo) {
+        return isSharedLibrary()
+                || mInjector.getDexUseManager().isPrimaryDexUsedByOtherApps(
+                        mPkgState.getPackageName(), dexInfo.dexPath());
     }
 
-    /**
-     * Returns a reference profile initialized from a prebuilt profile or a DM profile if exists, or
-     * null otherwise.
-     */
+    @Override
     @Nullable
-    private ProfilePath initReferenceProfile(@NonNull PackageState pkgState,
-            @NonNull DetailedPrimaryDexInfo dexInfo, int uid, int gid) throws RemoteException {
-        String profileName = getProfileName(dexInfo.splitName());
-        OutputProfile output = AidlUtils.buildOutputProfileForPrimary(
-                pkgState.getPackageName(), profileName, uid, gid, true /* isPublic */);
+    protected ProfilePath initReferenceProfile(@NonNull DetailedPrimaryDexInfo dexInfo)
+            throws RemoteException {
+        OutputProfile output = buildOutputProfile(dexInfo, true /* isPublic */);
 
         ProfilePath prebuiltProfile = AidlUtils.buildProfilePathForPrebuilt(dexInfo.dexPath());
         try {
@@ -350,44 +136,10 @@
         return null;
     }
 
-    /**
-     * Gets the existing reference profile if exists, or initializes a reference profile from an
-     * external profile.
-     *
-     * @return A pair where the first element is the found or initialized profile, and the second
-     *         element is true if the profile is readable by others. Or null if there is no
-     *         reference profile or external profile to use.
-     */
-    @Nullable
-    private Pair<ProfilePath, Boolean> getOrInitReferenceProfile(@NonNull PackageState pkgState,
-            @NonNull DetailedPrimaryDexInfo dexInfo, int uid, int gid) throws RemoteException {
-        String profileName = getProfileName(dexInfo.splitName());
-        ProfilePath refProfile =
-                AidlUtils.buildProfilePathForPrimaryRef(pkgState.getPackageName(), profileName);
-        try {
-            if (mInjector.getArtd().isProfileUsable(refProfile, dexInfo.dexPath())) {
-                boolean isOtherReadable = mInjector.getArtd().getProfileVisibility(refProfile)
-                        == FileVisibility.OTHER_READABLE;
-                return Pair.create(refProfile, isOtherReadable);
-            }
-        } catch (ServiceSpecificException e) {
-            Log.e(TAG,
-                    "Failed to use the existing reference profile "
-                            + AidlUtils.toString(refProfile),
-                    e);
-        }
-
-        ProfilePath initializedProfile = initReferenceProfile(pkgState, dexInfo, uid, gid);
-        return initializedProfile != null ? Pair.create(initializedProfile, true) : null;
-    }
-
+    @Override
     @NonNull
-    public String getProfileName(@Nullable String splitName) {
-        return splitName == null ? "primary" : splitName + ".split";
-    }
-
-    @NonNull
-    PermissionSettings getPermissionSettings(int sharedGid, boolean canBePublic) {
+    protected PermissionSettings getPermissionSettings(
+            @NonNull DetailedPrimaryDexInfo dexInfo, 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
@@ -395,262 +147,66 @@
         FsPermission dirFsPermission = AidlUtils.buildFsPermission(Process.SYSTEM_UID,
                 Process.SYSTEM_UID, false /* isOtherReadable */, true /* isOtherExecutable */);
         FsPermission fileFsPermission =
-                AidlUtils.buildFsPermission(Process.SYSTEM_UID, sharedGid, canBePublic);
+                AidlUtils.buildFsPermission(Process.SYSTEM_UID, mSharedGid, canBePublic);
         // For primary dex, we can use the default SELinux context.
         SeContext seContext = null;
         return AidlUtils.buildPermissionSettings(dirFsPermission, fileFsPermission, seContext);
     }
 
+    @Override
     @NonNull
-    private DexoptOptions getDexoptOptions(@NonNull PackageState pkgState,
-            @NonNull AndroidPackage pkg, @NonNull OptimizeParams params,
-            boolean isProfileGuidedFilter) {
-        DexoptOptions dexoptOptions = new DexoptOptions();
-        dexoptOptions.compilationReason = params.getReason();
-        dexoptOptions.targetSdkVersion = pkg.getTargetSdkVersion();
-        dexoptOptions.debuggable = pkg.isDebuggable() || isAlwaysDebuggable();
-        // 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;
+    protected List<Abi> getAllAbis(@NonNull DetailedPrimaryDexInfo dexInfo) {
+        return Utils.getAllAbis(mPkgState);
     }
 
-    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 AndroidPackage 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;
-    }
-
+    @Override
     @NonNull
-    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(
-                target.dexInfo().dexPath(), target.isa(), target.dexInfo().classLoaderContext(),
-                target.compilerFilter(), dexoptTrigger);
-
-        return result;
-    }
-
-    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 ((options.flags() & ArtFlags.FLAG_SHOULD_DOWNGRADE) != 0) {
-            return DexoptTrigger.COMPILER_FILTER_IS_WORSE;
-        }
-
-        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 DexoptResult dexoptFile(@NonNull DexoptTarget target, @Nullable ProfilePath profile,
-            @NonNull GetDexoptNeededResult getDexoptNeededResult,
-            @NonNull PermissionSettings permissionSettings, @PriorityClass int priorityClass,
-            @NonNull DexoptOptions dexoptOptions, IArtdCancellationSignal artdCancellationSignal)
-            throws RemoteException {
-        OutputArtifacts outputArtifacts = AidlUtils.buildOutputArtifacts(target.dexInfo().dexPath(),
-                target.isa(), target.isInDalvikCache(), permissionSettings);
-
-        VdexPath inputVdex =
-                getInputVdex(getDexoptNeededResult, target.dexInfo().dexPath(), target.isa());
-
-        return mInjector.getArtd().dexopt(outputArtifacts, target.dexInfo().dexPath(), target.isa(),
-                target.dexInfo().classLoaderContext(), target.compilerFilter(), profile, inputVdex,
-                priorityClass, dexoptOptions, artdCancellationSignal);
-    }
-
-    @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);
-        }
-    }
-
-    private boolean commitProfileChanges(@NonNull TmpProfilePath profile) throws RemoteException {
-        try {
-            mInjector.getArtd().commitTmpProfile(profile);
-            return true;
-        } catch (ServiceSpecificException e) {
-            Log.e(TAG, "Failed to commit profile changes " + AidlUtils.toString(profile.finalPath),
-                    e);
-            return false;
-        }
-    }
-
-    @Nullable
-    private ProfilePath mergeProfiles(@NonNull PackageState pkgState,
-            @NonNull DetailedPrimaryDexInfo dexInfo, int uid, int gid,
-            @Nullable ProfilePath referenceProfile) throws RemoteException {
+    protected ProfilePath buildRefProfilePath(@NonNull DetailedPrimaryDexInfo dexInfo) {
         String profileName = getProfileName(dexInfo.splitName());
-        OutputProfile output = AidlUtils.buildOutputProfileForPrimary(
-                pkgState.getPackageName(), profileName, uid, gid, false /* isPublic */);
-
-        try {
-            if (mInjector.getArtd().mergeProfiles(getCurProfiles(pkgState, dexInfo),
-                        referenceProfile, output, dexInfo.dexPath())) {
-                return ProfilePath.tmpProfilePath(output.profilePath);
-            }
-        } catch (ServiceSpecificException e) {
-            Log.e(TAG,
-                    "Failed to merge profiles " + AidlUtils.toString(output.profilePath.finalPath),
-                    e);
-        }
-
-        return null;
+        return AidlUtils.buildProfilePathForPrimaryRef(mPkgState.getPackageName(), profileName);
     }
 
-    private void cleanupCurProfiles(@NonNull PackageState pkgState,
-            @NonNull DetailedPrimaryDexInfo dexInfo) throws RemoteException {
-        for (ProfilePath profile : getCurProfiles(pkgState, dexInfo)) {
-            mInjector.getArtd().deleteProfile(profile);
-        }
+    @Override
+    protected boolean isAppImageAllowed() {
+        // 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).
+        return !PrimaryDexUtils.isIsolatedSplitLoading(mPkg);
     }
 
+    @Override
     @NonNull
-    private List<ProfilePath> getCurProfiles(
-            @NonNull PackageState pkgState, @NonNull DetailedPrimaryDexInfo dexInfo) {
+    protected OutputProfile buildOutputProfile(
+            @NonNull DetailedPrimaryDexInfo dexInfo, boolean isPublic) {
+        String profileName = getProfileName(dexInfo.splitName());
+        return AidlUtils.buildOutputProfileForPrimary(mPkgState.getPackageName(), profileName,
+                mPkgState.getAppId(), mSharedGid, isPublic);
+    }
+
+    @Override
+    @NonNull
+    protected List<ProfilePath> getCurProfiles(@NonNull DetailedPrimaryDexInfo dexInfo) {
         List<ProfilePath> profiles = new ArrayList<>();
         for (UserHandle handle :
                 mInjector.getUserManager().getUserHandles(true /* excludeDying */)) {
             int userId = handle.getIdentifier();
-            PackageUserState userState = pkgState.getStateForUser(handle);
+            PackageUserState userState = mPkgState.getStateForUser(handle);
             if (userState.isInstalled()) {
                 profiles.add(AidlUtils.buildProfilePathForPrimaryCur(
-                        userId, pkgState.getPackageName(), getProfileName(dexInfo.splitName())));
+                        userId, mPkgState.getPackageName(), getProfileName(dexInfo.splitName())));
             }
         }
         return profiles;
     }
 
-    @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();
-        }
+    private boolean isSharedLibrary() {
+        // TODO(b/242688548): Package manager should provide a better API for this.
+        return !TextUtils.isEmpty(mPkg.getSdkLibraryName())
+                || !TextUtils.isEmpty(mPkg.getStaticSharedLibraryName())
+                || !mPkg.getLibraryNames().isEmpty();
     }
 
-    @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.
-     *
-     * @hide
-     */
-    @VisibleForTesting
-    public static class Injector {
-        @NonNull private final Context mContext;
-
-        Injector(@NonNull Context context) {
-            mContext = context;
-        }
-
-        boolean isSystemUiPackage(@NonNull String packageName) {
-            return packageName.equals(mContext.getString(R.string.config_systemUi));
-        }
-
-        @NonNull
-        UserManager getUserManager() {
-            return mContext.getSystemService(UserManager.class);
-        }
-
-        @NonNull
-        DexUseManager getDexUseManager() {
-            return DexUseManager.getInstance();
-        }
-
-        @NonNull
-        public IArtd getArtd() {
-            return Utils.getArtd();
-        }
+    @NonNull
+    private String getProfileName(@Nullable String splitName) {
+        return splitName == null ? "primary" : splitName + ".split";
     }
 }
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
index 91507ed..3ffaf54 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
@@ -21,6 +21,7 @@
 import android.text.TextUtils;
 
 import com.android.internal.annotations.Immutable;
+import com.android.server.art.model.DetailedDexInfo;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.AndroidPackageSplit;
 import com.android.server.pm.pkg.PackageState;
@@ -302,11 +303,11 @@
      * producing it requires {@link PackageState}.
      */
     @Immutable
-    public static class DetailedPrimaryDexInfo extends PrimaryDexInfo {
+    public static class DetailedPrimaryDexInfo extends PrimaryDexInfo implements DetailedDexInfo {
         private final @Nullable String mClassLoaderContext;
 
-        DetailedPrimaryDexInfo(@NonNull AndroidPackageSplit split,
-                @Nullable String classLoaderContext) {
+        DetailedPrimaryDexInfo(
+                @NonNull AndroidPackageSplit split, @Nullable String classLoaderContext) {
             super(split);
             mClassLoaderContext = classLoaderContext;
         }
diff --git a/libartservice/service/java/com/android/server/art/model/DetailedDexInfo.java b/libartservice/service/java/com/android/server/art/model/DetailedDexInfo.java
new file mode 100644
index 0000000..0f9a20e
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/DetailedDexInfo.java
@@ -0,0 +1,37 @@
+/*
+ * 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.model;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.Immutable;
+
+/**
+ * Detailed information about a dex file.
+ *
+ * @hide
+ */
+@Immutable
+public interface DetailedDexInfo {
+    /** The path to the dex file. */
+    @NonNull String dexPath();
+
+    /**
+     * A string describing the structure of the class loader that the dex file is loaded with.
+     */
+    @NonNull String classLoaderContext();
+}
diff --git a/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java b/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
index a001e74..b8faf09 100644
--- a/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
@@ -90,7 +90,6 @@
 
     @Before
     public void setUp() throws Exception {
-        lenient().when(mInjector.getPrimaryDexOptimizer()).thenReturn(mPrimaryDexOptimizer);
         lenient().when(mInjector.getAppHibernationManager()).thenReturn(mAhm);
         lenient().when(mInjector.getPowerManager()).thenReturn(mPowerManager);
 
@@ -105,14 +104,17 @@
         mPkg = mPkgState.getAndroidPackage();
         mCancellationSignal = new CancellationSignal();
 
+        lenient()
+                .when(mInjector.getPrimaryDexOptimizer(
+                        same(mPkgState), same(mPkg), same(mParams), same(mCancellationSignal)))
+                .thenReturn(mPrimaryDexOptimizer);
+
         mDexOptHelper = new DexOptHelper(mInjector);
     }
 
     @Test
     public void testDexopt() throws Exception {
-        when(mPrimaryDexOptimizer.dexopt(
-                     same(mPkgState), same(mPkg), same(mParams), same(mCancellationSignal)))
-                .thenReturn(mPrimaryResults);
+        when(mPrimaryDexOptimizer.dexopt()).thenReturn(mPrimaryResults);
 
         OptimizeResult result = mDexOptHelper.dexopt(
                 mock(PackageManagerLocal.FilteredSnapshot.class), mPkgState, mPkg, mParams,
@@ -130,7 +132,7 @@
 
         InOrder inOrder = inOrder(mPrimaryDexOptimizer, mWakeLock);
         inOrder.verify(mWakeLock).acquire(anyLong());
-        inOrder.verify(mPrimaryDexOptimizer).dexopt(any(), any(), any(), any());
+        inOrder.verify(mPrimaryDexOptimizer).dexopt();
         inOrder.verify(mWakeLock).release();
     }
 
@@ -165,9 +167,7 @@
         lenient().when(mAhm.isHibernatingGlobally(PKG_NAME)).thenReturn(true);
         lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(false);
 
-        when(mPrimaryDexOptimizer.dexopt(
-                     same(mPkgState), same(mPkg), same(mParams), same(mCancellationSignal)))
-                .thenReturn(mPrimaryResults);
+        when(mPrimaryDexOptimizer.dexopt()).thenReturn(mPrimaryResults);
 
         OptimizeResult result = mDexOptHelper.dexopt(
                 mock(PackageManagerLocal.FilteredSnapshot.class), mPkgState, mPkg, mParams,
@@ -179,9 +179,7 @@
 
     @Test
     public void testDexoptAlwaysReleasesWakeLock() throws Exception {
-        when(mPrimaryDexOptimizer.dexopt(
-                     same(mPkgState), same(mPkg), same(mParams), same(mCancellationSignal)))
-                .thenThrow(IllegalStateException.class);
+        when(mPrimaryDexOptimizer.dexopt()).thenThrow(IllegalStateException.class);
 
         try {
             mDexOptHelper.dexopt(mock(PackageManagerLocal.FilteredSnapshot.class), mPkgState, mPkg,
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java
index 000858c..407fd61 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerParameterizedTest.java
@@ -70,6 +70,8 @@
 
     private OptimizeParams mOptimizeParams;
 
+    private PrimaryDexOptimizer mPrimaryDexOptimizer;
+
     @Parameter(0) public Params mParams;
 
     @Parameters(name = "{0}")
@@ -187,6 +189,9 @@
                         .setFlags(mParams.mShouldDowngrade ? ArtFlags.FLAG_SHOULD_DOWNGRADE : 0,
                                 ArtFlags.FLAG_SHOULD_DOWNGRADE)
                         .build();
+
+        mPrimaryDexOptimizer = new PrimaryDexOptimizer(
+                mInjector, mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
     }
 
     @Test
@@ -255,8 +260,7 @@
                         isNull() /* inputVdex */, eq(PriorityClass.INTERACTIVE),
                         deepEq(dexoptOptions), any());
 
-        assertThat(
-                mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal))
+        assertThat(mPrimaryDexOptimizer.dexopt())
                 .comparingElementsUsing(TestingUtils.<DexContainerFileOptimizeResult>deepEquality())
                 .containsExactly(
                         new DexContainerFileOptimizeResult("/data/app/foo/base.apk",
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
index 504be90..2faa59d 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
@@ -93,6 +93,8 @@
     private final DexoptResult mDexoptResult =
             createDexoptResult(false /* cancelled */, 200 /* wallTimeMs */, 200 /* cpuTimeMs */);
 
+    private PrimaryDexOptimizer mPrimaryDexOptimizer;
+
     private List<ProfilePath> mUsedProfiles;
 
     @Before
@@ -116,6 +118,9 @@
                 .when(mArtd.createCancellationSignal())
                 .thenReturn(mock(IArtdCancellationSignal.class));
 
+        mPrimaryDexOptimizer = new PrimaryDexOptimizer(
+                mInjector, mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+
         mUsedProfiles = new ArrayList<>();
     }
 
@@ -163,7 +168,7 @@
                                 AidlUtils.buildDexMetadataPath(mSplit0DexPath))),
                         anyInt(), any(), any());
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
     }
 
     @Test
@@ -176,7 +181,7 @@
         makeProfileUsable(mPrebuiltProfile);
         makeProfileUsable(mDmProfile);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         verify(mArtd).getDexoptNeeded(
                 eq(mDexPath), eq("arm64"), any(), eq("speed-profile"), eq(mDefaultDexoptTrigger));
@@ -210,7 +215,7 @@
         makeProfileUsable(mPrebuiltProfile);
         makeProfileUsable(mDmProfile);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm64", mRefProfile);
         checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm", mRefProfile);
@@ -225,7 +230,7 @@
         makeProfileUsable(mPrebuiltProfile);
         makeProfileUsable(mDmProfile);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         InOrder inOrder = inOrder(mArtd);
 
@@ -254,7 +259,7 @@
         when(mArtd.getProfileVisibility(deepEq(mRefProfile)))
                 .thenReturn(FileVisibility.OTHER_READABLE);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         InOrder inOrder = inOrder(mArtd);
 
@@ -295,7 +300,7 @@
         when(mArtd.getProfileVisibility(deepEq(mRefProfile)))
                 .thenReturn(FileVisibility.OTHER_READABLE);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         // It should still use "speed-profile", but with the existing reference profile only.
         verify(mArtd).getDexoptNeeded(
@@ -316,7 +321,7 @@
         makeProfileNotUsable(mPrebuiltProfile);
         makeProfileUsable(mDmProfile);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         verify(mArtd).copyAndRewriteProfile(
                 deepEq(mDmProfile), deepEq(mPublicOutputProfile), eq(mDexPath));
@@ -340,7 +345,7 @@
                      any()))
                 .thenThrow(ServiceSpecificException.class);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         verify(mArtd).deleteProfile(
                 deepEq(ProfilePath.tmpProfilePath(mPublicOutputProfile.profilePath)));
@@ -365,7 +370,7 @@
                      argThat(artifactsPath -> artifactsPath.dexPath == mDexPath)))
                 .thenReturn(FileVisibility.NOT_OTHER_READABLE);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         verify(mArtd).copyAndRewriteProfile(
                 deepEq(mDmProfile), deepEq(mPublicOutputProfile), eq(mDexPath));
@@ -403,7 +408,7 @@
                      argThat(artifactsPath -> artifactsPath.dexPath == mDexPath)))
                 .thenReturn(FileVisibility.OTHER_READABLE);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         // It should use the default dexopt trigger.
         verify(mArtd).getDexoptNeeded(
@@ -418,7 +423,7 @@
         when(mArtd.getProfileVisibility(deepEq(mSplit0RefProfile)))
                 .thenReturn(FileVisibility.NOT_OTHER_READABLE);
 
-        mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+        mPrimaryDexOptimizer.dexopt();
 
         verify(mArtd).getDexoptNeeded(eq(mSplit0DexPath), eq("arm64"), any(), eq("speed-profile"),
                 eq(mDefaultDexoptTrigger));
@@ -447,11 +452,10 @@
 
         // The result should only contain one element: the result of the first file with
         // OPTIMIZE_CANCELLED.
-        assertThat(
-                mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal)
-                        .stream()
-                        .map(DexContainerFileOptimizeResult::getStatus)
-                        .collect(Collectors.toList()))
+        assertThat(mPrimaryDexOptimizer.dexopt()
+                           .stream()
+                           .map(DexContainerFileOptimizeResult::getStatus)
+                           .collect(Collectors.toList()))
                 .containsExactly(OptimizeResult.OPTIMIZE_CANCELLED);
 
         // It shouldn't continue after being cancelled on the first file.
@@ -486,10 +490,8 @@
                 .cancel();
 
         Future<List<DexContainerFileOptimizeResult>> results =
-                Executors.newSingleThreadExecutor().submit(() -> {
-                    return mPrimaryDexOptimizer.dexopt(
-                            mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
-                });
+                Executors.newSingleThreadExecutor().submit(
+                        () -> { return mPrimaryDexOptimizer.dexopt(); });
 
         assertThat(dexoptStarted.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
 
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
index c175640..9b272d9 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
@@ -65,8 +65,6 @@
     protected PackageUserState mPkgUserStateInstalled;
     protected CancellationSignal mCancellationSignal;
 
-    protected PrimaryDexOptimizer mPrimaryDexOptimizer;
-
     @Before
     public void setUp() throws Exception {
         lenient().when(mInjector.getArtd()).thenReturn(mArtd);
@@ -103,8 +101,6 @@
         mPkgState = createPackageState();
         mPkg = mPkgState.getAndroidPackage();
         mCancellationSignal = new CancellationSignal();
-
-        mPrimaryDexOptimizer = new PrimaryDexOptimizer(mInjector);
     }
 
     private AndroidPackage createPackage() {