Support shared library compilation.

Bug: 254487975
Test: atest ArtServiceTests
Test: adb shell pm art optimize-package -m speed-profile -f --include-dependencies com.android.chrome
Ignore-AOSP-First: ART Services
Change-Id: I38b1acdf109f336ee6617cd6f22d30839dd070d8
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 7b85131..1685174 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -48,6 +48,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executors;
 
 /**
  * This class provides a system API for functionality provided by the ART module.
@@ -262,15 +263,15 @@
             throw new IllegalArgumentException("Nothing to optimize");
         }
 
-        PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
-        AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
-
-        try {
-            return mInjector.getDexOptHelper().dexopt(snapshot, pkgState, pkg, params,
-                    cancellationSignal);
-        } catch (RemoteException e) {
-            throw new IllegalStateException("An error occurred when calling artd", e);
+        if ((params.getFlags() & ArtFlags.FLAG_FOR_PRIMARY_DEX) == 0
+                && (params.getFlags() & ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES) != 0) {
+            throw new IllegalArgumentException(
+                    "FLAG_SHOULD_INCLUDE_DEPENDENCIES must not set if FLAG_FOR_PRIMARY_DEX is not "
+                    + "set.");
         }
+
+        return mInjector.getDexOptHelper().dexopt(
+                snapshot, List.of(packageName), params, cancellationSignal, Runnable::run);
     }
 
     /**
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index 2c90fb6..0e0d80c 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -95,6 +95,10 @@
                                         ArtFlags.FLAG_FOR_PRIMARY_DEX
                                                 | ArtFlags.FLAG_FOR_SECONDARY_DEX);
                                 break;
+                            case "--include-dependencies":
+                                paramsBuilder.setFlags(ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
+                                        ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES);
+                                break;
                             default:
                                 pw.println("Error: Unknown option: " + opt);
                                 return 1;
@@ -232,6 +236,7 @@
         pw.println("      -m Set the compiler filter.");
         pw.println("      -f Force compilation.");
         pw.println("      --secondary-dex Only compile secondary dex.");
+        pw.println("      --include-dependencies Include dependencies.");
         pw.println("  cancel JOB_ID");
         pw.println("    Cancel a job.");
         pw.println("  dex-use-notify PACKAGE_NAME DEX_PATH CLASS_LOADER_CONTEXT");
diff --git a/libartservice/service/java/com/android/server/art/DexOptHelper.java b/libartservice/service/java/com/android/server/art/DexOptHelper.java
index bf84e48..b30e25d 100644
--- a/libartservice/service/java/com/android/server/art/DexOptHelper.java
+++ b/libartservice/service/java/com/android/server/art/DexOptHelper.java
@@ -35,10 +35,21 @@
 import com.android.server.pm.PackageManagerLocal;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.PackageState;
+import com.android.server.pm.pkg.SharedLibrary;
 
 import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 /**
  * A helper class to handle dexopt.
@@ -51,10 +62,10 @@
     private static final String TAG = "DexoptHelper";
 
     /**
-     * Timeout of the wake lock. This is required by AndroidLint, but we set it to a value larger
-     * than artd's {@code kLongTimeoutSec} so that it should normally never triggered.
+     * Timeout of the wake lock. This is required by AndroidLint, but we set it to a very large
+     * value so that it should normally never triggered.
      */
-    private static final int WAKE_LOCK_TIMEOUT_MS = 11 * 60 * 1000; // 11 minutes.
+    private static final long WAKE_LOCK_TIMEOUT_MS = TimeUnit.DAYS.toMillis(1);
 
     @NonNull private final Injector mInjector;
 
@@ -74,21 +85,24 @@
      */
     @NonNull
     public OptimizeResult dexopt(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
-            @NonNull PackageState pkgState, @NonNull AndroidPackage pkg,
-            @NonNull OptimizeParams params, @NonNull CancellationSignal cancellationSignal)
-            throws RemoteException {
-        List<DexContainerFileOptimizeResult> results = new ArrayList<>();
-        Supplier<OptimizeResult> createResult = ()
-                -> new OptimizeResult(params.getCompilerFilter(), params.getReason(),
-                        List.of(new PackageOptimizeResult(pkgState.getPackageName(), results)));
-        Supplier<Boolean> hasCancelledResult = ()
-                -> results.stream().anyMatch(
-                        result -> result.getStatus() == OptimizeResult.OPTIMIZE_CANCELLED);
+            @NonNull List<String> packageNames, @NonNull OptimizeParams params,
+            @NonNull CancellationSignal cancellationSignal, @NonNull Executor executor) {
+        return dexoptPackages(
+                getPackageStates(snapshot, packageNames,
+                        (params.getFlags() & ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES) != 0),
+                params, cancellationSignal, executor);
+    }
 
-        if (!canOptimizePackage(pkgState, pkg)) {
-            return createResult.get();
-        }
-
+    /**
+     * DO NOT use this method directly. Use {@link
+     * ArtManagerLocal#optimizePackage(PackageManagerLocal.FilteredSnapshot, String,
+     * OptimizeParams)}.
+     */
+    @NonNull
+    private OptimizeResult dexoptPackages(@NonNull List<PackageState> pkgStates,
+            @NonNull OptimizeParams params, @NonNull CancellationSignal cancellationSignal,
+            @NonNull Executor executor) {
+        int callingUid = Binder.getCallingUid();
         long identityToken = Binder.clearCallingIdentity();
         PowerManager.WakeLock wakeLock = null;
 
@@ -96,39 +110,70 @@
             // Acquire a wake lock.
             PowerManager powerManager = mInjector.getPowerManager();
             wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
-            wakeLock.setWorkSource(new WorkSource(pkgState.getAppId()));
+            wakeLock.setWorkSource(new WorkSource(callingUid));
             wakeLock.acquire(WAKE_LOCK_TIMEOUT_MS);
 
-            if ((params.getFlags() & ArtFlags.FLAG_FOR_PRIMARY_DEX) != 0) {
-                results.addAll(
-                        mInjector.getPrimaryDexOptimizer(pkgState, pkg, params, cancellationSignal)
-                                .dexopt());
-                if (hasCancelledResult.get()) {
-                    return createResult.get();
-                }
+            List<Future<PackageOptimizeResult>> futures = new ArrayList<>();
+            for (PackageState pkgState : pkgStates) {
+                futures.add(Utils.execute(
+                        executor, () -> dexoptPackage(pkgState, params, cancellationSignal)));
             }
 
-            if ((params.getFlags() & ArtFlags.FLAG_FOR_SECONDARY_DEX) != 0) {
-                results.addAll(
-                        mInjector
-                                .getSecondaryDexOptimizer(pkgState, pkg, params, cancellationSignal)
-                                .dexopt());
-                if (hasCancelledResult.get()) {
-                    return createResult.get();
-                }
-            }
+            List<PackageOptimizeResult> results =
+                    futures.stream().map(Utils::getFuture).collect(Collectors.toList());
 
-            if ((params.getFlags() & ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES) != 0) {
-                // TODO(jiakaiz): Implement this.
-                throw new UnsupportedOperationException(
-                        "Optimizing dependencies is not implemented yet");
-            }
+            return new OptimizeResult(params.getCompilerFilter(), params.getReason(), results);
         } finally {
             if (wakeLock != null) {
                 wakeLock.release();
             }
             Binder.restoreCallingIdentity(identityToken);
         }
+    }
+
+    /**
+     * DO NOT use this method directly. Use {@link
+     * ArtManagerLocal#optimizePackage(PackageManagerLocal.FilteredSnapshot, String,
+     * OptimizeParams)}.
+     */
+    @NonNull
+    private PackageOptimizeResult dexoptPackage(@NonNull PackageState pkgState,
+            @NonNull OptimizeParams params, @NonNull CancellationSignal cancellationSignal) {
+        List<DexContainerFileOptimizeResult> results = new ArrayList<>();
+        Supplier<PackageOptimizeResult> createResult = ()
+                -> new PackageOptimizeResult(
+                        pkgState.getPackageName(), results, cancellationSignal.isCanceled());
+
+        AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
+
+        if (!canOptimizePackage(pkgState, pkg)) {
+            return createResult.get();
+        }
+
+        try {
+            if ((params.getFlags() & ArtFlags.FLAG_FOR_PRIMARY_DEX) != 0) {
+                if (cancellationSignal.isCanceled()) {
+                    return createResult.get();
+                }
+
+                results.addAll(
+                        mInjector.getPrimaryDexOptimizer(pkgState, pkg, params, cancellationSignal)
+                                .dexopt());
+            }
+
+            if ((params.getFlags() & ArtFlags.FLAG_FOR_SECONDARY_DEX) != 0) {
+                if (cancellationSignal.isCanceled()) {
+                    return createResult.get();
+                }
+
+                results.addAll(
+                        mInjector
+                                .getSecondaryDexOptimizer(pkgState, pkg, params, cancellationSignal)
+                                .dexopt());
+            }
+        } catch (RemoteException e) {
+            throw new IllegalStateException("An error occurred when calling artd", e);
+        }
 
         return createResult.get();
     }
@@ -149,6 +194,57 @@
         return true;
     }
 
+    @NonNull
+    private List<PackageState> getPackageStates(
+            @NonNull PackageManagerLocal.FilteredSnapshot snapshot,
+            @NonNull List<String> packageNames, boolean includeDependencies) {
+        var pkgStates = new LinkedHashMap<String, PackageState>();
+        Set<String> visitedLibraries = new HashSet<>();
+        Queue<SharedLibrary> queue = new LinkedList<>();
+
+        Consumer<SharedLibrary> maybeEnqueue = library -> {
+            // The package name is not null if the library is an APK.
+            // TODO(jiakaiz): Support JAR libraries.
+            if (library.getPackageName() != null && !visitedLibraries.contains(library.getName())) {
+                visitedLibraries.add(library.getName());
+                queue.add(library);
+            }
+        };
+
+        for (String packageName : packageNames) {
+            PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
+            AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
+            pkgStates.put(packageName, pkgState);
+            if (includeDependencies && canOptimizePackage(pkgState, pkg)) {
+                for (SharedLibrary library : pkgState.getUsesLibraries()) {
+                    maybeEnqueue.accept(library);
+                }
+            }
+        }
+
+        SharedLibrary library;
+        while ((library = queue.poll()) != null) {
+            String packageName = library.getPackageName();
+            PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
+            AndroidPackage pkg = pkgState.getAndroidPackage();
+            if (pkg != null && canOptimizePackage(pkgState, pkg)) {
+                pkgStates.put(packageName, pkgState);
+
+                // Note that `library.getDependencies()` is different from
+                // `pkgState.getUsesLibraries()`. Different libraries can belong to the same
+                // package. `pkgState.getUsesLibraries()` returns a union of dependencies of
+                // libraries that belong to the same package, which is not what we want here.
+                // Therefore, this loop cannot be unified with the one above.
+                for (SharedLibrary dep : library.getDependencies()) {
+                    maybeEnqueue.accept(dep);
+                }
+            }
+        }
+
+        // `LinkedHashMap` guarantees deterministic order.
+        return new ArrayList<>(pkgStates.values());
+    }
+
     /**
      * Injector pattern for testing purpose.
      *
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index 8136b0a..e4060c1 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -37,6 +37,11 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
 import java.util.stream.Collectors;
 
 /** @hide */
@@ -206,6 +211,26 @@
         return str;
     }
 
+    public static <T> Future<T> execute(@NonNull Executor executor, @NonNull Callable<T> callable) {
+        var future = new FutureTask<T>(callable);
+        executor.execute(future);
+        return future;
+    }
+
+    public static <T> T getFuture(Future<T> future) {
+        try {
+            return future.get();
+        } catch (ExecutionException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof RuntimeException) {
+                throw (RuntimeException) cause;
+            }
+            throw new RuntimeException(cause);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     @AutoValue
     public abstract static class Abi {
         static @NonNull Abi create(
diff --git a/libartservice/service/java/com/android/server/art/model/OptimizeResult.java b/libartservice/service/java/com/android/server/art/model/OptimizeResult.java
index 5ee5016..6714f9a 100644
--- a/libartservice/service/java/com/android/server/art/model/OptimizeResult.java
+++ b/libartservice/service/java/com/android/server/art/model/OptimizeResult.java
@@ -112,13 +112,16 @@
         private final @NonNull String mPackageName;
         private final
                 @NonNull List<DexContainerFileOptimizeResult> mDexContainerFileOptimizeResults;
+        private final boolean mIsCanceled;
 
         /** @hide */
-        public PackageOptimizeResult(@NonNull String packageName,
-                @NonNull List<DexContainerFileOptimizeResult> dexContainerFileOptimizeResults) {
-            mPackageName = packageName;
-            mDexContainerFileOptimizeResults = dexContainerFileOptimizeResults;
-        }
+    public PackageOptimizeResult(@NonNull String packageName,
+            @NonNull List<DexContainerFileOptimizeResult> dexContainerFileOptimizeResults,
+            boolean isCanceled) {
+        mPackageName = packageName;
+        mDexContainerFileOptimizeResults = dexContainerFileOptimizeResults;
+        mIsCanceled = isCanceled;
+    }
 
         /** The package name. */
         public @NonNull String getPackageName() {
@@ -136,10 +139,11 @@
 
         /** The overall status of the package. */
         public @OptimizeStatus int getStatus() {
-            return mDexContainerFileOptimizeResults.stream()
-                    .mapToInt(result -> result.getStatus())
-                    .max()
-                    .orElse(OPTIMIZE_SKIPPED);
+            return mIsCanceled ? OPTIMIZE_CANCELLED
+                               : mDexContainerFileOptimizeResults.stream()
+                                         .mapToInt(result -> result.getStatus())
+                                         .max()
+                                         .orElse(OPTIMIZE_SKIPPED);
         }
     }
 
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index 0dfc084..e9a2beb 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -17,6 +17,7 @@
 package com.android.server.art;
 
 import static com.android.server.art.model.OptimizationStatus.DexContainerFileOptimizationStatus;
+import static com.android.server.art.testing.TestingUtils.deepEq;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -277,8 +278,8 @@
         var result = mock(OptimizeResult.class);
         var cancellationSignal = new CancellationSignal();
 
-        when(mDexOptHelper.dexopt(any(), same(mPkgState), same(mPkg), same(params),
-                same(cancellationSignal)))
+        when(mDexOptHelper.dexopt(any(), deepEq(List.of(PKG_NAME)), same(params),
+                     same(cancellationSignal), any()))
                 .thenReturn(result);
 
         assertThat(mArtManagerLocal.optimizePackage(mSnapshot,
@@ -286,22 +287,6 @@
                 .isSameInstanceAs(result);
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testOptimizePackagePackageNotFound() throws Exception {
-        when(mSnapshot.getPackageState(anyString())).thenReturn(null);
-
-        mArtManagerLocal.optimizePackage(mSnapshot, PKG_NAME,
-                new OptimizeParams.Builder("install").build());
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testOptimizePackageNoPackage() throws Exception {
-        lenient().when(mPkgState.getAndroidPackage()).thenReturn(null);
-
-        mArtManagerLocal.optimizePackage(mSnapshot, PKG_NAME,
-                new OptimizeParams.Builder("install").build());
-    }
-
     private AndroidPackage createPackage() {
         AndroidPackage pkg = mock(AndroidPackage.class);
 
diff --git a/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java b/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
index b8faf09..f71f46d 100644
--- a/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
@@ -27,7 +27,9 @@
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -38,16 +40,16 @@
 
 import androidx.test.filters.SmallTest;
 
+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.art.testing.OnSuccessRule;
 import com.android.server.pm.PackageManagerLocal;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.AndroidPackageSplit;
 import com.android.server.pm.pkg.PackageState;
+import com.android.server.pm.pkg.SharedLibrary;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InOrder;
@@ -55,37 +57,46 @@
 import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
 
 @SmallTest
 @RunWith(MockitoJUnitRunner.StrictStubs.class)
 public class DexOptHelperTest {
-    private static final String PKG_NAME = "com.example.foo";
+    private static final String PKG_NAME_FOO = "com.example.foo";
+    private static final String PKG_NAME_BAR = "com.example.bar";
+    private static final String PKG_NAME_LIB1 = "com.example.lib1";
+    private static final String PKG_NAME_LIB2 = "com.example.lib2";
+    private static final String PKG_NAME_LIB3 = "com.example.lib3";
+    private static final String PKG_NAME_LIB4 = "com.example.lib4";
+    private static final String PKG_NAME_LIBBAZ = "com.example.libbaz";
 
     @Mock private DexOptHelper.Injector mInjector;
     @Mock private PrimaryDexOptimizer mPrimaryDexOptimizer;
+    @Mock private SecondaryDexOptimizer mSecondaryDexOptimizer;
     @Mock private AppHibernationManager mAhm;
     @Mock private PowerManager mPowerManager;
     @Mock private PowerManager.WakeLock mWakeLock;
-    private PackageState mPkgState;
-    private AndroidPackage mPkg;
+    @Mock private PackageManagerLocal.FilteredSnapshot mSnapshot;
+    private PackageState mPkgStateFoo;
+    private PackageState mPkgStateBar;
+    private PackageState mPkgStateLib1;
+    private PackageState mPkgStateLib2;
+    private PackageState mPkgStateLib4;
+    private PackageState mPkgStateLibbaz;
+    private AndroidPackage mPkgFoo;
+    private AndroidPackage mPkgBar;
+    private AndroidPackage mPkgLib1;
+    private AndroidPackage mPkgLib2;
+    private AndroidPackage mPkgLib4;
+    private AndroidPackage mPkgLibbaz;
     private CancellationSignal mCancellationSignal;
-
-    @Rule
-    public OnSuccessRule onSuccessRule = new OnSuccessRule(() -> {
-        // Don't do this on failure because it will make the failure hard to understand.
-        verifyNoMoreInteractions(mPrimaryDexOptimizer);
-    });
-
-    private final OptimizeParams mParams =
-            new OptimizeParams.Builder("install").setCompilerFilter("speed-profile").build();
-    private final List<DexContainerFileOptimizeResult> mPrimaryResults = List.of(
-            new DexContainerFileOptimizeResult("/data/app/foo/base.apk", true /* isPrimaryAbi */,
-                    "arm64-v8a", "verify", OptimizeResult.OPTIMIZE_PERFORMED,
-                    100 /* dex2oatWallTimeMillis */, 400 /* dex2oatCpuTimeMillis */),
-            new DexContainerFileOptimizeResult("/data/app/foo/base.apk", false /* isPrimaryAbi */,
-                    "armeabi-v7a", "verify", OptimizeResult.OPTIMIZE_FAILED,
-                    100 /* dex2oatWallTimeMillis */, 400 /* dex2oatCpuTimeMillis */));
-
+    private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+    private List<DexContainerFileOptimizeResult> mPrimaryResults;
+    private List<DexContainerFileOptimizeResult> mSecondaryResults;
+    private OptimizeParams mParams;
+    private List<String> mRequestedPackages;
     private DexOptHelper mDexOptHelper;
 
     @Before
@@ -97,84 +108,283 @@
                 .when(mPowerManager.newWakeLock(eq(PowerManager.PARTIAL_WAKE_LOCK), any()))
                 .thenReturn(mWakeLock);
 
-        lenient().when(mAhm.isHibernatingGlobally(PKG_NAME)).thenReturn(false);
+        lenient().when(mAhm.isHibernatingGlobally(any())).thenReturn(false);
         lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(true);
 
-        mPkgState = createPackageState();
-        mPkg = mPkgState.getAndroidPackage();
         mCancellationSignal = new CancellationSignal();
 
+        preparePackagesAndLibraries();
+
+        mPrimaryResults = createResults("/data/app/foo/base.apk", false /* partialFailure */);
+        mSecondaryResults =
+                createResults("/data/user_de/0/foo/foo.apk", false /* partialFailure */);
+
         lenient()
-                .when(mInjector.getPrimaryDexOptimizer(
-                        same(mPkgState), same(mPkg), same(mParams), same(mCancellationSignal)))
+                .when(mInjector.getPrimaryDexOptimizer(any(), any(), any(), any()))
                 .thenReturn(mPrimaryDexOptimizer);
+        lenient().when(mPrimaryDexOptimizer.dexopt()).thenReturn(mPrimaryResults);
+
+        lenient()
+                .when(mInjector.getSecondaryDexOptimizer(any(), any(), any(), any()))
+                .thenReturn(mSecondaryDexOptimizer);
+        lenient().when(mSecondaryDexOptimizer.dexopt()).thenReturn(mSecondaryResults);
+
+        mParams = new OptimizeParams.Builder("install")
+                          .setCompilerFilter("speed-profile")
+                          .setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX
+                                          | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
+                                  ArtFlags.FLAG_FOR_SECONDARY_DEX
+                                          | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
+                          .build();
 
         mDexOptHelper = new DexOptHelper(mInjector);
     }
 
     @Test
     public void testDexopt() throws Exception {
-        when(mPrimaryDexOptimizer.dexopt()).thenReturn(mPrimaryResults);
+        // Only package libbaz fails.
+        var failingPrimaryDexOptimizer = mock(PrimaryDexOptimizer.class);
+        List<DexContainerFileOptimizeResult> partialFailureResults =
+                createResults("/data/app/foo/base.apk", true /* partialFailure */);
+        lenient().when(failingPrimaryDexOptimizer.dexopt()).thenReturn(partialFailureResults);
+        when(mInjector.getPrimaryDexOptimizer(same(mPkgStateLibbaz), any(), any(), any()))
+                .thenReturn(failingPrimaryDexOptimizer);
 
         OptimizeResult result = mDexOptHelper.dexopt(
-                mock(PackageManagerLocal.FilteredSnapshot.class), mPkgState, mPkg, mParams,
-                mCancellationSignal);
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
 
         assertThat(result.getRequestedCompilerFilter()).isEqualTo("speed-profile");
         assertThat(result.getReason()).isEqualTo("install");
         assertThat(result.getFinalStatus()).isEqualTo(OptimizeResult.OPTIMIZE_FAILED);
-        assertThat(result.getPackageOptimizeResults()).hasSize(1);
 
-        PackageOptimizeResult packageResult = result.getPackageOptimizeResults().get(0);
-        assertThat(packageResult.getPackageName()).isEqualTo(PKG_NAME);
-        assertThat(packageResult.getDexContainerFileOptimizeResults())
-                .containsExactlyElementsIn(mPrimaryResults);
+        // The requested packages must come first.
+        assertThat(result.getPackageOptimizeResults()).hasSize(6);
+        checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, OptimizeResult.OPTIMIZE_FAILED,
+                List.of(partialFailureResults, mSecondaryResults));
+        checkPackageResult(result, 3 /* index */, PKG_NAME_LIB1, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 4 /* index */, PKG_NAME_LIB2, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 5 /* index */, PKG_NAME_LIB4, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
 
-        InOrder inOrder = inOrder(mPrimaryDexOptimizer, mWakeLock);
+        // The order matters. It should acquire the wake lock only once, at the beginning, and
+        // release the wake lock at the end. When running in a single thread, it should dexopt
+        // primary dex files and the secondary dex files together for each package, and it should
+        // dexopt requested packages, in the given order, and then dexopt dependencies.
+        InOrder inOrder = inOrder(mInjector, mWakeLock);
+        inOrder.verify(mWakeLock).setWorkSource(any());
         inOrder.verify(mWakeLock).acquire(anyLong());
-        inOrder.verify(mPrimaryDexOptimizer).dexopt();
+        inOrder.verify(mInjector).getPrimaryDexOptimizer(
+                same(mPkgStateFoo), same(mPkgFoo), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getSecondaryDexOptimizer(
+                same(mPkgStateFoo), same(mPkgFoo), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getPrimaryDexOptimizer(
+                same(mPkgStateBar), same(mPkgBar), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getSecondaryDexOptimizer(
+                same(mPkgStateBar), same(mPkgBar), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getPrimaryDexOptimizer(
+                same(mPkgStateLibbaz), same(mPkgLibbaz), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getSecondaryDexOptimizer(
+                same(mPkgStateLibbaz), same(mPkgLibbaz), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getPrimaryDexOptimizer(
+                same(mPkgStateLib1), same(mPkgLib1), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getSecondaryDexOptimizer(
+                same(mPkgStateLib1), same(mPkgLib1), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getPrimaryDexOptimizer(
+                same(mPkgStateLib2), same(mPkgLib2), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getSecondaryDexOptimizer(
+                same(mPkgStateLib2), same(mPkgLib2), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getPrimaryDexOptimizer(
+                same(mPkgStateLib4), same(mPkgLib4), same(mParams), same(mCancellationSignal));
+        inOrder.verify(mInjector).getSecondaryDexOptimizer(
+                same(mPkgStateLib4), same(mPkgLib4), same(mParams), same(mCancellationSignal));
         inOrder.verify(mWakeLock).release();
+
+        verifyNoMoreDexopt(6 /* expectedPrimaryTimes */, 6 /* expectedSecondaryTimes */);
+
+        verifyNoMoreInteractions(mWakeLock);
+    }
+
+    @Test
+    public void testDexoptNoDependencies() throws Exception {
+        mParams = new OptimizeParams.Builder("install")
+                          .setCompilerFilter("speed-profile")
+                          .setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX,
+                                  ArtFlags.FLAG_FOR_SECONDARY_DEX
+                                          | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
+                          .build();
+
+        OptimizeResult result = mDexOptHelper.dexopt(
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
+
+        assertThat(result.getPackageOptimizeResults()).hasSize(3);
+        checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ,
+                OptimizeResult.OPTIMIZE_PERFORMED, List.of(mPrimaryResults, mSecondaryResults));
+
+        verifyNoMoreDexopt(3 /* expectedPrimaryTimes */, 3 /* expectedSecondaryTimes */);
+    }
+
+    @Test
+    public void testDexoptPrimaryOnly() throws Exception {
+        mParams = new OptimizeParams.Builder("install")
+                          .setCompilerFilter("speed-profile")
+                          .setFlags(ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
+                                  ArtFlags.FLAG_FOR_SECONDARY_DEX
+                                          | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
+                          .build();
+
+        OptimizeResult result = mDexOptHelper.dexopt(
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
+
+        assertThat(result.getPackageOptimizeResults()).hasSize(6);
+        checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults));
+        checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults));
+        checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ,
+                OptimizeResult.OPTIMIZE_PERFORMED, List.of(mPrimaryResults));
+        checkPackageResult(result, 3 /* index */, PKG_NAME_LIB1, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults));
+        checkPackageResult(result, 4 /* index */, PKG_NAME_LIB2, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults));
+        checkPackageResult(result, 5 /* index */, PKG_NAME_LIB4, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults));
+
+        verifyNoMoreDexopt(6 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
+    }
+
+    @Test
+    public void testDexoptPrimaryOnlyNoDependencies() throws Exception {
+        mParams = new OptimizeParams.Builder("install")
+                          .setCompilerFilter("speed-profile")
+                          .setFlags(0,
+                                  ArtFlags.FLAG_FOR_SECONDARY_DEX
+                                          | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
+                          .build();
+
+        OptimizeResult result = mDexOptHelper.dexopt(
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
+
+        assertThat(result.getPackageOptimizeResults()).hasSize(3);
+        checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults));
+        checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults));
+        checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ,
+                OptimizeResult.OPTIMIZE_PERFORMED, List.of(mPrimaryResults));
+
+        verifyNoMoreDexopt(3 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
+    }
+
+    @Test
+    public void testDexoptCancelledBetweenDex2oatInvocations() throws Exception {
+        when(mPrimaryDexOptimizer.dexopt()).thenAnswer(invocation -> {
+            mCancellationSignal.cancel();
+            return mPrimaryResults;
+        });
+
+        OptimizeResult result = mDexOptHelper.dexopt(
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
+
+        assertThat(result.getFinalStatus()).isEqualTo(OptimizeResult.OPTIMIZE_CANCELLED);
+
+        assertThat(result.getPackageOptimizeResults()).hasSize(6);
+        checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, OptimizeResult.OPTIMIZE_CANCELLED,
+                List.of(mPrimaryResults));
+        checkPackageResult(
+                result, 1 /* index */, PKG_NAME_BAR, OptimizeResult.OPTIMIZE_CANCELLED, List.of());
+        checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ,
+                OptimizeResult.OPTIMIZE_CANCELLED, List.of());
+        checkPackageResult(
+                result, 3 /* index */, PKG_NAME_LIB1, OptimizeResult.OPTIMIZE_CANCELLED, List.of());
+        checkPackageResult(
+                result, 4 /* index */, PKG_NAME_LIB2, OptimizeResult.OPTIMIZE_CANCELLED, List.of());
+        checkPackageResult(
+                result, 5 /* index */, PKG_NAME_LIB4, OptimizeResult.OPTIMIZE_CANCELLED, List.of());
+
+        verify(mInjector).getPrimaryDexOptimizer(
+                same(mPkgStateFoo), same(mPkgFoo), same(mParams), same(mCancellationSignal));
+
+        verifyNoMoreDexopt(1 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
     }
 
     @Test
     public void testDexoptNoCode() throws Exception {
-        when(mPkg.getSplits().get(0).isHasCode()).thenReturn(false);
+        when(mPkgFoo.getSplits().get(0).isHasCode()).thenReturn(false);
 
+        mRequestedPackages = List.of(PKG_NAME_FOO);
         OptimizeResult result = mDexOptHelper.dexopt(
-                mock(PackageManagerLocal.FilteredSnapshot.class), mPkgState, mPkg, mParams,
-                mCancellationSignal);
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
 
         assertThat(result.getFinalStatus()).isEqualTo(OptimizeResult.OPTIMIZE_SKIPPED);
-        assertThat(result.getPackageOptimizeResults().get(0).getDexContainerFileOptimizeResults())
-                .isEmpty();
+        assertThat(result.getPackageOptimizeResults()).hasSize(1);
+        checkPackageResult(
+                result, 0 /* index */, PKG_NAME_FOO, OptimizeResult.OPTIMIZE_SKIPPED, List.of());
+
+        verifyNoDexopt();
+    }
+
+    @Test
+    public void testDexoptLibraryNoCode() throws Exception {
+        when(mPkgLib1.getSplits().get(0).isHasCode()).thenReturn(false);
+
+        mRequestedPackages = List.of(PKG_NAME_FOO);
+        OptimizeResult result = mDexOptHelper.dexopt(
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
+
+        assertThat(result.getFinalStatus()).isEqualTo(OptimizeResult.OPTIMIZE_PERFORMED);
+        assertThat(result.getPackageOptimizeResults()).hasSize(1);
+        checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+
+        verifyNoMoreDexopt(1 /* expectedPrimaryTimes */, 1 /* expectedSecondaryTimes */);
     }
 
     @Test
     public void testDexoptIsHibernating() throws Exception {
-        lenient().when(mAhm.isHibernatingGlobally(PKG_NAME)).thenReturn(true);
+        lenient().when(mAhm.isHibernatingGlobally(PKG_NAME_FOO)).thenReturn(true);
 
+        mRequestedPackages = List.of(PKG_NAME_FOO);
         OptimizeResult result = mDexOptHelper.dexopt(
-                mock(PackageManagerLocal.FilteredSnapshot.class), mPkgState, mPkg, mParams,
-                mCancellationSignal);
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
 
         assertThat(result.getFinalStatus()).isEqualTo(OptimizeResult.OPTIMIZE_SKIPPED);
-        assertThat(result.getPackageOptimizeResults().get(0).getDexContainerFileOptimizeResults())
-                .isEmpty();
+        checkPackageResult(
+                result, 0 /* index */, PKG_NAME_FOO, OptimizeResult.OPTIMIZE_SKIPPED, List.of());
+
+        verifyNoDexopt();
     }
 
     @Test
     public void testDexoptIsHibernatingButOatArtifactDeletionDisabled() throws Exception {
-        lenient().when(mAhm.isHibernatingGlobally(PKG_NAME)).thenReturn(true);
+        lenient().when(mAhm.isHibernatingGlobally(PKG_NAME_FOO)).thenReturn(true);
         lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(false);
 
-        when(mPrimaryDexOptimizer.dexopt()).thenReturn(mPrimaryResults);
-
         OptimizeResult result = mDexOptHelper.dexopt(
-                mock(PackageManagerLocal.FilteredSnapshot.class), mPkgState, mPkg, mParams,
-                mCancellationSignal);
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
 
-        assertThat(result.getPackageOptimizeResults().get(0).getDexContainerFileOptimizeResults())
-                .containsExactlyElementsIn(mPrimaryResults);
+        assertThat(result.getPackageOptimizeResults()).hasSize(6);
+        checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ,
+                OptimizeResult.OPTIMIZE_PERFORMED, List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 3 /* index */, PKG_NAME_LIB1, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 4 /* index */, PKG_NAME_LIB2, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
+        checkPackageResult(result, 5 /* index */, PKG_NAME_LIB4, OptimizeResult.OPTIMIZE_PERFORMED,
+                List.of(mPrimaryResults, mSecondaryResults));
     }
 
     @Test
@@ -182,14 +392,34 @@
         when(mPrimaryDexOptimizer.dexopt()).thenThrow(IllegalStateException.class);
 
         try {
-            mDexOptHelper.dexopt(mock(PackageManagerLocal.FilteredSnapshot.class), mPkgState, mPkg,
-                    mParams, mCancellationSignal);
+            mDexOptHelper.dexopt(
+                    mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
         } catch (Exception ignored) {
         }
 
         verify(mWakeLock).release();
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testDexoptPackageNotFound() throws Exception {
+        when(mSnapshot.getPackageState(any())).thenReturn(null);
+
+        mDexOptHelper.dexopt(
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
+
+        verifyNoDexopt();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testDexoptNoPackage() throws Exception {
+        lenient().when(mPkgStateFoo.getAndroidPackage()).thenReturn(null);
+
+        mDexOptHelper.dexopt(
+                mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
+
+        verifyNoDexopt();
+    }
+
     private AndroidPackage createPackage() {
         AndroidPackage pkg = mock(AndroidPackage.class);
         var baseSplit = mock(AndroidPackageSplit.class);
@@ -198,12 +428,109 @@
         return pkg;
     }
 
-    private PackageState createPackageState() {
+    private PackageState createPackageState(String packageName, List<SharedLibrary> deps) {
         PackageState pkgState = mock(PackageState.class);
-        lenient().when(pkgState.getPackageName()).thenReturn(PKG_NAME);
+        lenient().when(pkgState.getPackageName()).thenReturn(packageName);
         lenient().when(pkgState.getAppId()).thenReturn(12345);
+        lenient().when(pkgState.getUsesLibraries()).thenReturn(deps);
         AndroidPackage pkg = createPackage();
         lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
         return pkgState;
     }
+
+    private SharedLibrary createLibrary(
+            String libraryName, String packageName, List<SharedLibrary> deps) {
+        SharedLibrary library = mock(SharedLibrary.class);
+        lenient().when(library.getName()).thenReturn(libraryName);
+        lenient().when(library.getPackageName()).thenReturn(packageName);
+        lenient().when(library.getDependencies()).thenReturn(deps);
+        return library;
+    }
+
+    private void preparePackagesAndLibraries() {
+        // Dependency graph:
+        //                foo                bar
+        //                 |                  |
+        //            lib1a (lib1)       lib1b (lib1)       lib1c (lib1)
+        //               /   \             /   \                  |
+        //              /     \           /     \                 |
+        //  libbaz (libbaz)    lib2 (lib2)    lib4 (lib4)    lib3 (lib3)
+        //
+        // "lib1a", "lib1b", and "lib1c" belong to the same package "lib1".
+
+        mRequestedPackages = List.of(PKG_NAME_FOO, PKG_NAME_BAR, PKG_NAME_LIBBAZ);
+
+        SharedLibrary libbaz = createLibrary("libbaz", PKG_NAME_LIBBAZ, List.of());
+        SharedLibrary lib4 = createLibrary("lib4", PKG_NAME_LIB4, List.of());
+        SharedLibrary lib3 = createLibrary("lib3", PKG_NAME_LIB3, List.of());
+        SharedLibrary lib2 = createLibrary("lib2", PKG_NAME_LIB2, List.of());
+        SharedLibrary lib1a = createLibrary("lib1a", PKG_NAME_LIB1, List.of(libbaz, lib2));
+        SharedLibrary lib1b = createLibrary("lib1b", PKG_NAME_LIB1, List.of(lib2, lib4));
+        SharedLibrary lib1c = createLibrary("lib1c", PKG_NAME_LIB1, List.of(lib3));
+
+        mPkgStateFoo = createPackageState(PKG_NAME_FOO, List.of(lib1a));
+        mPkgFoo = mPkgStateFoo.getAndroidPackage();
+        lenient().when(mSnapshot.getPackageState(PKG_NAME_FOO)).thenReturn(mPkgStateFoo);
+
+        mPkgStateBar = createPackageState(PKG_NAME_BAR, List.of(lib1b));
+        mPkgBar = mPkgStateBar.getAndroidPackage();
+        lenient().when(mSnapshot.getPackageState(PKG_NAME_BAR)).thenReturn(mPkgStateBar);
+
+        mPkgStateLib1 = createPackageState(PKG_NAME_LIB1, List.of(libbaz, lib2, lib3, lib4));
+        mPkgLib1 = mPkgStateLib1.getAndroidPackage();
+        lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB1)).thenReturn(mPkgStateLib1);
+
+        mPkgStateLib2 = createPackageState(PKG_NAME_LIB2, List.of());
+        mPkgLib2 = mPkgStateLib2.getAndroidPackage();
+        lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB2)).thenReturn(mPkgStateLib2);
+
+        // This should not be considered as a transitive dependency of any requested package, even
+        // though it is a dependency of package "lib1".
+        PackageState pkgStateLib3 = createPackageState(PKG_NAME_LIB3, List.of());
+        lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB3)).thenReturn(pkgStateLib3);
+
+        mPkgStateLib4 = createPackageState(PKG_NAME_LIB4, List.of());
+        mPkgLib4 = mPkgStateLib4.getAndroidPackage();
+        lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB4)).thenReturn(mPkgStateLib4);
+
+        mPkgStateLibbaz = createPackageState(PKG_NAME_LIBBAZ, List.of());
+        mPkgLibbaz = mPkgStateLibbaz.getAndroidPackage();
+        lenient().when(mSnapshot.getPackageState(PKG_NAME_LIBBAZ)).thenReturn(mPkgStateLibbaz);
+    }
+
+    private void verifyNoDexopt() {
+        verify(mInjector, never()).getPrimaryDexOptimizer(any(), any(), any(), any());
+        verify(mInjector, never()).getSecondaryDexOptimizer(any(), any(), any(), any());
+    }
+
+    private void verifyNoMoreDexopt(int expectedPrimaryTimes, int expectedSecondaryTimes) {
+        verify(mInjector, times(expectedPrimaryTimes))
+                .getPrimaryDexOptimizer(any(), any(), any(), any());
+        verify(mInjector, times(expectedSecondaryTimes))
+                .getSecondaryDexOptimizer(any(), any(), any(), any());
+    }
+
+    private List<DexContainerFileOptimizeResult> createResults(
+            String dexPath, boolean partialFailure) {
+        return List.of(new DexContainerFileOptimizeResult(dexPath, true /* isPrimaryAbi */,
+                               "arm64-v8a", "verify", OptimizeResult.OPTIMIZE_PERFORMED,
+                               100 /* dex2oatWallTimeMillis */, 400 /* dex2oatCpuTimeMillis */),
+                new DexContainerFileOptimizeResult(dexPath, false /* isPrimaryAbi */, "armeabi-v7a",
+                        "verify",
+                        partialFailure ? OptimizeResult.OPTIMIZE_FAILED
+                                       : OptimizeResult.OPTIMIZE_PERFORMED,
+                        100 /* dex2oatWallTimeMillis */, 400 /* dex2oatCpuTimeMillis */));
+    }
+
+    private void checkPackageResult(OptimizeResult result, int index, String packageName,
+            @OptimizeResult.OptimizeStatus int status,
+            List<List<DexContainerFileOptimizeResult>> dexContainerFileOptimizeResults) {
+        PackageOptimizeResult packageResult = result.getPackageOptimizeResults().get(index);
+        assertThat(packageResult.getPackageName()).isEqualTo(packageName);
+        assertThat(packageResult.getStatus()).isEqualTo(status);
+        assertThat(packageResult.getDexContainerFileOptimizeResults())
+                .containsExactlyElementsIn(dexContainerFileOptimizeResults.stream()
+                                                   .flatMap(r -> r.stream())
+                                                   .collect(Collectors.toList()));
+    }
 }