Add an API to handle boot events.

Bug: 260419279
Test: Presubmit
Ignore-AOSP-First: ART Services.
Change-Id: Id396a54ef616d4014873f6fe02077bb6d30d2359
diff --git a/libartservice/service/api/system-server-current.txt b/libartservice/service/api/system-server-current.txt
index 992c08c..de59623 100644
--- a/libartservice/service/api/system-server-current.txt
+++ b/libartservice/service/api/system-server-current.txt
@@ -13,6 +13,7 @@
     method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String);
     method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, int);
     method public int handleShellCommand(@NonNull android.os.Binder, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull String[]);
+    method public void onBoot(@NonNull String, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<com.android.server.art.model.OperationProgress>);
     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 removeOptimizePackageDoneCallback(@NonNull com.android.server.art.ArtManagerLocal.OptimizePackageDoneCallback);
@@ -101,6 +102,10 @@
     method public long getFreedBytes();
   }
 
+  public abstract class OperationProgress {
+    method public int getPercentage();
+  }
+
   public abstract class OptimizationStatus {
     method @NonNull public abstract java.util.List<com.android.server.art.model.OptimizationStatus.DexContainerFileOptimizationStatus> getDexContainerFileOptimizationStatuses();
   }
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 847fb91..185d113 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -19,6 +19,7 @@
 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.ReasonMapping.BootReason;
 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;
@@ -49,9 +50,9 @@
 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.OperationProgress;
 import com.android.server.art.model.OptimizationStatus;
 import com.android.server.art.model.OptimizeParams;
-import com.android.server.art.model.OptimizeProgress;
 import com.android.server.art.model.OptimizeResult;
 import com.android.server.pm.PackageManagerLocal;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -329,7 +330,7 @@
             @NonNull @BatchOptimizeReason String reason,
             @NonNull CancellationSignal cancellationSignal,
             @Nullable @CallbackExecutor Executor processCallbackExecutor,
-            @Nullable Consumer<OptimizeProgress> progressCallback) {
+            @Nullable Consumer<OperationProgress> progressCallback) {
         List<String> defaultPackages =
                 Collections.unmodifiableList(getDefaultPackages(snapshot, reason));
         OptimizeParams defaultOptimizeParams = new OptimizeParams.Builder(reason).build();
@@ -590,6 +591,25 @@
     }
 
     /**
+     * Notifies ART Service that this is a boot that falls into one of the categories listed in
+     * {@link BootReason}. The current behavior is that ART Service goes through all recently used
+     * packages and optimizes those that are not optimized. This might change in the future.
+     *
+     * This method is blocking. It takes about 30 seconds to a few minutes. During execution, {@code
+     * progressCallback} is repeatedly called whenever there is an update on the progress.
+     *
+     * See {@link #optimizePackages} for how to customize the behavior.
+     */
+    public void onBoot(@NonNull @BootReason String bootReason,
+            @Nullable @CallbackExecutor Executor progressCallbackExecutor,
+            @Nullable Consumer<OperationProgress> progressCallback) {
+        try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot()) {
+            optimizePackages(snapshot, bootReason, new CancellationSignal(),
+                    progressCallbackExecutor, progressCallback);
+        }
+    }
+
+    /**
      * Should be used by {@link BackgroundDexOptJobService} ONLY.
      *
      * @hide
@@ -603,6 +623,7 @@
     private List<String> getDefaultPackages(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
             @NonNull @BatchOptimizeReason String reason) {
         var packages = new ArrayList<String>();
+        // TODO(b/258818709): Filter packages by last active time.
         snapshot.forAllPackageStates((pkgState) -> {
             if (Utils.canOptimizePackage(pkgState, mInjector.getAppHibernationManager())) {
                 packages.add(pkgState.getPackageName());
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index 727803f..95996b3 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -38,6 +38,7 @@
 import com.android.modules.utils.BasicShellCommandHandler;
 import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.DeleteResult;
+import com.android.server.art.model.OperationProgress;
 import com.android.server.art.model.OptimizationStatus;
 import com.android.server.art.model.OptimizeParams;
 import com.android.server.art.model.OptimizeResult;
@@ -164,9 +165,8 @@
                     try (var signal = new WithCancellationSignal(pw)) {
                         result = mArtManagerLocal.optimizePackages(snapshot, getNextArgRequired(),
                                 signal.get(), executor, progress -> {
-                                    pw.println(String.format("Optimizing packages: %d/%d",
-                                            progress.getDonePackageCount(),
-                                            progress.getTotalPackageCount()));
+                                    pw.println(String.format(
+                                            "Optimizing apps: %d%%", progress.getPercentage()));
                                     pw.flush();
                                 });
                     }
diff --git a/libartservice/service/java/com/android/server/art/DexOptHelper.java b/libartservice/service/java/com/android/server/art/DexOptHelper.java
index 5bd70f7..1370cf9 100644
--- a/libartservice/service/java/com/android/server/art/DexOptHelper.java
+++ b/libartservice/service/java/com/android/server/art/DexOptHelper.java
@@ -34,8 +34,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.Config;
+import com.android.server.art.model.OperationProgress;
 import com.android.server.art.model.OptimizeParams;
-import com.android.server.art.model.OptimizeProgress;
 import com.android.server.art.model.OptimizeResult;
 import com.android.server.pm.PackageManagerLocal;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -107,7 +107,7 @@
             @NonNull List<String> packageNames, @NonNull OptimizeParams params,
             @NonNull CancellationSignal cancellationSignal, @NonNull Executor dexoptExecutor,
             @Nullable Executor progressCallbackExecutor,
-            @Nullable Consumer<OptimizeProgress> progressCallback) {
+            @Nullable Consumer<OperationProgress> progressCallback) {
         return dexoptPackages(
                 getPackageStates(snapshot, packageNames,
                         (params.getFlags() & ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES) != 0),
@@ -123,7 +123,7 @@
     private OptimizeResult dexoptPackages(@NonNull List<PackageState> pkgStates,
             @NonNull OptimizeParams params, @NonNull CancellationSignal cancellationSignal,
             @NonNull Executor dexoptExecutor, @Nullable Executor progressCallbackExecutor,
-            @Nullable Consumer<OptimizeProgress> progressCallback) {
+            @Nullable Consumer<OperationProgress> progressCallback) {
         int callingUid = Binder.getCallingUid();
         long identityToken = Binder.clearCallingIdentity();
         PowerManager.WakeLock wakeLock = null;
@@ -144,13 +144,13 @@
             if (progressCallback != null) {
                 CompletableFuture.runAsync(() -> {
                     progressCallback.accept(
-                            OptimizeProgress.create(0 /* donePackageCount */, futures.size()));
+                            OperationProgress.create(0 /* current */, futures.size()));
                 }, progressCallbackExecutor);
-                AtomicInteger donePackageCount = new AtomicInteger(0);
+                AtomicInteger current = new AtomicInteger(0);
                 for (CompletableFuture<PackageOptimizeResult> future : futures) {
                     future.thenRunAsync(() -> {
-                        progressCallback.accept(OptimizeProgress.create(
-                                donePackageCount.incrementAndGet(), futures.size()));
+                        progressCallback.accept(OperationProgress.create(
+                                current.incrementAndGet(), futures.size()));
                     }, progressCallbackExecutor);
                 }
             }
diff --git a/libartservice/service/java/com/android/server/art/ReasonMapping.java b/libartservice/service/java/com/android/server/art/ReasonMapping.java
index d408759..c236e21 100644
--- a/libartservice/service/java/com/android/server/art/ReasonMapping.java
+++ b/libartservice/service/java/com/android/server/art/ReasonMapping.java
@@ -42,7 +42,7 @@
 public class ReasonMapping {
     private ReasonMapping() {}
 
-    /** Optimizing apps on the first boot. */
+    /** Optimizing apps on the first boot after flashing or factory resetting the device. */
     public static final String REASON_FIRST_BOOT = "first-boot";
     /** Optimizing apps on the next boot after an OTA. */
     public static final String REASON_BOOT_AFTER_OTA = "boot-after-ota";
@@ -84,6 +84,20 @@
     public @interface BatchOptimizeReason {}
 
     /**
+     * Reasons for {@link ArtManagerLocal#onBoot(String, Executor, Consumer<OperationProgress>)}.
+     *
+     * @hide
+     */
+    // clang-format off
+    @StringDef(prefix = "REASON_", value = {
+        REASON_FIRST_BOOT,
+        REASON_BOOT_AFTER_OTA,
+    })
+    // clang-format on
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BootReason {}
+
+    /**
      * Loads the compiler filter from the system property for the given reason and checks for
      * validity.
      *
diff --git a/libartservice/service/java/com/android/server/art/model/OperationProgress.java b/libartservice/service/java/com/android/server/art/model/OperationProgress.java
new file mode 100644
index 0000000..2e46f59
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/OperationProgress.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+/** @hide */
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+@Immutable
+@AutoValue
+public abstract class OperationProgress {
+    /** @hide */
+    protected OperationProgress() {}
+
+    /** @hide */
+    public static @NonNull OperationProgress create(int current, int total) {
+        return new AutoValue_OperationProgress(current, total);
+    }
+
+    /** The overall progress, in the range of [0, 100]. */
+    public int getPercentage() {
+        return 100 * getCurrent() / getTotal();
+    }
+
+    /**
+     * The number of processed items. Can be 0, which means the operation was just started.
+     *
+     * Currently, this is the number of packages, for which optimization has been done, regardless
+     * of the results (performed, failed, skipped, etc.).
+     *
+     * @hide
+     */
+    public abstract int getCurrent();
+
+    /**
+     * The total number of items. Stays constant during the operation.
+     *
+     * Currently, this is the total number of packages to optimize.
+     *
+     * @hide
+     */
+    public abstract int getTotal();
+}
diff --git a/libartservice/service/java/com/android/server/art/model/OptimizeProgress.java b/libartservice/service/java/com/android/server/art/model/OptimizeProgress.java
deleted file mode 100644
index 39310a5..0000000
--- a/libartservice/service/java/com/android/server/art/model/OptimizeProgress.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.model.OptimizeResult.PackageOptimizeResult;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import com.android.internal.annotations.Immutable;
-
-import com.google.auto.value.AutoValue;
-
-/** @hide */
-@Immutable
-@AutoValue
-public abstract class OptimizeProgress {
-    /** @hide */
-    protected OptimizeProgress() {}
-
-    /** @hide */
-    public static @NonNull OptimizeProgress create(int donePackageCount, int totalPackageCount) {
-        return new AutoValue_OptimizeProgress(donePackageCount, totalPackageCount);
-    }
-
-    /**
-     * The number of packages, for which optimization has been done, regardless of the results
-     * (performed, failed, skipped, etc.). Can be 0, which means the optimization was just started.
-     */
-    public abstract int getDonePackageCount();
-
-    /**
-     * The total number of packages to optimize. Stays constant during the operation.
-     */
-    public abstract int getTotalPackageCount();
-}
diff --git a/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java b/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
index c090bd5..59af8be 100644
--- a/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexOptHelperTest.java
@@ -45,8 +45,8 @@
 
 import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.Config;
+import com.android.server.art.model.OperationProgress;
 import com.android.server.art.model.OptimizeParams;
-import com.android.server.art.model.OptimizeProgress;
 import com.android.server.art.model.OptimizeResult;
 import com.android.server.pm.PackageManagerLocal;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -512,7 +512,7 @@
         // Delay the executor to verify that the commands passed to the executor are not bound to
         // changing variables.
         var progressCallbackExecutor = new DelayedExecutor();
-        Consumer<OptimizeProgress> progressCallback = mock(Consumer.class);
+        Consumer<OperationProgress> progressCallback = mock(Consumer.class);
 
         mDexOptHelper.dexopt(mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor,
                 progressCallbackExecutor, progressCallback);
@@ -521,17 +521,13 @@
 
         InOrder inOrder = inOrder(progressCallback);
         inOrder.verify(progressCallback)
-                .accept(eq(OptimizeProgress.create(
-                        0 /* donePackageCount */, 3 /* totalPackageCount */)));
+                .accept(eq(OperationProgress.create(0 /* current */, 3 /* total */)));
         inOrder.verify(progressCallback)
-                .accept(eq(OptimizeProgress.create(
-                        1 /* donePackageCount */, 3 /* totalPackageCount */)));
+                .accept(eq(OperationProgress.create(1 /* current */, 3 /* total */)));
         inOrder.verify(progressCallback)
-                .accept(eq(OptimizeProgress.create(
-                        2 /* donePackageCount */, 3 /* totalPackageCount */)));
+                .accept(eq(OperationProgress.create(2 /* current */, 3 /* total */)));
         inOrder.verify(progressCallback)
-                .accept(eq(OptimizeProgress.create(
-                        3 /* donePackageCount */, 3 /* totalPackageCount */)));
+                .accept(eq(OperationProgress.create(3 /* current */, 3 /* total */)));
     }
 
     private AndroidPackage createPackage(boolean multiSplit) {