ART services: get optimization status.

- Implements the functionality of getting the optimization status of
  primary dex'es (except the real artd implementation).
- Defines an artd internal API for getting the optimization status.
- Adds a shell command (`get-optimization-status`).

Bug: 233383589
Test: atest ArtServiceTests
Test: adb shell pm art get-optimization-status com.google.android.youtube
Ignore-AOSP-First: ART Services
Change-Id: I4128a2c8a618c348954a81b22c21f0bd0944496d
diff --git a/artd/artd.cc b/artd/artd.cc
index 16127b0..89b9421 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -33,6 +33,7 @@
 
 using ::aidl::com::android::server::art::ArtifactsPath;
 using ::aidl::com::android::server::art::BnArtd;
+using ::aidl::com::android::server::art::GetOptimizationStatusResult;
 using ::android::base::Error;
 using ::android::base::Result;
 using ::ndk::ScopedAStatus;
@@ -55,6 +56,17 @@
     return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
   }
 
+  ScopedAStatus getOptimizationStatus(const std::string& in_dexFile,
+                                      const std::string& in_instructionSet,
+                                      const std::string& in_classLoaderContext,
+                                      GetOptimizationStatusResult* _aidl_return) override {
+    (void)in_dexFile;
+    (void)in_instructionSet;
+    (void)in_classLoaderContext;
+    (void)_aidl_return;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+  }
+
   Result<void> Start() {
     LOG(INFO) << "Starting artd";
 
diff --git a/artd/binder/com/android/server/art/GetOptimizationStatusResult.aidl b/artd/binder/com/android/server/art/GetOptimizationStatusResult.aidl
new file mode 100644
index 0000000..99a2e37
--- /dev/null
+++ b/artd/binder/com/android/server/art/GetOptimizationStatusResult.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * The result of {@code IArtd.getOptimizationStatus}. Each field corresponds to a field in
+ * {@code com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus}.
+ *
+ * @hide
+ */
+parcelable GetOptimizationStatusResult {
+    @utf8InCpp String compilerFilter;
+    @utf8InCpp String compilationReason;
+    @utf8InCpp String locationDebugString;
+}
diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl
index 8fc1660..a1df266 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -23,4 +23,9 @@
 
     /** Deletes artifacts and returns the released space, in bytes. */
     long deleteArtifacts(in com.android.server.art.ArtifactsPath artifactsPath);
+
+    /** Returns the optimization status of a dex file. */
+    com.android.server.art.GetOptimizationStatusResult getOptimizationStatus(
+            @utf8InCpp String dexFile, @utf8InCpp String instructionSet,
+            @utf8InCpp String classLoaderContext);
 }
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 3a50d3d..19e529a 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -16,7 +16,9 @@
 
 package com.android.server.art;
 
+import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo;
 import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo;
+import static com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus;
 
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
@@ -28,12 +30,16 @@
 import com.android.server.art.IArtd;
 import com.android.server.art.model.DeleteOptions;
 import com.android.server.art.model.DeleteResult;
+import com.android.server.art.model.GetStatusOptions;
+import com.android.server.art.model.OptimizationStatus;
 import com.android.server.art.wrapper.AndroidPackageApi;
 import com.android.server.art.wrapper.PackageDataSnapshot;
 import com.android.server.art.wrapper.PackageManagerLocal;
 import com.android.server.art.wrapper.PackageState;
 
 import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * This class provides a system API for functionality provided by the ART module.
@@ -97,15 +103,8 @@
             throw new IllegalArgumentException("Nothing to delete");
         }
 
-        PackageState pkgState = mInjector.getPackageManagerLocal().getPackageState(
-                snapshot, Binder.getCallingUid(), packageName);
-        if (pkgState == null) {
-            throw new IllegalArgumentException("Package not found: " + packageName);
-        }
-        AndroidPackageApi pkg = pkgState.getAndroidPackage();
-        if (pkg == null) {
-            throw new IllegalStateException("Unable to get package " + pkgState.getPackageName());
-        }
+        PackageState pkgState = getPackageStateOrThrow(snapshot, packageName);
+        AndroidPackageApi pkg = getPackageOrThrow(pkgState);
 
         try {
             long freedBytes = 0;
@@ -136,6 +135,74 @@
     }
 
     /**
+     * Returns the optimization status of a package.
+     *
+     * @throws IllegalArgumentException if the package is not found or the options are illegal
+     * @throws IllegalStateException if an internal error occurs
+     *
+     * @hide
+     */
+    @NonNull
+    public OptimizationStatus getOptimizationStatus(@NonNull PackageDataSnapshot snapshot,
+            @NonNull String packageName, @NonNull GetStatusOptions options) {
+        if (!options.isForPrimaryDex() && !options.isForSecondaryDex()) {
+            throw new IllegalArgumentException("Nothing to check");
+        }
+
+        PackageState pkgState = getPackageStateOrThrow(snapshot, packageName);
+        AndroidPackageApi pkg = getPackageOrThrow(pkgState);
+
+        try {
+            List<DexFileOptimizationStatus> statuses = new ArrayList<>();
+
+            if (options.isForPrimaryDex()) {
+                for (DetailedPrimaryDexInfo dexInfo :
+                        PrimaryDexUtils.getDetailedDexInfo(pkgState, pkg)) {
+                    if (!dexInfo.hasCode()) {
+                        continue;
+                    }
+                    for (String isa : Utils.getAllIsas(pkgState)) {
+                        GetOptimizationStatusResult result =
+                                mInjector.getArtd().getOptimizationStatus(
+                                        dexInfo.dexPath(), isa, dexInfo.classLoaderContext());
+                        statuses.add(new DexFileOptimizationStatus(dexInfo.dexPath(), isa,
+                                result.compilerFilter, result.compilationReason,
+                                result.locationDebugString));
+                    }
+                }
+            }
+
+            if (options.isForSecondaryDex()) {
+                // TODO(jiakaiz): Implement this.
+                throw new UnsupportedOperationException(
+                        "Getting optimization status of secondary dex'es is not implemented yet");
+            }
+
+            return new OptimizationStatus(statuses);
+        } catch (RemoteException e) {
+            throw new IllegalStateException("An error occurred when calling artd", e);
+        }
+    }
+
+    private PackageState getPackageStateOrThrow(
+            @NonNull PackageDataSnapshot snapshot, @NonNull String packageName) {
+        PackageState pkgState = mInjector.getPackageManagerLocal().getPackageState(
+                snapshot, Binder.getCallingUid(), packageName);
+        if (pkgState == null) {
+            throw new IllegalArgumentException("Package not found: " + packageName);
+        }
+        return pkgState;
+    }
+
+    private AndroidPackageApi getPackageOrThrow(@NonNull PackageState pkgState) {
+        AndroidPackageApi pkg = pkgState.getAndroidPackage();
+        if (pkg == null) {
+            throw new IllegalStateException("Unable to get package " + pkgState.getPackageName());
+        }
+        return pkg;
+    }
+
+    /**
      * Injector pattern for testing purpose.
      *
      * @hide
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index ab8330f..ba01ed5 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -16,12 +16,16 @@
 
 package com.android.server.art;
 
+import static com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus;
+
 import android.os.Binder;
 import android.os.Process;
 
 import com.android.modules.utils.BasicShellCommandHandler;
 import com.android.server.art.model.DeleteOptions;
 import com.android.server.art.model.DeleteResult;
+import com.android.server.art.model.GetStatusOptions;
+import com.android.server.art.model.OptimizationStatus;
 import com.android.server.art.wrapper.PackageDataSnapshot;
 import com.android.server.art.wrapper.PackageManagerLocal;
 
@@ -55,6 +59,18 @@
                         snapshot, getNextArgRequired(), new DeleteOptions.Builder().build());
                 pw.printf("Freed %d bytes\n", result.getFreedBytes());
                 return 0;
+            case "get-optimization-status":
+                OptimizationStatus optimizationStatus = mArtManagerLocal.getOptimizationStatus(
+                        snapshot, getNextArgRequired(), new GetStatusOptions.Builder().build());
+                for (DexFileOptimizationStatus status :
+                        optimizationStatus.getDexFileOptimizationStatuses()) {
+                    pw.printf("dexFile = %s, instructionSet = %s, compilerFilter = %s, "
+                                    + "compilationReason = %s, locationDebugString = %s\n",
+                            status.getDexFile(), status.getInstructionSet(),
+                            status.getCompilerFilter(), status.getCompilationReason(),
+                            status.getLocationDebugString());
+                }
+                return 0;
             default:
                 // Handles empty, help, and invalid commands.
                 return handleDefaultCommands(cmd);
@@ -78,6 +94,10 @@
         pw.println("    Delete the optimized artifacts of a package.");
         pw.println("    By default, the command only deletes the optimized artifacts of primary "
                 + "dex'es.");
+        pw.println("  get-optimization-status <package-name>");
+        pw.println("    Print the optimization status of a package.");
+        pw.println("    By default, the command only prints the optimization status of primary "
+                + "dex'es.");
     }
 
     private void enforceRoot() {
diff --git a/libartservice/service/java/com/android/server/art/LoggingArtd.java b/libartservice/service/java/com/android/server/art/LoggingArtd.java
index a29144a..811cb6f 100644
--- a/libartservice/service/java/com/android/server/art/LoggingArtd.java
+++ b/libartservice/service/java/com/android/server/art/LoggingArtd.java
@@ -43,6 +43,15 @@
         return 0;
     }
 
+    @Override
+    public GetOptimizationStatusResult getOptimizationStatus(
+            String dexFile, String instructionSet, String classLoaderContext) {
+        Log.i(TAG,
+                "getOptimizationStatus " + dexFile + ", " + instructionSet + ", "
+                        + classLoaderContext);
+        return new GetOptimizationStatusResult();
+    }
+
     private String artifactsPathToString(ArtifactsPath artifactsPath) {
         return String.format("ArtifactsPath{dexPath = \"%s\", isa = \"%s\", isInDalvikCache = %s}",
                 artifactsPath.dexPath, artifactsPath.isa,
diff --git a/libartservice/service/java/com/android/server/art/model/GetStatusOptions.java b/libartservice/service/java/com/android/server/art/model/GetStatusOptions.java
new file mode 100644
index 0000000..22b0131
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/GetStatusOptions.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+/** @hide */
+public class GetStatusOptions {
+    public static final class Builder {
+        private GetStatusOptions mGetStatusOptions = new GetStatusOptions();
+
+        /** Whether to get optimization status of primary dex'es. Default: true. */
+        public Builder setForPrimaryDex(boolean value) {
+            mGetStatusOptions.mIsForPrimaryDex = value;
+            return this;
+        }
+
+        /** Whether to get optimization status of secondary dex'es. Default: false. */
+        public Builder setForSecondaryDex(boolean value) {
+            mGetStatusOptions.mIsForSecondaryDex = value;
+            return this;
+        }
+
+        /** Returns the built object. */
+        public GetStatusOptions build() {
+            return mGetStatusOptions;
+        }
+    }
+
+    private boolean mIsForPrimaryDex = true;
+    private boolean mIsForSecondaryDex = false;
+
+    private GetStatusOptions() {}
+
+    /** Whether to get optimization status of primary dex'es. */
+    public boolean isForPrimaryDex() {
+        return mIsForPrimaryDex;
+    }
+
+    /** Whether to get optimization status of secondary dex'es. */
+    public boolean isForSecondaryDex() {
+        return mIsForSecondaryDex;
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/model/OptimizationStatus.java b/libartservice/service/java/com/android/server/art/model/OptimizationStatus.java
new file mode 100644
index 0000000..4b6bae2
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/OptimizationStatus.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.art.model;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.Immutable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Describes the optimization status of a package.
+ *
+ * @hide
+ */
+@Immutable
+public class OptimizationStatus {
+    private final @NonNull List<DexFileOptimizationStatus> mDexFileOptimizationStatuses;
+
+    /** @hide */
+    public OptimizationStatus(
+            @NonNull List<DexFileOptimizationStatus> dexFileOptimizationStatuses) {
+        mDexFileOptimizationStatuses = dexFileOptimizationStatuses;
+    }
+
+    /** The optimization status of each individual dex file. */
+    @NonNull
+    public List<DexFileOptimizationStatus> getDexFileOptimizationStatuses() {
+        return mDexFileOptimizationStatuses;
+    }
+
+    /** Describes the optimization status of a dex file. */
+    @Immutable
+    public static class DexFileOptimizationStatus {
+        private final @NonNull String mDexFile;
+        private final @NonNull String mInstructionSet;
+        private final @NonNull String mCompilerFilter;
+        private final @NonNull String mCompilationReason;
+        private final @NonNull String mLocationDebugString;
+
+        /** @hide */
+        public DexFileOptimizationStatus(@NonNull String dexFile, @NonNull String instructionSet,
+                @NonNull String compilerFilter, @NonNull String compilationReason,
+                @NonNull String locationDebugString) {
+            mDexFile = dexFile;
+            mInstructionSet = instructionSet;
+            mCompilerFilter = compilerFilter;
+            mCompilationReason = compilationReason;
+            mLocationDebugString = locationDebugString;
+        }
+
+        /** The absolute path to the dex file. */
+        public @NonNull String getDexFile() {
+            return mDexFile;
+        }
+
+        /** The instruction set. */
+        public @NonNull String getInstructionSet() {
+            return mInstructionSet;
+        }
+
+        /**
+         * A string that describes the compiler filter.
+         *
+         * Possible values are:
+         * <ul>
+         *   <li>A valid value of the {@code --compiler-filer} option passed to {@code dex2oat}, if
+         *     the optimized artifacts are valid.
+         *   <li>{@code "run-from-apk"}, if the optimized artifacts do not exist.
+         *   <li>{@code "run-from-apk-fallback"}, if the optimized artifacts exist but are invalid
+         *     because the dex file has changed.
+         *   <li>{@code "error"}, if an unexpected error occurs.
+         * </ul>
+         */
+        public @NonNull String getCompilerFilter() {
+            return mCompilerFilter;
+        }
+
+        /**
+         * A string that describes the compilation reason.
+         *
+         * Possible values are:
+         * <ul>
+         *   <li>The compilation reason, in text format, passed to {@code dex2oat}.
+         *   <li>{@code "unknown"}: if the reason is empty or the optimized artifacts do not exist.
+         *   <li>{@code "error"}: if an unexpected error occurs.
+         * </ul>
+         */
+        public @NonNull String getCompilationReason() {
+            return mCompilationReason;
+        }
+
+        /**
+         * A human-readable string that describes the location of the optimized artifacts.
+         *
+         * Note that this string is for debugging purposes only. There is no stability guarantees
+         * for the format of the string. DO NOT use it programmatically.
+         */
+        public @NonNull String getLocationDebugString() {
+            return mLocationDebugString;
+        }
+    }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index 249de3c..bbfa5f9 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.art;
 
+import static com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.any;
@@ -34,6 +36,8 @@
 
 import com.android.server.art.model.DeleteOptions;
 import com.android.server.art.model.DeleteResult;
+import com.android.server.art.model.GetStatusOptions;
+import com.android.server.art.model.OptimizationStatus;
 import com.android.server.art.wrapper.AndroidPackageApi;
 import com.android.server.art.wrapper.PackageDataSnapshot;
 import com.android.server.art.wrapper.PackageManagerLocal;
@@ -131,6 +135,65 @@
                 mock(PackageDataSnapshot.class), PKG_NAME, new DeleteOptions.Builder().build());
     }
 
+    @Test
+    public void testGetOptimizationStatus() throws Exception {
+        when(mArtd.getOptimizationStatus(any(), any(), any()))
+                .thenReturn(createGetOptimizationStatusResult(
+                                    "speed", "compilation-reason-0", "location-debug-string-0"),
+                        createGetOptimizationStatusResult(
+                                "speed-profile", "compilation-reason-1", "location-debug-string-1"),
+                        createGetOptimizationStatusResult(
+                                "verify", "compilation-reason-2", "location-debug-string-2"),
+                        createGetOptimizationStatusResult(
+                                "extract", "compilation-reason-3", "location-debug-string-3"));
+
+        OptimizationStatus result = mArtManagerLocal.getOptimizationStatus(
+                mock(PackageDataSnapshot.class), PKG_NAME, new GetStatusOptions.Builder().build());
+
+        List<DexFileOptimizationStatus> statuses = result.getDexFileOptimizationStatuses();
+        assertThat(statuses.size()).isEqualTo(4);
+
+        assertThat(statuses.get(0).getDexFile()).isEqualTo("/data/app/foo/base.apk");
+        assertThat(statuses.get(0).getInstructionSet()).isEqualTo("arm64");
+        assertThat(statuses.get(0).getCompilerFilter()).isEqualTo("speed");
+        assertThat(statuses.get(0).getCompilationReason()).isEqualTo("compilation-reason-0");
+        assertThat(statuses.get(0).getLocationDebugString()).isEqualTo("location-debug-string-0");
+
+        assertThat(statuses.get(1).getDexFile()).isEqualTo("/data/app/foo/base.apk");
+        assertThat(statuses.get(1).getInstructionSet()).isEqualTo("arm");
+        assertThat(statuses.get(1).getCompilerFilter()).isEqualTo("speed-profile");
+        assertThat(statuses.get(1).getCompilationReason()).isEqualTo("compilation-reason-1");
+        assertThat(statuses.get(1).getLocationDebugString()).isEqualTo("location-debug-string-1");
+
+        assertThat(statuses.get(2).getDexFile()).isEqualTo("/data/app/foo/split_0.apk");
+        assertThat(statuses.get(2).getInstructionSet()).isEqualTo("arm64");
+        assertThat(statuses.get(2).getCompilerFilter()).isEqualTo("verify");
+        assertThat(statuses.get(2).getCompilationReason()).isEqualTo("compilation-reason-2");
+        assertThat(statuses.get(2).getLocationDebugString()).isEqualTo("location-debug-string-2");
+
+        assertThat(statuses.get(3).getDexFile()).isEqualTo("/data/app/foo/split_0.apk");
+        assertThat(statuses.get(3).getInstructionSet()).isEqualTo("arm");
+        assertThat(statuses.get(3).getCompilerFilter()).isEqualTo("extract");
+        assertThat(statuses.get(3).getCompilationReason()).isEqualTo("compilation-reason-3");
+        assertThat(statuses.get(3).getLocationDebugString()).isEqualTo("location-debug-string-3");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testGetOptimizationStatusPackageNotFound() throws Exception {
+        when(mPackageManagerLocal.getPackageState(any(), anyInt(), eq(PKG_NAME))).thenReturn(null);
+
+        mArtManagerLocal.getOptimizationStatus(
+                mock(PackageDataSnapshot.class), PKG_NAME, new GetStatusOptions.Builder().build());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testGetOptimizationStatusNoPackage() throws Exception {
+        when(mPkgState.getAndroidPackage()).thenReturn(null);
+
+        mArtManagerLocal.getOptimizationStatus(
+                mock(PackageDataSnapshot.class), PKG_NAME, new GetStatusOptions.Builder().build());
+    }
+
     private AndroidPackageApi createPackage() {
         AndroidPackageApi pkg = mock(AndroidPackageApi.class);
 
@@ -163,4 +226,13 @@
 
         return pkgState;
     }
+
+    private GetOptimizationStatusResult createGetOptimizationStatusResult(
+            String compilerFilter, String compilationReason, String locationDebugString) {
+        var getOptimizationStatusResult = new GetOptimizationStatusResult();
+        getOptimizationStatusResult.compilerFilter = compilerFilter;
+        getOptimizationStatusResult.compilationReason = compilationReason;
+        getOptimizationStatusResult.locationDebugString = locationDebugString;
+        return getOptimizationStatusResult;
+    }
 }