Implement batch optimization.

Bug: 255518446
Bug: 244714876
Test: atest ArtServiceTests
Test: adb shell pm art optimize-packages bg-dexopt
Ignore-AOSP-First: ART Services.
Change-Id: I27fb4b009615f4f6b1284c30299a24ef956a78bf
diff --git a/libartservice/service/api/system-server-current.txt b/libartservice/service/api/system-server-current.txt
index f2b4819..1003030 100644
--- a/libartservice/service/api/system-server-current.txt
+++ b/libartservice/service/api/system-server-current.txt
@@ -4,6 +4,7 @@
   public final class ArtManagerLocal {
     ctor @Deprecated public ArtManagerLocal();
     ctor public ArtManagerLocal(@NonNull android.content.Context);
+    method public void clearOptimizePackagesCallback();
     method @NonNull public com.android.server.art.model.DeleteResult deleteOptimizedArtifacts(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String);
     method @NonNull public com.android.server.art.model.DeleteResult deleteOptimizedArtifacts(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, int);
     method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String);
@@ -12,6 +13,11 @@
     method public void notifyDexContainersLoaded(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull java.util.Map<java.lang.String,java.lang.String>);
     method @NonNull public com.android.server.art.model.OptimizeResult optimizePackage(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull com.android.server.art.model.OptimizeParams);
     method @NonNull public com.android.server.art.model.OptimizeResult optimizePackage(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull com.android.server.art.model.OptimizeParams, @NonNull android.os.CancellationSignal);
+    method public void setOptimizePackagesCallback(@NonNull java.util.concurrent.Executor, @NonNull com.android.server.art.ArtManagerLocal.OptimizePackagesCallback);
+  }
+
+  public static interface ArtManagerLocal.OptimizePackagesCallback {
+    method public void onOverrideBatchOptimizeParams(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull java.util.List<java.lang.String>, @NonNull com.android.server.art.model.BatchOptimizeParams.Builder);
   }
 
   public class ReasonMapping {
@@ -47,6 +53,17 @@
     field public static final int PRIORITY_INTERACTIVE_FAST = 80; // 0x50
   }
 
+  public abstract class BatchOptimizeParams {
+    method @NonNull public abstract com.android.server.art.model.OptimizeParams getOptimizeParams();
+    method @NonNull public abstract java.util.List<java.lang.String> getPackages();
+  }
+
+  public static final class BatchOptimizeParams.Builder {
+    method @NonNull public com.android.server.art.model.BatchOptimizeParams build();
+    method @NonNull public com.android.server.art.model.BatchOptimizeParams.Builder setOptimizeParams(@NonNull com.android.server.art.model.OptimizeParams);
+    method @NonNull public com.android.server.art.model.BatchOptimizeParams.Builder setPackages(@NonNull java.util.List<java.lang.String>);
+  }
+
   public class DeleteResult {
     method public long getFreedBytes();
   }
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 1685174..cc64f71 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -18,15 +18,19 @@
 
 import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo;
 import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo;
+import static com.android.server.art.ReasonMapping.BatchOptimizeReason;
 import static com.android.server.art.Utils.Abi;
 import static com.android.server.art.model.ArtFlags.DeleteFlags;
 import static com.android.server.art.model.ArtFlags.GetStatusFlags;
+import static com.android.server.art.model.Config.Callback;
 import static com.android.server.art.model.OptimizationStatus.DexContainerFileOptimizationStatus;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.apphibernation.AppHibernationManager;
 import android.content.Context;
 import android.os.Binder;
 import android.os.CancellationSignal;
@@ -37,6 +41,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.BatchOptimizeParams;
+import com.android.server.art.model.Config;
 import com.android.server.art.model.DeleteResult;
 import com.android.server.art.model.OptimizationStatus;
 import com.android.server.art.model.OptimizeParams;
@@ -46,8 +52,11 @@
 import com.android.server.pm.pkg.PackageState;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
 /**
@@ -275,6 +284,80 @@
     }
 
     /**
+     * Runs batch optimization for the given reason.
+     *
+     * This is called by ART Service automatically during boot / background dexopt.
+     *
+     * The list of packages and options are determined by {@code reason}, and can be overridden by
+     * {@link #setOptimizePackagesCallback(Executor, OptimizePackagesCallback)}.
+     *
+     * The optimization is done in a thread pool. The number of packages being optimized
+     * simultaneously can be configured by system property {@code pm.dexopt.<reason>.concurrency}
+     * (e.g., {@code pm.dexopt.bg-dexopt.concurrency=4}), and the number of threads for each {@code
+     * dex2oat} invocation can be configured by system property {@code dalvik.vm.*dex2oat-threads}
+     * (e.g., {@code dalvik.vm.background-dex2oat-threads=4}). I.e., the maximum number of
+     * concurrent threads is the product of the two system properties. Note that the physical core
+     * usage is always bound by {@code dalvik.vm.*dex2oat-cpu-set} regardless of the number of
+     * threads.
+     *
+     * @param snapshot the snapshot from {@link PackageManagerLocal} to operate on
+     * @param reason determines the default list of packages and options
+     * @param cancellationSignal provides the ability to cancel this operation
+     * @throws IllegalStateException if an internal error occurs, or the callback set by {@link
+     *         #setOptimizePackagesCallback(Executor, OptimizePackagesCallback)} provides invalid
+     *         params.
+     *
+     * @hide
+     */
+    @NonNull
+    public OptimizeResult optimizePackages(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
+            @NonNull @BatchOptimizeReason String reason,
+            @NonNull CancellationSignal cancellationSignal) {
+        List<String> defaultPackages =
+                Collections.unmodifiableList(getDefaultPackages(snapshot, reason));
+        OptimizeParams defaultOptimizeParams = new OptimizeParams.Builder(reason).build();
+        var builder = new BatchOptimizeParams.Builder(defaultPackages, defaultOptimizeParams);
+        Callback<OptimizePackagesCallback> callback =
+                mInjector.getConfig().getOptimizePackagesCallback();
+        if (callback != null) {
+            Utils.executeAndWait(callback.executor(), () -> {
+                callback.get().onOverrideBatchOptimizeParams(
+                        snapshot, reason, defaultPackages, builder);
+            });
+        }
+        BatchOptimizeParams params = builder.build();
+        Utils.check(params.getOptimizeParams().getReason().equals(reason));
+
+        return mInjector.getDexOptHelper().dexopt(snapshot, params.getPackages(),
+                params.getOptimizeParams(), cancellationSignal,
+                Executors.newFixedThreadPool(ReasonMapping.getConcurrencyForReason(reason)));
+    }
+
+    /**
+     * Overrides the default params for {@link
+     * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String). This method is thread-safe.
+     *
+     * This method gives users the opportunity to change the behavior of {@link
+     * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String)}, which is called by ART
+     * Service automatically during boot / background dexopt.
+     *
+     * If this method is not called, the default list of packages and options determined by {@code
+     * reason} will be used.
+     */
+    public void setOptimizePackagesCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull OptimizePackagesCallback callback) {
+        mInjector.getConfig().setOptimizePackagesCallback(executor, callback);
+    }
+
+    /**
+     * Clears the callback set by {@link #setOptimizePackagesCallback(Executor,
+     * OptimizePackagesCallback)}. This method is thread-safe.
+     */
+    public void clearOptimizePackagesCallback() {
+        mInjector.getConfig().clearOptimizePackagesCallback();
+    }
+
+    /**
      * Notifies ART Service that a list of dex container files have been loaded.
      *
      * ART Service uses this information to:
@@ -298,6 +381,40 @@
                 snapshot, loadingPackageName, classLoaderContextByDexContainerFile);
     }
 
+    @NonNull
+    private List<String> getDefaultPackages(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
+            @NonNull @BatchOptimizeReason String reason) {
+        var packages = new ArrayList<String>();
+        snapshot.forAllPackageStates((pkgState) -> {
+            if (Utils.canOptimizePackage(pkgState, mInjector.getAppHibernationManager())) {
+                packages.add(pkgState.getPackageName());
+            }
+        });
+        return packages;
+    }
+
+    public interface OptimizePackagesCallback {
+        /**
+         * Mutates {@code builder} to override the default params for {@link
+         * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String). It must ignore unknown
+         * reasons because more reasons may be added in the future.
+         *
+         * If {@code builder.setPackages} is not called, {@code defaultPackages} will be used as the
+         * list of packages to optimize.
+         *
+         * If {@code builder.setOptimizeParams} is not called, the default params built from {@code
+         * new OptimizeParams.Builder(reason)} will to used as the params for optimizing each
+         * package.
+         *
+         * Changing the reason is not allowed. Doing so will result in {@link IllegalStateException}
+         * when {@link #optimizePackages(PackageManagerLocal.FilteredSnapshot, String,
+         * CancellationSignal)} is called.
+         */
+        void onOverrideBatchOptimizeParams(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
+                @NonNull @BatchOptimizeReason String reason, @NonNull List<String> defaultPackages,
+                @NonNull BatchOptimizeParams.Builder builder);
+    }
+
     /**
      * Injector pattern for testing purpose.
      *
@@ -307,26 +424,28 @@
     public static class Injector {
         @Nullable private final Context mContext;
         @Nullable private final PackageManagerLocal mPackageManagerLocal;
+        @Nullable private final Config mConfig;
 
         Injector(@Nullable Context context) {
             mContext = context;
-            mPackageManagerLocal = LocalManagerRegistry.getManager(PackageManagerLocal.class);
+            if (context != null) {
+                // We only need them on Android U and above, where a context is passed.
+                mPackageManagerLocal = LocalManagerRegistry.getManager(PackageManagerLocal.class);
+                mConfig = new Config();
+            } else {
+                mPackageManagerLocal = null;
+                mConfig = null;
+            }
         }
 
         @NonNull
         public Context getContext() {
-            if (mContext == null) {
-                throw new IllegalStateException("Context is null");
-            }
-            return mContext;
+            return Objects.requireNonNull(mContext);
         }
 
         @NonNull
         public PackageManagerLocal getPackageManagerLocal() {
-            if (mPackageManagerLocal == null) {
-                throw new IllegalStateException("PackageManagerLocal is null");
-            }
-            return mPackageManagerLocal;
+            return Objects.requireNonNull(mPackageManagerLocal);
         }
 
         @NonNull
@@ -338,5 +457,15 @@
         public DexOptHelper getDexOptHelper() {
             return new DexOptHelper(getContext());
         }
+
+        @NonNull
+        public Config getConfig() {
+            return mConfig;
+        }
+
+        @NonNull
+        public AppHibernationManager getAppHibernationManager() {
+            return Objects.requireNonNull(mContext.getSystemService(AppHibernationManager.class));
+        }
     }
 }
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index 0e0d80c..b50ad3e 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -27,6 +27,7 @@
 import android.os.CancellationSignal;
 import android.os.Process;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.modules.utils.BasicShellCommandHandler;
 import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.DeleteResult;
@@ -53,7 +54,9 @@
     private final PackageManagerLocal mPackageManagerLocal;
     private final DexUseManager mDexUseManager = DexUseManager.getInstance();
 
-    private static Map<String, CancellationSignal> sCancellationSignalMap = new HashMap<>();
+    @GuardedBy("sCancellationSignalMap")
+    @NonNull
+    private static final Map<String, CancellationSignal> sCancellationSignalMap = new HashMap<>();
 
     public ArtShellCommand(
             ArtManagerLocal artManagerLocal, PackageManagerLocal packageManagerLocal) {
@@ -105,40 +108,21 @@
                         }
                     }
 
-                    String jobId = UUID.randomUUID().toString();
-                    var signal = new CancellationSignal();
-                    pw.printf("Job ID: %s\n", jobId);
-                    pw.flush();
-
-                    synchronized (sCancellationSignalMap) {
-                        sCancellationSignalMap.put(jobId, signal);
-                    }
-
                     OptimizeResult result;
-                    try {
-                        result = mArtManagerLocal.optimizePackage(
-                                snapshot, getNextArgRequired(), paramsBuilder.build(), signal);
-                    } finally {
-                        synchronized (sCancellationSignalMap) {
-                            sCancellationSignalMap.remove(jobId);
-                        }
+                    try (var signal = new WithCancellationSignal(pw)) {
+                        result = mArtManagerLocal.optimizePackage(snapshot, getNextArgRequired(),
+                                paramsBuilder.build(), signal.get());
                     }
-
-                    pw.println(optimizeStatusToString(result.getFinalStatus()));
-                    for (PackageOptimizeResult packageResult : result.getPackageOptimizeResults()) {
-                        pw.printf("[%s]\n", packageResult.getPackageName());
-                        for (DexContainerFileOptimizeResult fileResult :
-                                packageResult.getDexContainerFileOptimizeResults()) {
-                            pw.printf("dexContainerFile = %s, isPrimaryAbi = %b, abi = %s, "
-                                            + "compilerFilter = %s, status = %s, "
-                                            + "dex2oatWallTimeMillis = %d, dex2oatCpuTimeMillis = %d\n",
-                                    fileResult.getDexContainerFile(), fileResult.isPrimaryAbi(),
-                                    fileResult.getAbi(), fileResult.getActualCompilerFilter(),
-                                    optimizeStatusToString(fileResult.getStatus()),
-                                    fileResult.getDex2oatWallTimeMillis(),
-                                    fileResult.getDex2oatCpuTimeMillis());
-                        }
+                    printOptimizeResult(pw, result);
+                    return 0;
+                }
+                case "optimize-packages": {
+                    OptimizeResult result;
+                    try (var signal = new WithCancellationSignal(pw)) {
+                        result = mArtManagerLocal.optimizePackages(
+                                snapshot, getNextArgRequired(), signal.get());
                     }
+                    printOptimizeResult(pw, result);
                     return 0;
                 }
                 case "cancel": {
@@ -237,6 +221,10 @@
         pw.println("      -f Force compilation.");
         pw.println("      --secondary-dex Only compile secondary dex.");
         pw.println("      --include-dependencies Include dependencies.");
+        pw.println("  optimize-packages REASON");
+        pw.println("    Run batch optimization for the given reason.");
+        pw.println("    The command prints a job ID, which can be used to cancel the job using the"
+                + "'cancel' command.");
         pw.println("  cancel JOB_ID");
         pw.println("    Cancel a job.");
         pw.println("  dex-use-notify PACKAGE_NAME DEX_PATH CLASS_LOADER_CONTEXT");
@@ -277,4 +265,48 @@
         }
         throw new IllegalArgumentException("Unknown optimize status " + status);
     }
+
+    private void printOptimizeResult(@NonNull PrintWriter pw, @NonNull OptimizeResult result) {
+        pw.println(optimizeStatusToString(result.getFinalStatus()));
+        for (PackageOptimizeResult packageResult : result.getPackageOptimizeResults()) {
+            pw.printf("[%s]\n", packageResult.getPackageName());
+            for (DexContainerFileOptimizeResult fileResult :
+                    packageResult.getDexContainerFileOptimizeResults()) {
+                pw.printf("dexContainerFile = %s, isPrimaryAbi = %b, abi = %s, "
+                                + "compilerFilter = %s, status = %s, "
+                                + "dex2oatWallTimeMillis = %d, dex2oatCpuTimeMillis = %d\n",
+                        fileResult.getDexContainerFile(), fileResult.isPrimaryAbi(),
+                        fileResult.getAbi(), fileResult.getActualCompilerFilter(),
+                        optimizeStatusToString(fileResult.getStatus()),
+                        fileResult.getDex2oatWallTimeMillis(),
+                        fileResult.getDex2oatCpuTimeMillis());
+            }
+        }
+    }
+
+    private static class WithCancellationSignal implements AutoCloseable {
+        @NonNull private final CancellationSignal mSignal = new CancellationSignal();
+        @NonNull private final String mJobId;
+
+        public WithCancellationSignal(@NonNull PrintWriter pw) {
+            mJobId = UUID.randomUUID().toString();
+            pw.printf("Job ID: %s\n", mJobId);
+            pw.flush();
+
+            synchronized (sCancellationSignalMap) {
+                sCancellationSignalMap.put(mJobId, mSignal);
+            }
+        }
+
+        @NonNull
+        public CancellationSignal get() {
+            return mSignal;
+        }
+
+        public void close() {
+            synchronized (sCancellationSignalMap) {
+                sCancellationSignalMap.remove(mJobId);
+            }
+        }
+    }
 }
diff --git a/libartservice/service/java/com/android/server/art/DexOptHelper.java b/libartservice/service/java/com/android/server/art/DexOptHelper.java
index b30e25d..43be7d3 100644
--- a/libartservice/service/java/com/android/server/art/DexOptHelper.java
+++ b/libartservice/service/java/com/android/server/art/DexOptHelper.java
@@ -42,13 +42,14 @@
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Objects;
 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.function.Supplier;
 import java.util.stream.Collectors;
 
 /**
@@ -146,7 +147,7 @@
 
         AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
 
-        if (!canOptimizePackage(pkgState, pkg)) {
+        if (!canOptimizePackage(pkgState)) {
             return createResult.get();
         }
 
@@ -178,20 +179,8 @@
         return createResult.get();
     }
 
-    private boolean canOptimizePackage(
-            @NonNull PackageState pkgState, @NonNull AndroidPackage pkg) {
-        if (!pkg.getSplits().get(0).isHasCode()) {
-            return false;
-        }
-
-        // We do not dexopt unused packages.
-        AppHibernationManager ahm = mInjector.getAppHibernationManager();
-        if (ahm.isHibernatingGlobally(pkgState.getPackageName())
-                && ahm.isOatArtifactDeletionEnabled()) {
-            return false;
-        }
-
-        return true;
+    private boolean canOptimizePackage(@NonNull PackageState pkgState) {
+        return Utils.canOptimizePackage(pkgState, mInjector.getAppHibernationManager());
     }
 
     @NonNull
@@ -213,9 +202,9 @@
 
         for (String packageName : packageNames) {
             PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
-            AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
+            Utils.getPackageOrThrow(pkgState);
             pkgStates.put(packageName, pkgState);
-            if (includeDependencies && canOptimizePackage(pkgState, pkg)) {
+            if (includeDependencies && canOptimizePackage(pkgState)) {
                 for (SharedLibrary library : pkgState.getUsesLibraries()) {
                     maybeEnqueue.accept(library);
                 }
@@ -226,8 +215,7 @@
         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)) {
+            if (canOptimizePackage(pkgState)) {
                 pkgStates.put(packageName, pkgState);
 
                 // Note that `library.getDependencies()` is different from
@@ -274,12 +262,12 @@
 
         @NonNull
         public AppHibernationManager getAppHibernationManager() {
-            return mContext.getSystemService(AppHibernationManager.class);
+            return Objects.requireNonNull(mContext.getSystemService(AppHibernationManager.class));
         }
 
         @NonNull
         public PowerManager getPowerManager() {
-            return mContext.getSystemService(PowerManager.class);
+            return Objects.requireNonNull(mContext.getSystemService(PowerManager.class));
         }
     }
 }
diff --git a/libartservice/service/java/com/android/server/art/DexUseManager.java b/libartservice/service/java/com/android/server/art/DexUseManager.java
index e51c3bf..e3e8134 100644
--- a/libartservice/service/java/com/android/server/art/DexUseManager.java
+++ b/libartservice/service/java/com/android/server/art/DexUseManager.java
@@ -220,7 +220,7 @@
         // "android" comes from `SystemServerDexLoadReporter`. ART Services doesn't need to handle
         // this case because it doesn't compile system server and system server isn't allowed to
         // load artifacts produced by ART Services.
-        if (loadingPackageName.equals("android")) {
+        if (loadingPackageName.equals(Utils.PLATFORM_PACKAGE_NAME)) {
             return;
         }
 
diff --git a/libartservice/service/java/com/android/server/art/ReasonMapping.java b/libartservice/service/java/com/android/server/art/ReasonMapping.java
index 08140ad..98c82d8 100644
--- a/libartservice/service/java/com/android/server/art/ReasonMapping.java
+++ b/libartservice/service/java/com/android/server/art/ReasonMapping.java
@@ -19,15 +19,19 @@
 import static com.android.server.art.model.ArtFlags.PriorityClassApi;
 
 import android.annotation.NonNull;
+import android.annotation.StringDef;
 import android.annotation.SystemApi;
 import android.os.SystemProperties;
 import android.text.TextUtils;
 
 import com.android.server.art.model.ArtFlags;
+import com.android.server.pm.PackageManagerLocal;
 
 import dalvik.system.DexFile;
 
 import java.util.Set;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * Maps a compilation reason to a compiler filter and a priority class.
@@ -65,6 +69,22 @@
             REASON_INSTALL_BULK_DOWNGRADED, REASON_INSTALL_BULK_SECONDARY_DOWNGRADED);
 
     /**
+     * Reasons for
+     * {@link ArtManagerLocal#optimizePackages(PackageManagerLocal.FilteredSnapshot, String)}.
+     *
+     * @hide
+     */
+    // clang-format off
+    @StringDef(prefix = "REASON_", value = {
+        REASON_FIRST_BOOT,
+        REASON_BOOT_AFTER_OTA,
+        REASON_BG_DEXOPT,
+    })
+    // clang-format on
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BatchOptimizeReason {}
+
+    /**
      * Loads the compiler filter from the system property for the given reason and checks for
      * validity.
      *
@@ -136,4 +156,15 @@
                 throw new IllegalArgumentException("No priority class for reason '" + reason + "'");
         }
     }
+
+    /**
+     * Loads the concurrency from the system property, for batch optimization ({@link
+     * ArtManagerLocal#optimizePackages(PackageManagerLocal.FilteredSnapshot, String)}), or 1 if the
+     * system property is not found or cannot be parsed.
+     *
+     * @hide
+     */
+    public static int getConcurrencyForReason(@NonNull @BatchOptimizeReason String reason) {
+        return SystemProperties.getInt("pm.dexopt." + reason + ".concurrency", 1 /* def */);
+    }
 }
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index e4060c1..19adc3f 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.apphibernation.AppHibernationManager;
 import android.os.ServiceManager;
 import android.os.SystemProperties;
 import android.text.TextUtils;
@@ -40,12 +41,15 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.FutureTask;
 import java.util.stream.Collectors;
 
 /** @hide */
 public final class Utils {
+    public static final String PLATFORM_PACKAGE_NAME = "android";
+
     private Utils() {}
 
     /**
@@ -211,6 +215,10 @@
         return str;
     }
 
+    public static void executeAndWait(@NonNull Executor executor, @NonNull Runnable runnable) {
+        getFuture(execute(executor, Executors.callable(runnable)));
+    }
+
     public static <T> Future<T> execute(@NonNull Executor executor, @NonNull Callable<T> callable) {
         var future = new FutureTask<T>(callable);
         executor.execute(future);
@@ -231,6 +239,41 @@
         }
     }
 
+    /**
+     * Returns true if the given package is optimizable.
+     *
+     * @param appHibernationManager the {@link AppHibernationManager} instance for checking
+     *         hibernation status, or null to skip the check
+     */
+    public static boolean canOptimizePackage(
+            @NonNull PackageState pkgState, @Nullable AppHibernationManager appHibernationManager) {
+        // An APEX has a uid of -1.
+        // TODO(b/256637152): Consider using `isApex` instead.
+        if (pkgState.getAppId() <= 0) {
+            return false;
+        }
+
+        // "android" is a special package that represents the platform, not an app.
+        if (pkgState.getPackageName().equals(Utils.PLATFORM_PACKAGE_NAME)) {
+            return false;
+        }
+
+        AndroidPackage pkg = pkgState.getAndroidPackage();
+        if (pkg == null || !pkg.getSplits().get(0).isHasCode()) {
+            return false;
+        }
+
+        // We do not dexopt unused packages.
+        // If `appHibernationManager` is null, the caller's intention is to skip the check.
+        if (appHibernationManager != null
+                && appHibernationManager.isHibernatingGlobally(pkgState.getPackageName())
+                && appHibernationManager.isOatArtifactDeletionEnabled()) {
+            return false;
+        }
+
+        return true;
+    }
+
     @AutoValue
     public abstract static class Abi {
         static @NonNull Abi create(
diff --git a/libartservice/service/java/com/android/server/art/model/BatchOptimizeParams.java b/libartservice/service/java/com/android/server/art/model/BatchOptimizeParams.java
new file mode 100644
index 0000000..f0025e4
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/BatchOptimizeParams.java
@@ -0,0 +1,85 @@
+/*
+ * 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 android.annotation.SystemApi;
+
+import com.android.internal.annotations.Immutable;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** @hide */
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+@Immutable
+@AutoValue
+public abstract class BatchOptimizeParams {
+    public static final class Builder {
+        private @NonNull List<String> mPackageNames; // This is assumed immutable.
+        private @NonNull OptimizeParams mOptimizeParams;
+
+        /** @hide */
+        public Builder(@NonNull List<String> defaultPackages,
+                @NonNull OptimizeParams defaultOptimizeParams) {
+            mPackageNames = defaultPackages; // The argument is assumed immutable.
+            mOptimizeParams = defaultOptimizeParams;
+        }
+
+        /**
+         * Sets the list of packages to optimize. The optimization will be scheduled in the given
+         * order.
+         *
+         * If not called, the default list will be used.
+         */
+        @NonNull
+        public Builder setPackages(@NonNull List<String> packageNames) {
+            mPackageNames = Collections.unmodifiableList(new ArrayList<>(packageNames));
+            return this;
+        }
+
+        /**
+         * Sets the params for optimizing each package.
+         *
+         * If not called, the default params built from {@link OptimizeParams#Builder(String)} will
+         * be used.
+         */
+        @NonNull
+        public Builder setOptimizeParams(@NonNull OptimizeParams optimizeParams) {
+            mOptimizeParams = optimizeParams;
+            return this;
+        }
+
+        /** Returns the built object. */
+        @NonNull
+        public BatchOptimizeParams build() {
+            return new AutoValue_BatchOptimizeParams(mPackageNames, mOptimizeParams);
+        }
+    }
+
+    /** @hide */
+    protected BatchOptimizeParams() {}
+
+    /** The ordered list of packages to optimize. */
+    public abstract @NonNull List<String> getPackages();
+
+    /** The params for optimizing each package. */
+    public abstract @NonNull OptimizeParams getOptimizeParams();
+}
diff --git a/libartservice/service/java/com/android/server/art/model/Config.java b/libartservice/service/java/com/android/server/art/model/Config.java
new file mode 100644
index 0000000..3082685
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/Config.java
@@ -0,0 +1,65 @@
+/*
+ * 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 static com.android.server.art.ArtManagerLocal.OptimizePackagesCallback;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.art.ArtManagerLocal;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A class that stores the configurations set by the consumer of ART Service at runtime. This class
+ * is thread-safe.
+ *
+ * @hide
+ */
+public class Config {
+    /** @see ArtManagerLocal#setOptimizePackagesCallback(Executor, OptimizePackagesCallback) */
+    @GuardedBy("this")
+    @Nullable
+    private Callback<OptimizePackagesCallback> mOptimizePackagesCallback = null;
+
+    public synchronized void setOptimizePackagesCallback(
+            @NonNull Executor executor, @NonNull OptimizePackagesCallback callback) {
+        mOptimizePackagesCallback = Callback.<OptimizePackagesCallback>create(callback, executor);
+    }
+
+    public synchronized void clearOptimizePackagesCallback() {
+        mOptimizePackagesCallback = null;
+    }
+
+    @Nullable
+    public synchronized Callback<OptimizePackagesCallback> getOptimizePackagesCallback() {
+        return mOptimizePackagesCallback;
+    }
+
+    @AutoValue
+    public static abstract class Callback<T> {
+        public abstract @NonNull T get();
+        public abstract @NonNull Executor executor();
+        static <T> @NonNull Callback<T> create(@NonNull T callback, @NonNull Executor executor) {
+            return new AutoValue_Config_Callback<T>(callback, executor);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/model/OptimizeParams.java b/libartservice/service/java/com/android/server/art/model/OptimizeParams.java
index 6f40546..ae8c984 100644
--- a/libartservice/service/java/com/android/server/art/model/OptimizeParams.java
+++ b/libartservice/service/java/com/android/server/art/model/OptimizeParams.java
@@ -22,11 +22,13 @@
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 
+import com.android.internal.annotations.Immutable;
 import com.android.server.art.ReasonMapping;
 import com.android.server.art.Utils;
 
 /** @hide */
 @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+@Immutable
 public class OptimizeParams {
     public static final class Builder {
         private OptimizeParams mParams = new OptimizeParams();
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index e9a2beb..f0aa7e5 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -21,9 +21,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.AdditionalMatchers.not;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mock;
@@ -32,12 +34,14 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import android.apphibernation.AppHibernationManager;
 import android.os.CancellationSignal;
 import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.art.model.Config;
 import com.android.server.art.model.DeleteResult;
 import com.android.server.art.model.OptimizationStatus;
 import com.android.server.art.model.OptimizeParams;
@@ -58,11 +62,15 @@
 import org.mockito.Mock;
 
 import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
 
 @SmallTest
 @RunWith(Parameterized.class)
 public class ArtManagerLocalTest {
     private static final String PKG_NAME = "com.example.foo";
+    private static final String PKG_NAME_SYS_UI = "com.android.systemui";
+    private static final String PKG_NAME_HIBERNATING = "com.example.hibernating";
 
     @Rule
     public StaticMockitoRule mockitoRule =
@@ -73,8 +81,10 @@
     @Mock private PackageManagerLocal.FilteredSnapshot mSnapshot;
     @Mock private IArtd mArtd;
     @Mock private DexOptHelper mDexOptHelper;
+    @Mock private AppHibernationManager mAppHibernationManager;
     private PackageState mPkgState;
     private AndroidPackage mPkg;
+    private Config mConfig;
 
     // True if the primary dex'es are in a readonly partition.
     @Parameter(0) public boolean mIsInReadonlyPartition;
@@ -88,14 +98,23 @@
 
     @Before
     public void setUp() throws Exception {
+        mConfig = new Config();
+
         // Use `lenient()` to suppress `UnnecessaryStubbingException` thrown by the strict stubs.
         // These are the default test setups. They may or may not be used depending on the code path
         // that each test case examines.
         lenient().when(mInjector.getPackageManagerLocal()).thenReturn(mPackageManagerLocal);
         lenient().when(mInjector.getArtd()).thenReturn(mArtd);
         lenient().when(mInjector.getDexOptHelper()).thenReturn(mDexOptHelper);
+        lenient().when(mInjector.getConfig()).thenReturn(mConfig);
+        lenient().when(mInjector.getAppHibernationManager()).thenReturn(mAppHibernationManager);
 
         lenient().when(SystemProperties.get(eq("pm.dexopt.install"))).thenReturn("speed-profile");
+        lenient().when(SystemProperties.get(eq("pm.dexopt.bg-dexopt"))).thenReturn("speed-profile");
+        lenient().when(SystemProperties.get(eq("pm.dexopt.first-boot"))).thenReturn("verify");
+        lenient()
+                .when(SystemProperties.getInt(eq("pm.dexopt.bg-dexopt.concurrency"), anyInt()))
+                .thenReturn(3);
 
         // No ISA translation.
         lenient()
@@ -106,14 +125,28 @@
         lenient().when(Constants.getNative64BitAbi()).thenReturn("arm64-v8a");
         lenient().when(Constants.getNative32BitAbi()).thenReturn("armeabi-v7a");
 
-        mPkgState = createPackageState();
+        lenient().when(mAppHibernationManager.isHibernatingGlobally(any())).thenReturn(false);
+        lenient().when(mAppHibernationManager.isOatArtifactDeletionEnabled()).thenReturn(true);
+
+        lenient().when(mPackageManagerLocal.withFilteredSnapshot()).thenReturn(mSnapshot);
+        List<PackageState> pkgStates = createPackageStates();
+        for (PackageState pkgState : pkgStates) {
+            lenient()
+                    .when(mSnapshot.getPackageState(pkgState.getPackageName()))
+                    .thenReturn(pkgState);
+        }
+        lenient()
+                .doAnswer(invocation -> {
+                    var consumer = invocation.<Consumer<PackageState>>getArgument(0);
+                    for (PackageState pkgState : pkgStates) {
+                        consumer.accept(pkgState);
+                    }
+                    return null;
+                })
+                .when(mSnapshot)
+                .forAllPackageStates(any());
+        mPkgState = mSnapshot.getPackageState(PKG_NAME);
         mPkg = mPkgState.getAndroidPackage();
-        lenient()
-                .when(mPackageManagerLocal.withFilteredSnapshot())
-                .thenReturn(mSnapshot);
-        lenient()
-                .when(mSnapshot.getPackageState(mPkgState.getPackageName()))
-                .thenReturn(mPkgState);
 
         mArtManagerLocal = new ArtManagerLocal(mInjector);
     }
@@ -122,8 +155,7 @@
     public void testDeleteOptimizedArtifacts() throws Exception {
         when(mArtd.deleteArtifacts(any())).thenReturn(1l);
 
-        DeleteResult result = mArtManagerLocal.deleteOptimizedArtifacts(
-                mSnapshot, PKG_NAME);
+        DeleteResult result = mArtManagerLocal.deleteOptimizedArtifacts(mSnapshot, PKG_NAME);
         assertThat(result.getFreedBytes()).isEqualTo(4);
 
         verify(mArtd).deleteArtifacts(argThat(artifactsPath
@@ -203,8 +235,7 @@
                         createGetOptimizationStatusResult(
                                 "extract", "compilation-reason-3", "location-debug-string-3"));
 
-        OptimizationStatus result =
-                mArtManagerLocal.getOptimizationStatus(mSnapshot, PKG_NAME);
+        OptimizationStatus result = mArtManagerLocal.getOptimizationStatus(mSnapshot, PKG_NAME);
 
         List<DexContainerFileOptimizationStatus> statuses =
                 result.getDexContainerFileOptimizationStatuses();
@@ -258,8 +289,7 @@
         when(mArtd.getOptimizationStatus(any(), any(), any()))
                 .thenThrow(new ServiceSpecificException(1 /* errorCode */, "some error message"));
 
-        OptimizationStatus result =
-                mArtManagerLocal.getOptimizationStatus(mSnapshot, PKG_NAME);
+        OptimizationStatus result = mArtManagerLocal.getOptimizationStatus(mSnapshot, PKG_NAME);
 
         List<DexContainerFileOptimizationStatus> statuses =
                 result.getDexContainerFileOptimizationStatuses();
@@ -282,47 +312,158 @@
                      same(cancellationSignal), any()))
                 .thenReturn(result);
 
-        assertThat(mArtManagerLocal.optimizePackage(mSnapshot,
-                PKG_NAME, params, cancellationSignal))
+        assertThat(
+                mArtManagerLocal.optimizePackage(mSnapshot, PKG_NAME, params, cancellationSignal))
                 .isSameInstanceAs(result);
     }
 
-    private AndroidPackage createPackage() {
+    @Test
+    public void testOptimizePackages() throws Exception {
+        var result = mock(OptimizeResult.class);
+        var cancellationSignal = new CancellationSignal();
+
+        // It should use the default package list and params.
+        when(mDexOptHelper.dexopt(any(), deepEq(List.of(PKG_NAME, PKG_NAME_SYS_UI)), any(),
+                     same(cancellationSignal), any()))
+                .thenReturn(result);
+
+        assertThat(mArtManagerLocal.optimizePackages(mSnapshot, "bg-dexopt", cancellationSignal))
+                .isSameInstanceAs(result);
+    }
+
+    @Test
+    public void testOptimizePackagesOverride() throws Exception {
+        var params = new OptimizeParams.Builder("bg-dexopt").build();
+        var result = mock(OptimizeResult.class);
+        var cancellationSignal = new CancellationSignal();
+
+        mArtManagerLocal.setOptimizePackagesCallback(Executors.newSingleThreadExecutor(),
+                (snapshot, reason, defaultPackages, builder) -> {
+                    assertThat(reason).isEqualTo("bg-dexopt");
+                    assertThat(defaultPackages).containsExactly(PKG_NAME, PKG_NAME_SYS_UI);
+                    builder.setPackages(List.of(PKG_NAME)).setOptimizeParams(params);
+                });
+
+        // It should use the overridden package list and params.
+        when(mDexOptHelper.dexopt(any(), deepEq(List.of(PKG_NAME)), same(params),
+                     same(cancellationSignal), any()))
+                .thenReturn(result);
+
+        assertThat(mArtManagerLocal.optimizePackages(mSnapshot, "bg-dexopt", cancellationSignal))
+                .isSameInstanceAs(result);
+    }
+
+    @Test
+    public void testOptimizePackagesOverrideCleared() throws Exception {
+        var params = new OptimizeParams.Builder("bg-dexopt").build();
+        var result = mock(OptimizeResult.class);
+        var cancellationSignal = new CancellationSignal();
+
+        mArtManagerLocal.setOptimizePackagesCallback(Executors.newSingleThreadExecutor(),
+                (snapshot, reason, defaultPackages, builder) -> {
+                    builder.setPackages(List.of(PKG_NAME)).setOptimizeParams(params);
+                });
+        mArtManagerLocal.clearOptimizePackagesCallback();
+
+        // It should use the default package list and params.
+        when(mDexOptHelper.dexopt(any(), deepEq(List.of(PKG_NAME, PKG_NAME_SYS_UI)),
+                     not(same(params)), same(cancellationSignal), any()))
+                .thenReturn(result);
+
+        assertThat(mArtManagerLocal.optimizePackages(mSnapshot, "bg-dexopt", cancellationSignal))
+                .isSameInstanceAs(result);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testOptimizePackagesOverrideReasonChanged() throws Exception {
+        var params = new OptimizeParams.Builder("first-boot").build();
+        var cancellationSignal = new CancellationSignal();
+
+        mArtManagerLocal.setOptimizePackagesCallback(Executors.newSingleThreadExecutor(),
+                (snapshot, reason, defaultPackages, builder) -> {
+                    builder.setOptimizeParams(params);
+                });
+
+        mArtManagerLocal.optimizePackages(mSnapshot, "bg-dexopt", cancellationSignal);
+    }
+
+    private AndroidPackage createPackage(boolean multiSplit) {
         AndroidPackage pkg = mock(AndroidPackage.class);
 
         var baseSplit = mock(AndroidPackageSplit.class);
         lenient().when(baseSplit.getPath()).thenReturn("/data/app/foo/base.apk");
         lenient().when(baseSplit.isHasCode()).thenReturn(true);
 
-        // split_0 has code while split_1 doesn't.
-        var split0 = mock(AndroidPackageSplit.class);
-        lenient().when(split0.getName()).thenReturn("split_0");
-        lenient().when(split0.getPath()).thenReturn("/data/app/foo/split_0.apk");
-        lenient().when(split0.isHasCode()).thenReturn(true);
-        var split1 = mock(AndroidPackageSplit.class);
-        lenient().when(split1.getName()).thenReturn("split_1");
-        lenient().when(split1.getPath()).thenReturn("/data/app/foo/split_1.apk");
-        lenient().when(split1.isHasCode()).thenReturn(false);
+        if (multiSplit) {
+            // split_0 has code while split_1 doesn't.
+            var split0 = mock(AndroidPackageSplit.class);
+            lenient().when(split0.getName()).thenReturn("split_0");
+            lenient().when(split0.getPath()).thenReturn("/data/app/foo/split_0.apk");
+            lenient().when(split0.isHasCode()).thenReturn(true);
+            var split1 = mock(AndroidPackageSplit.class);
+            lenient().when(split1.getName()).thenReturn("split_1");
+            lenient().when(split1.getPath()).thenReturn("/data/app/foo/split_1.apk");
+            lenient().when(split1.isHasCode()).thenReturn(false);
 
-        var splits = List.of(baseSplit, split0, split1);
-        lenient().when(pkg.getSplits()).thenReturn(splits);
+            lenient().when(pkg.getSplits()).thenReturn(List.of(baseSplit, split0, split1));
+        } else {
+            lenient().when(pkg.getSplits()).thenReturn(List.of(baseSplit));
+        }
+
         return pkg;
     }
 
-    private PackageState createPackageState() {
+    private PackageState createPackageState(
+            String packageName, int appId, boolean hasPackage, boolean multiSplit) {
         PackageState pkgState = mock(PackageState.class);
 
-        lenient().when(pkgState.getPackageName()).thenReturn(PKG_NAME);
+        lenient().when(pkgState.getPackageName()).thenReturn(packageName);
         lenient().when(pkgState.getPrimaryCpuAbi()).thenReturn("arm64-v8a");
         lenient().when(pkgState.getSecondaryCpuAbi()).thenReturn("armeabi-v7a");
         lenient().when(pkgState.isSystem()).thenReturn(mIsInReadonlyPartition);
         lenient().when(pkgState.isUpdatedSystemApp()).thenReturn(false);
-        AndroidPackage pkg = createPackage();
-        lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
+        lenient().when(pkgState.getAppId()).thenReturn(appId);
+
+        if (hasPackage) {
+            AndroidPackage pkg = createPackage(multiSplit);
+            lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
+        } else {
+            lenient().when(pkgState.getAndroidPackage()).thenReturn(null);
+        }
 
         return pkgState;
     }
 
+    private List<PackageState> createPackageStates() {
+        PackageState pkgState = createPackageState(
+                PKG_NAME, 10001 /* appId */, true /* hasPackage */, true /* multiSplit */);
+
+        PackageState sysUiPkgState = createPackageState(
+                PKG_NAME_SYS_UI, 1234 /* appId */, true /* hasPackage */, false /* multiSplit */);
+
+        // This should not be optimized because it's hibernating.
+        PackageState pkgHibernatingState = createPackageState(PKG_NAME_HIBERNATING,
+                10002 /* appId */, true /* hasPackage */, false /* multiSplit */);
+        lenient()
+                .when(mAppHibernationManager.isHibernatingGlobally(PKG_NAME_HIBERNATING))
+                .thenReturn(true);
+
+        // This should not be optimized because it does't have AndroidPackage.
+        PackageState nullPkgState = createPackageState("com.example.null", 10003 /* appId */,
+                false /* hasPackage */, false /* multiSplit */);
+
+        // This should not be optimized because it has a negative app id.
+        PackageState apexPkgState = createPackageState(
+                "com.android.art", -1 /* appId */, true /* hasPackage */, false /* multiSplit */);
+
+        // This should not be optimized because it's "android".
+        PackageState platformPkgState = createPackageState(Utils.PLATFORM_PACKAGE_NAME,
+                1000 /* appId */, true /* hasPackage */, false /* multiSplit */);
+
+        return List.of(pkgState, sysUiPkgState, pkgHibernatingState, nullPkgState, apexPkgState,
+                platformPkgState);
+    }
+
     private GetOptimizationStatusResult createGetOptimizationStatusResult(
             String compilerFilter, String compilationReason, String locationDebugString) {
         var getOptimizationStatusResult = new GetOptimizationStatusResult();
diff --git a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
index f83d1bc..7fa2dfc 100644
--- a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
@@ -211,7 +211,7 @@
 
     private void verifyPrimaryDexMultipleEntries(boolean saveAndLoad) throws Exception {
         // These should be ignored.
-        mDexUseManager.addDexUse(mSnapshot, "android", Map.of(BASE_APK, "CLC"));
+        mDexUseManager.addDexUse(mSnapshot, Utils.PLATFORM_PACKAGE_NAME, Map.of(BASE_APK, "CLC"));
         mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
                 Map.of("/data/app/" + OWNING_PKG_NAME + "/non-existing.apk", "CLC"));
 
@@ -333,7 +333,8 @@
 
     private void verifySecondaryDexMultipleEntries(boolean saveAndLoad) throws Exception {
         // These should be ignored.
-        mDexUseManager.addDexUse(mSnapshot, "android", Map.of(mCeDir + "/foo.apk", "CLC"));
+        mDexUseManager.addDexUse(
+                mSnapshot, Utils.PLATFORM_PACKAGE_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
         mDexUseManager.addDexUse(
                 mSnapshot, OWNING_PKG_NAME, Map.of("/some/non-existing.apk", "CLC"));
 
diff --git a/libartservice/service/javatests/com/android/server/art/ReasonMappingTest.java b/libartservice/service/javatests/com/android/server/art/ReasonMappingTest.java
index 5933643..55fd0b4 100644
--- a/libartservice/service/javatests/com/android/server/art/ReasonMappingTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ReasonMappingTest.java
@@ -18,6 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.when;
 
 import android.os.SystemProperties;
@@ -76,4 +78,11 @@
     public void testGetPriorityClassForReasonInvalidReason() throws Exception {
         ReasonMapping.getPriorityClassForReason("foo");
     }
+
+    @Test
+    public void testGetConcurrencyForReason() {
+        when(SystemProperties.getInt(eq("pm.dexopt.bg-dexopt.concurrency"), anyInt()))
+                .thenReturn(3);
+        assertThat(ReasonMapping.getConcurrencyForReason("bg-dexopt")).isEqualTo(3);
+    }
 }
diff --git a/libartservice/service/javatests/com/android/server/art/UtilsTest.java b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
index 79d1fd3..93e927b 100644
--- a/libartservice/service/javatests/com/android/server/art/UtilsTest.java
+++ b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
@@ -38,7 +38,10 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -172,4 +175,25 @@
     public void testCheckFailed() throws Exception {
         Utils.check(false);
     }
+
+    @Test
+    public void testExecuteAndWait() {
+        Executor executor = Executors.newSingleThreadExecutor();
+        List<String> results = new ArrayList<>();
+        Utils.executeAndWait(executor, () -> {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+            results.add("foo");
+        });
+        assertThat(results).containsExactly("foo");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testExecuteAndWaitPropagatesException() {
+        Executor executor = Executors.newSingleThreadExecutor();
+        Utils.executeAndWait(executor, () -> { throw new IllegalArgumentException(); });
+    }
 }