Implement 'pm force-dex-opt' and 'pm bg-dexopt-job'.

Deprecated syntaxes are left out from the help text to discourage
usages.

Bug: 263247832
Test: adb shell pm force-dex-opt com.google.android.youtube
Test: adb shell pm bg-dexopt-job com.android.systemui com.android.chrome (warning)
Test: adb shell pm bg-dexopt-job
Test: adb shell pm bg-dexopt-job --cancel
Test: adb shell pm bg-dexopt-job --disable
Test: adb shell pm bg-dexopt-job --enable
Ignore-AOSP-First: ART Services.
Change-Id: Ie49da3a63c99c8b5defcd1114b8c776e0684a037
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index aaed1f4..e670dd2 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -81,6 +81,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -611,6 +612,26 @@
     }
 
     /**
+     * Same as above, but also returns a {@link CompletableFuture}.
+     *
+     * @hide
+     */
+    @NonNull
+    public CompletableFuture<BackgroundDexoptJob.Result> startBackgroundDexoptJobAndReturnFuture() {
+        return mInjector.getBackgroundDexoptJob().start();
+    }
+
+    /**
+     * Returns the running background dexopt job, or null of no background dexopt job is running.
+     *
+     * @hide
+     */
+    @Nullable
+    public CompletableFuture<BackgroundDexoptJob.Result> getRunningBackgroundDexoptJob() {
+        return mInjector.getBackgroundDexoptJob().get();
+    }
+
+    /**
      * Cancels the running background dexopt job started by the job scheduler or by {@link
      * #startBackgroundDexoptJob()}. Does nothing if the job is not running. This method is not
      * blocking.
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index a70d45b..378d736f 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -63,10 +63,12 @@
 import java.io.PrintWriter;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
@@ -108,9 +110,16 @@
                 case "compile":
                     return handleCompile(pw, snapshot);
                 case "reconcile-secondary-dex-files":
+                    // TODO(b/263247832): Implement this.
+                    throw new UnsupportedOperationException();
                 case "force-dex-opt":
+                    return handleForceDexopt(pw, snapshot);
                 case "bg-dexopt-job":
+                    return handleBgDexoptJob(pw, snapshot);
                 case "cancel-bg-dexopt-job":
+                    pw.println("Warning: 'pm cancel-bg-dexopt-job' is deprecated. It is now an "
+                            + "alias of 'pm bg-dexopt-job --cancel'");
+                    return handleCancelBgDexoptJob(pw);
                 case "delete-dexopt":
                 case "dump-profiles":
                 case "snapshot-profile":
@@ -197,43 +206,6 @@
                 pw.println(mDexUseManager.dump());
                 return 0;
             }
-            case "bg-dexopt-job": {
-                enforceRoot();
-                String opt = getNextOption();
-                if (opt == null) {
-                    mArtManagerLocal.startBackgroundDexoptJob();
-                    return 0;
-                }
-                switch (opt) {
-                    case "--cancel": {
-                        mArtManagerLocal.cancelBackgroundDexoptJob();
-                        return 0;
-                    }
-                    case "--enable": {
-                        // This operation requires the uid to be "system" (1000).
-                        long identityToken = Binder.clearCallingIdentity();
-                        try {
-                            mArtManagerLocal.scheduleBackgroundDexoptJob();
-                        } finally {
-                            Binder.restoreCallingIdentity(identityToken);
-                        }
-                        return 0;
-                    }
-                    case "--disable": {
-                        // This operation requires the uid to be "system" (1000).
-                        long identityToken = Binder.clearCallingIdentity();
-                        try {
-                            mArtManagerLocal.unscheduleBackgroundDexoptJob();
-                        } finally {
-                            Binder.restoreCallingIdentity(identityToken);
-                        }
-                        return 0;
-                    }
-                    default:
-                        pw.println("Error: Unknown option: " + opt);
-                        return 1;
-                }
-            }
             case "snapshot-app-profile": {
                 enforceRoot();
                 String packageName = getNextArgRequired();
@@ -473,6 +445,102 @@
         }
     }
 
+    private int handleForceDexopt(
+            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
+        pw.println("Warning: 'pm force-dex-opt' is deprecated. Please use 'pm compile "
+                + "-f PACKAGE_NAME' instead");
+        return dexoptPackages(pw, snapshot, List.of(getNextArgRequired()),
+                new DexoptParams.Builder(ReasonMapping.REASON_CMDLINE)
+                        .setFlags(ArtFlags.FLAG_FORCE, ArtFlags.FLAG_FORCE)
+                        .setFlags(ArtFlags.FLAG_FOR_PRIMARY_DEX
+                                        | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
+                                ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX
+                                        | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
+                        .build());
+    }
+
+    private int handleBgDexoptJob(
+            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
+        String opt = getNextOption();
+        if (opt == null) {
+            List<String> packageNames = new ArrayList<>();
+            String arg;
+            while ((arg = getNextArg()) != null) {
+                packageNames.add(arg);
+            }
+            if (!packageNames.isEmpty()) {
+                pw.println("Warning: Running 'pm bg-dexopt-job' with package names is deprecated. "
+                        + "Please use 'pm compile -r bg-dexopt PACKAGE_NAME' instead");
+                return dexoptPackages(pw, snapshot, packageNames,
+                        new DexoptParams.Builder(ReasonMapping.REASON_BG_DEXOPT).build());
+            }
+
+            CompletableFuture<BackgroundDexoptJob.Result> runningJob =
+                    mArtManagerLocal.getRunningBackgroundDexoptJob();
+            if (runningJob != null) {
+                pw.println("Another job already running. Waiting for it to finish... To cancel it, "
+                        + "run 'pm bg-dexopt-job --cancel'. in a separate shell.");
+                pw.flush();
+                Utils.getFuture(runningJob);
+            }
+            CompletableFuture<BackgroundDexoptJob.Result> future =
+                    mArtManagerLocal.startBackgroundDexoptJobAndReturnFuture();
+            pw.println("Job running...  To cancel it, run 'pm bg-dexopt-job --cancel'. in a "
+                    + "separate shell.");
+            pw.flush();
+            BackgroundDexoptJob.Result result = Utils.getFuture(future);
+            if (result instanceof BackgroundDexoptJob.CompletedResult) {
+                var completedResult = (BackgroundDexoptJob.CompletedResult) result;
+                if (completedResult.dexoptResult().getFinalStatus()
+                        == DexoptResult.DEXOPT_CANCELLED) {
+                    pw.println("Job cancelled. See logs for details");
+                } else {
+                    pw.println("Job finished. See logs for details");
+                }
+            } else if (result instanceof BackgroundDexoptJob.FatalErrorResult) {
+                // Never expected.
+                pw.println("Job encountered a fatal error");
+            }
+            return 0;
+        }
+        switch (opt) {
+            case "--cancel": {
+                return handleCancelBgDexoptJob(pw);
+            }
+            case "--enable": {
+                // This operation requires the uid to be "system" (1000).
+                long identityToken = Binder.clearCallingIdentity();
+                try {
+                    mArtManagerLocal.scheduleBackgroundDexoptJob();
+                } finally {
+                    Binder.restoreCallingIdentity(identityToken);
+                }
+                pw.println("Background dexopt job enabled");
+                return 0;
+            }
+            case "--disable": {
+                // This operation requires the uid to be "system" (1000).
+                long identityToken = Binder.clearCallingIdentity();
+                try {
+                    mArtManagerLocal.unscheduleBackgroundDexoptJob();
+                } finally {
+                    Binder.restoreCallingIdentity(identityToken);
+                }
+                pw.println("Background dexopt job disabled");
+                return 0;
+            }
+            default:
+                pw.println("Error: Unknown option: " + opt);
+                return 1;
+        }
+    }
+
+    private int handleCancelBgDexoptJob(@NonNull PrintWriter pw) {
+        mArtManagerLocal.cancelBackgroundDexoptJob();
+        pw.println("Background dexopt job cancelled");
+        return 0;
+    }
+
     @Override
     public void onHelp() {
         // No one should call this. The help text should be printed by the `onHelp` handler of `cmd
@@ -524,6 +592,37 @@
         pw.println("    Note: If none of the scope options above are set, the scope defaults to");
         pw.println("    '--primary-dex --include-dependencies'.");
         pw.println();
+        pw.println("bg-dexopt-job [--cancel | --disable | --enable]");
+        pw.println("  Control the background dexopt job.");
+        pw.println("  Without flags, it starts a background dexopt job immediately and waits for");
+        pw.println("    it to finish. If a job is already started either automatically by the");
+        pw.println("    system or through this command, it will wait for the running job to");
+        pw.println("    finish and then start a new one.");
+        pw.println("  Different from 'pm compile -r bg-dexopt -a', the behavior of this command");
+        pw.println("  is the same as a real background dexopt job. Specifically,");
+        pw.println("    - It only dexopts a subset of apps determined by either the system's");
+        pw.println("      default logic based on app usage data or the custom logic specified by");
+        pw.println("      the 'ArtManagerLocal.setBatchDexoptStartCallback' Java API.");
+        pw.println("    - It runs dexopt in parallel, where the concurrency setting is specified");
+        pw.println("      by the system property 'pm.dexopt.bg-dexopt.concurrency'.");
+        pw.println("    - If the storage is low, it also downgrades unused apps.");
+        pw.println("    - It also cleans up obsolete files.");
+        pw.println("  Options:");
+        pw.println("    --cancel Cancel any currently running background dexopt job immediately.");
+        pw.println("      This cancels jobs started either automatically by the system or through");
+        pw.println("      this command. This command is not blocking.");
+        pw.println("    --disable: Disable the background dexopt job from being started by the");
+        pw.println("      job scheduler. If a job is already started by the job scheduler and is");
+        pw.println("      running, it will be cancelled immediately. Does not affect jobs started");
+        pw.println("      through this command or by the system in other ways.");
+        pw.println("      This state will be lost when the system_server process exits.");
+        pw.println("    --enable: Enable the background dexopt job to be started by the job");
+        pw.println("      scheduler again, if previously disabled by --disable.");
+        pw.println("  When a list of package names is passed, this command does NOT start a real");
+        pw.println("  background dexopt job. Instead, it dexopts the given packages sequentially.");
+        pw.println("  This usage is deprecated. Please use 'pm compile -r bg-dexopt PACKAGE_NAME'");
+        pw.println("  instead.");
+        pw.println();
         pw.println("art SUB_COMMAND [ARGS]...");
         pw.println("  Run ART Service commands");
         pw.println();
@@ -565,23 +664,6 @@
         pw.println("  dex-use-dump");
         pw.println("    Print all dex use information in textproto format.");
         pw.println();
-        pw.println("  bg-dexopt-job [--cancel | --disable | --enable]");
-        pw.println("    Control the background dexopt job.");
-        pw.println("    Without flags, it starts a background dexopt job immediately. It does");
-        pw.println("      nothing if a job is already started either automatically by the system");
-        pw.println("      or through this command. This command is not blocking.");
-        pw.println("    Options:");
-        pw.println("      --cancel Cancel any currently running background dexopt job");
-        pw.println("        immediately. This cancels jobs started either automatically by the");
-        pw.println("        system or through this command. This command is not blocking.");
-        pw.println("      --disable: Disable the background dexopt job from being started by the");
-        pw.println("        job scheduler. If a job is already started by the job scheduler and");
-        pw.println("        is running, it will be cancelled immediately. Does not affect");
-        pw.println("        jobs started through this command or by the system in other ways.");
-        pw.println("        This state will be lost when the system_server process exits.");
-        pw.println("      --enable: Enable the background dexopt job to be started by the job");
-        pw.println("        scheduler again, if previously disabled by --disable.");
-        pw.println();
         pw.println("  snapshot-app-profile PACKAGE_NAME [SPLIT_NAME]");
         pw.println("    Snapshot the profile of the given app and save it to");
         pw.println("    '" + PROFILE_DEBUG_LOCATION + "'.");
diff --git a/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java b/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java
index 9298928..e2332e3 100644
--- a/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java
+++ b/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java
@@ -193,6 +193,11 @@
         Log.i(TAG, "Job cancelled");
     }
 
+    @Nullable
+    public synchronized CompletableFuture<Result> get() {
+        return mRunningJob;
+    }
+
     @NonNull
     private CompletedResult run(@NonNull CancellationSignal cancellationSignal) {
         // TODO(b/254013427): Cleanup dex use info.