Revert "Disable test 426 on redefinition configurations." am: 404e8881c2 am: 6d80c2dce9 am: 1eaabbc72d am: c976852283 am: 754cf11db9

Original change: https://android-review.googlesource.com/c/platform/art/+/2139994

Change-Id: I2af680cea64026272de7c5f45107b338602ef2af
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/artd/Android.bp b/artd/Android.bp
index b645ec6..292f09b 100644
--- a/artd/Android.bp
+++ b/artd/Android.bp
@@ -27,6 +27,7 @@
     defaults: ["art_defaults"],
     srcs: [
         "artd.cc",
+        "path_utils.cc",
     ],
     shared_libs: [
         "libarttools",
@@ -35,6 +36,7 @@
     ],
     static_libs: [
         "artd-aidl-ndk",
+        "libc++fs",
     ],
 }
 
@@ -45,6 +47,7 @@
         "artd_main.cc",
     ],
     shared_libs: [
+        "libart",
         "libartbase",
     ],
     apex_available: [
@@ -63,8 +66,13 @@
 art_cc_defaults {
     name: "art_artd_tests_defaults",
     defaults: ["artd_defaults"],
+    static_libs: [
+        "libcap",
+        "libgmock",
+    ],
     srcs: [
         "artd_test.cc",
+        "path_utils_test.cc",
     ],
 }
 
diff --git a/artd/artd.cc b/artd/artd.cc
index 27a609d..392f587 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -16,16 +16,32 @@
 
 #include "artd.h"
 
+#include <stdlib.h>
 #include <unistd.h>
 
+#include <cstdint>
+#include <filesystem>
+#include <memory>
 #include <string>
+#include <system_error>
+#include <utility>
+#include <vector>
 
 #include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/errors.h"
 #include "android-base/logging.h"
+#include "android-base/properties.h"
 #include "android-base/result.h"
+#include "android-base/stringprintf.h"
+#include "android-base/strings.h"
 #include "android/binder_auto_utils.h"
 #include "android/binder_manager.h"
 #include "android/binder_process.h"
+#include "base/array_ref.h"
+#include "base/file_utils.h"
+#include "oat_file_assistant.h"
+#include "path_utils.h"
+#include "runtime.h"
 #include "tools/tools.h"
 
 namespace art {
@@ -33,12 +49,77 @@
 
 namespace {
 
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::aidl::com::android::server::art::GetOptimizationStatusResult;
 using ::android::base::Error;
+using ::android::base::GetBoolProperty;
 using ::android::base::Result;
+using ::android::base::Split;
+using ::android::base::StringPrintf;
 using ::ndk::ScopedAStatus;
 
 constexpr const char* kServiceName = "artd";
 
+constexpr const char* kPhenotypeFlagPrefix = "persist.device_config.runtime_native_boot.";
+constexpr const char* kDalvikVmFlagPrefix = "dalvik.vm.";
+
+Result<std::vector<std::string>> GetBootClassPath() {
+  const char* env_value = getenv("BOOTCLASSPATH");
+  if (env_value == nullptr || strlen(env_value) == 0) {
+    return Errorf("Failed to get environment variable 'BOOTCLASSPATH'");
+  }
+  return Split(env_value, ":");
+}
+
+Result<std::vector<std::string>> GetBootImageLocations(bool deny_art_apex_data_files) {
+  std::string error_msg;
+  std::string android_root = GetAndroidRootSafe(&error_msg);
+  if (!error_msg.empty()) {
+    return Errorf("Failed to get ANDROID_ROOT: {}", error_msg);
+  }
+
+  std::string location_str = GetDefaultBootImageLocation(android_root, deny_art_apex_data_files);
+  return Split(location_str, ":");
+}
+
+bool UseJitZygote() {
+  bool profile_boot_class_path_phenotype =
+      GetBoolProperty(std::string(kPhenotypeFlagPrefix) + "profilebootclasspath",
+                      /*default_value=*/false);
+
+  bool profile_boot_class_path =
+      GetBoolProperty(std::string(kDalvikVmFlagPrefix) + "profilebootclasspath",
+                      /*default_value=*/profile_boot_class_path_phenotype);
+
+  return profile_boot_class_path;
+}
+
+bool DenyArtApexDataFiles() {
+  return !GetBoolProperty("odsign.verification.success", /*default_value=*/false);
+}
+
+// Deletes a file. Returns the size of the deleted file, or 0 if the deleted file is empty or an
+// error occurs.
+int64_t GetSizeAndDeleteFile(const std::string& path) {
+  std::error_code ec;
+  int64_t size = std::filesystem::file_size(path, ec);
+  if (ec) {
+    // It is okay if the file does not exist. We don't have to log it.
+    if (ec.value() != ENOENT) {
+      LOG(ERROR) << StringPrintf(
+          "Failed to get the file size of '%s': %s", path.c_str(), ec.message().c_str());
+    }
+    return 0;
+  }
+
+  if (!std::filesystem::remove(path, ec)) {
+    LOG(ERROR) << StringPrintf("Failed to remove '%s': %s", path.c_str(), ec.message().c_str());
+    return 0;
+  }
+
+  return size;
+}
+
 }  // namespace
 
 ScopedAStatus Artd::isAlive(bool* _aidl_return) {
@@ -46,6 +127,62 @@
   return ScopedAStatus::ok();
 }
 
+ScopedAStatus Artd::deleteArtifacts(const ArtifactsPath& in_artifactsPath, int64_t* _aidl_return) {
+  Result<std::string> oat_path = BuildOatPath(in_artifactsPath);
+  if (!oat_path.ok()) {
+    return ScopedAStatus::fromExceptionCodeWithMessage(EX_ILLEGAL_STATE,
+                                                       oat_path.error().message().c_str());
+  }
+
+  *_aidl_return = 0;
+  *_aidl_return += GetSizeAndDeleteFile(*oat_path);
+  *_aidl_return += GetSizeAndDeleteFile(OatPathToVdexPath(*oat_path));
+  *_aidl_return += GetSizeAndDeleteFile(OatPathToArtPath(*oat_path));
+
+  return ScopedAStatus::ok();
+}
+
+ScopedAStatus Artd::getOptimizationStatus(const std::string& in_dexFile,
+                                          const std::string& in_instructionSet,
+                                          const std::string& in_classLoaderContext,
+                                          GetOptimizationStatusResult* _aidl_return) {
+  Result<OatFileAssistant::RuntimeOptions> runtime_options = GetRuntimeOptions();
+  if (!runtime_options.ok()) {
+    return ScopedAStatus::fromExceptionCodeWithMessage(
+        EX_ILLEGAL_STATE,
+        ("Failed to get runtime options: " + runtime_options.error().message()).c_str());
+  }
+
+  std::string error_msg;
+  auto oat_file_assistant = OatFileAssistant::Create(
+      in_dexFile.c_str(),
+      in_instructionSet.c_str(),
+      in_classLoaderContext.c_str(),
+      /*load_executable=*/false,
+      /*only_load_trusted_executable=*/true,
+      std::make_unique<OatFileAssistant::RuntimeOptions>(std::move(*runtime_options)),
+      &error_msg);
+  if (oat_file_assistant == nullptr) {
+    return ScopedAStatus::fromExceptionCodeWithMessage(
+        EX_ILLEGAL_STATE, ("Failed to create OatFileAssistant: " + error_msg).c_str());
+  }
+
+  std::string ignored_odex_status;
+  oat_file_assistant->GetOptimizationStatus(&_aidl_return->compilerFilter,
+                                            &_aidl_return->compilationReason,
+                                            &_aidl_return->locationDebugString,
+                                            &ignored_odex_status);
+
+  // We ignore odex_status because it is not meaningful. It can only be either "up-to-date",
+  // "apk-more-recent", or "io-error-no-oat", which means it doesn't give us information in addition
+  // to what we can learn from compiler_filter because compiler_filter will be the actual compiler
+  // filter, "run-from-apk-fallback", and "run-from-apk" in those three cases respectively.
+  DCHECK(ignored_odex_status == "up-to-date" || ignored_odex_status == "apk-more-recent" ||
+         ignored_odex_status == "io-error-no-oat");
+
+  return ScopedAStatus::ok();
+}
+
 Result<void> Artd::Start() {
   ScopedAStatus status = ScopedAStatus::fromStatus(
       AServiceManager_registerLazyService(this->asBinder().get(), kServiceName));
@@ -58,5 +195,43 @@
   return {};
 }
 
+Result<OatFileAssistant::RuntimeOptions> Artd::GetRuntimeOptions() {
+  // We don't cache this system property because it can change.
+  bool use_jit_zygote = UseJitZygote();
+
+  if (!HasRuntimeOptionsCache()) {
+    OR_RETURN(BuildRuntimeOptionsCache());
+  }
+
+  return OatFileAssistant::RuntimeOptions{
+      .image_locations = cached_boot_image_locations_,
+      .boot_class_path = cached_boot_class_path_,
+      .boot_class_path_locations = cached_boot_class_path_,
+      .use_jit_zygote = use_jit_zygote,
+      .deny_art_apex_data_files = cached_deny_art_apex_data_files_,
+      .apex_versions = cached_apex_versions_,
+  };
+}
+
+Result<void> Artd::BuildRuntimeOptionsCache() {
+  // This system property can only be set by odsign on boot, so it won't change.
+  bool deny_art_apex_data_files = DenyArtApexDataFiles();
+
+  std::vector<std::string> image_locations =
+      OR_RETURN(GetBootImageLocations(deny_art_apex_data_files));
+  std::vector<std::string> boot_class_path = OR_RETURN(GetBootClassPath());
+  std::string apex_versions =
+      Runtime::GetApexVersions(ArrayRef<const std::string>(boot_class_path));
+
+  cached_boot_image_locations_ = std::move(image_locations);
+  cached_boot_class_path_ = std::move(boot_class_path);
+  cached_apex_versions_ = std::move(apex_versions);
+  cached_deny_art_apex_data_files_ = deny_art_apex_data_files;
+
+  return {};
+}
+
+bool Artd::HasRuntimeOptionsCache() const { return !cached_boot_image_locations_.empty(); }
+
 }  // namespace artd
 }  // namespace art
diff --git a/artd/artd.h b/artd/artd.h
index f01d9a8..2a05267 100644
--- a/artd/artd.h
+++ b/artd/artd.h
@@ -17,9 +17,13 @@
 #ifndef ART_ARTD_ARTD_H_
 #define ART_ARTD_ARTD_H_
 
+#include <string>
+#include <vector>
+
 #include "aidl/com/android/server/art/BnArtd.h"
 #include "android-base/result.h"
 #include "android/binder_auto_utils.h"
+#include "oat_file_assistant.h"
 
 namespace art {
 namespace artd {
@@ -28,7 +32,29 @@
  public:
   ndk::ScopedAStatus isAlive(bool* _aidl_return) override;
 
+  ndk::ScopedAStatus deleteArtifacts(
+      const aidl::com::android::server::art::ArtifactsPath& in_artifactsPath,
+      int64_t* _aidl_return) override;
+
+  ndk::ScopedAStatus getOptimizationStatus(
+      const std::string& in_dexFile,
+      const std::string& in_instructionSet,
+      const std::string& in_classLoaderContext,
+      aidl::com::android::server::art::GetOptimizationStatusResult* _aidl_return) override;
+
   android::base::Result<void> Start();
+
+ private:
+  android::base::Result<OatFileAssistant::RuntimeOptions> GetRuntimeOptions();
+
+  android::base::Result<void> BuildRuntimeOptionsCache();
+
+  bool HasRuntimeOptionsCache() const;
+
+  std::vector<std::string> cached_boot_image_locations_;
+  std::vector<std::string> cached_boot_class_path_;
+  std::string cached_apex_versions_;
+  bool cached_deny_art_apex_data_files_;
 };
 
 }  // namespace artd
diff --git a/artd/artd_test.cc b/artd/artd_test.cc
index 14bccc2..f16a58e 100644
--- a/artd/artd_test.cc
+++ b/artd/artd_test.cc
@@ -16,26 +16,94 @@
 
 #include "artd.h"
 
+#include <sys/capability.h>
+
+#include <filesystem>
+#include <functional>
 #include <memory>
 
+#include "android-base/file.h"
+#include "android-base/logging.h"
+#include "android-base/scopeguard.h"
 #include "android/binder_interface_utils.h"
 #include "base/common_art_test.h"
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
 namespace art {
 namespace artd {
 namespace {
 
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::android::base::make_scope_guard;
+using ::android::base::ScopeGuard;
+using ::testing::_;
+using ::testing::ContainsRegex;
+using ::testing::HasSubstr;
+using ::testing::MockFunction;
+
+// A wrapper of `cap_t` that automatically calls `cap_free`.
+class ScopedCap {
+ public:
+  explicit ScopedCap(cap_t cap) : cap_(cap) { CHECK_NE(cap, nullptr); }
+
+  ScopedCap(ScopedCap&& other) : cap_(std::exchange(other.cap_, nullptr)) {}
+
+  ~ScopedCap() {
+    if (cap_ != nullptr) {
+      CHECK_EQ(cap_free(cap_), 0);
+    }
+  }
+
+  cap_t Get() const { return cap_; }
+
+ private:
+  cap_t cap_;
+};
+
+// Temporarily drops all root capabilities when the test is run as root. This is a noop otherwise.
+ScopeGuard<std::function<void()>> ScopedUnroot() {
+  ScopedCap old_cap(cap_get_proc());
+  ScopedCap new_cap(cap_dup(old_cap.Get()));
+  CHECK_EQ(cap_clear_flag(new_cap.Get(), CAP_EFFECTIVE), 0);
+  CHECK_EQ(cap_set_proc(new_cap.Get()), 0);
+  // `old_cap` is actually not shared with anyone else, but we have to wrap it with a `shared_ptr`
+  // because `std::function` requires captures to be copyable.
+  return make_scope_guard([old_cap = std::make_shared<ScopedCap>(std::move(old_cap))]() {
+    CHECK_EQ(cap_set_proc(old_cap->Get()), 0);
+  });
+}
+
+// Temporarily drops all permission on a file/directory.
+ScopeGuard<std::function<void()>> ScopedInaccessible(const std::string& path) {
+  std::filesystem::perms old_perms = std::filesystem::status(path).permissions();
+  std::filesystem::permissions(path, std::filesystem::perms::none);
+  return make_scope_guard([=]() { std::filesystem::permissions(path, old_perms); });
+}
+
+ScopeGuard<std::function<void()>> ScopedSetLogger(android::base::LogFunction&& logger) {
+  android::base::LogFunction old_logger = android::base::SetLogger(std::move(logger));
+  return make_scope_guard([old_logger = std::move(old_logger)]() mutable {
+    android::base::SetLogger(std::move(old_logger));
+  });
+}
+
 class ArtdTest : public CommonArtTest {
  protected:
   void SetUp() override {
     CommonArtTest::SetUp();
     artd_ = ndk::SharedRefBase::make<Artd>();
+    scratch_dir_ = std::make_unique<ScratchDir>();
   }
 
-  void TearDown() override { CommonArtTest::TearDown(); }
+  void TearDown() override {
+    scratch_dir_.reset();
+    CommonArtTest::TearDown();
+  }
 
   std::shared_ptr<Artd> artd_;
+  std::unique_ptr<ScratchDir> scratch_dir_;
+  MockFunction<android::base::LogFunction> mock_logger_;
 };
 
 TEST_F(ArtdTest, isAlive) {
@@ -44,6 +112,130 @@
   EXPECT_TRUE(result);
 }
 
+TEST_F(ArtdTest, deleteArtifacts) {
+  std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+  std::filesystem::create_directories(oat_dir);
+  android::base::WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
+  android::base::WriteStringToFile("ab", oat_dir + "/b.vdex");    // 2 bytes.
+  android::base::WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = false,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 4 + 2 + 1);
+
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex"));
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.vdex"));
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art"));
+}
+
+TEST_F(ArtdTest, deleteArtifactsMissingFile) {
+  // Missing VDEX file.
+  std::string oat_dir = dalvik_cache_ + "/arm64";
+  std::filesystem::create_directories(oat_dir);
+  android::base::WriteStringToFile("abcd", oat_dir + "/a@b.apk@classes.dex");  // 4 bytes.
+  android::base::WriteStringToFile("a", oat_dir + "/a@b.apk@classes.art");     // 1 byte.
+
+  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+  EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = true,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 4 + 1);
+
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.dex"));
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.art"));
+}
+
+TEST_F(ArtdTest, deleteArtifactsNoFile) {
+  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+  EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = android_data_ + "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = false,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 0);
+}
+
+TEST_F(ArtdTest, deleteArtifactsPermissionDenied) {
+  std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+  std::filesystem::create_directories(oat_dir);
+  android::base::WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
+  android::base::WriteStringToFile("ab", oat_dir + "/b.vdex");    // 2 bytes.
+  android::base::WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
+
+  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+  EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(3);
+
+  auto scoped_inaccessible = ScopedInaccessible(oat_dir);
+  auto scoped_unroot = ScopedUnroot();
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = false,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 0);
+}
+
+TEST_F(ArtdTest, deleteArtifactsFileIsDir) {
+  // VDEX file is a directory.
+  std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+  std::filesystem::create_directories(oat_dir);
+  std::filesystem::create_directories(oat_dir + "/b.vdex");
+  android::base::WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
+  android::base::WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
+
+  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+  EXPECT_CALL(mock_logger_,
+              Call(_, _, _, _, _, ContainsRegex(R"re(Failed to get the file size.*b\.vdex)re")))
+      .Times(1);
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = false,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 4 + 1);
+
+  // The directory is kept because getting the file size failed.
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex"));
+  EXPECT_TRUE(std::filesystem::exists(oat_dir + "/b.vdex"));
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art"));
+}
+
 }  // namespace
 }  // namespace artd
 }  // namespace art
diff --git a/artd/binder/Android.bp b/artd/binder/Android.bp
index ad8474f..b6fd5b8 100644
--- a/artd/binder/Android.bp
+++ b/artd/binder/Android.bp
@@ -31,6 +31,10 @@
     backend: {
         java: {
             enabled: true,
+            apex_available: [
+                "com.android.art",
+                "com.android.art.debug",
+            ],
         },
         cpp: {
             enabled: false,
@@ -40,9 +44,7 @@
             apex_available: [
                 "com.android.art",
                 "com.android.art.debug",
-                "com.android.compos",
             ],
-            min_sdk_version: "31",
         },
     },
     unstable: true,
@@ -50,4 +52,5 @@
         "//system/tools/aidl/build",
         "//art:__subpackages__",
     ],
+    min_sdk_version: "31",
 }
diff --git a/artd/binder/com/android/server/art/ArtifactsPath.aidl b/artd/binder/com/android/server/art/ArtifactsPath.aidl
new file mode 100644
index 0000000..f69b439
--- /dev/null
+++ b/artd/binder/com/android/server/art/ArtifactsPath.aidl
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Represents the path to the optimized artifacts of a dex file (i.e., ART, OAT, and VDEX files).
+ *
+ * @hide
+ */
+parcelable ArtifactsPath {
+    /** The absolute path starting with '/' to the dex file (i.e., APK or JAR file). */
+    @utf8InCpp String dexPath;
+    /** The instruction set of the optimized artifacts. */
+    @utf8InCpp String isa;
+    /** Whether the optimized artifacts are in the dalvik-cache folder. */
+    boolean isInDalvikCache;
+}
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 58b2aae..a1df266 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -16,8 +16,16 @@
 
 package com.android.server.art;
 
-/** {@hide} */
+/** @hide */
 interface IArtd {
     // Test to see if the artd service is available.
     boolean isAlive();
+
+    /** 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/artd/path_utils.cc b/artd/path_utils.cc
new file mode 100644
index 0000000..c4d9031
--- /dev/null
+++ b/artd/path_utils.cc
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+
+#include "path_utils.h"
+
+#include <filesystem>
+
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/errors.h"
+#include "android-base/result.h"
+#include "android-base/strings.h"
+#include "arch/instruction_set.h"
+#include "base/file_utils.h"
+#include "oat_file_assistant.h"
+
+namespace art {
+namespace artd {
+
+namespace {
+
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::android::base::EndsWith;
+using ::android::base::Error;
+using ::android::base::Result;
+
+Result<void> ValidateAbsoluteNormalPath(const std::string& path_str) {
+  if (path_str.empty()) {
+    return Errorf("Path is empty");
+  }
+  std::filesystem::path path(path_str);
+  if (!path.is_absolute()) {
+    return Errorf("Path '{}' is not an absolute path", path_str);
+  }
+  if (path.lexically_normal() != path_str) {
+    return Errorf("Path '{}' is not in normal form", path_str);
+  }
+  return {};
+}
+
+Result<void> ValidateDexPath(const std::string& dex_path) {
+  OR_RETURN(ValidateAbsoluteNormalPath(dex_path));
+  if (!EndsWith(dex_path, ".apk") && !EndsWith(dex_path, ".jar")) {
+    return Errorf("Dex path '{}' has an invalid extension", dex_path);
+  }
+  return {};
+}
+
+}  // namespace
+
+Result<std::string> BuildOatPath(const ArtifactsPath& artifacts_path) {
+  OR_RETURN(ValidateDexPath(artifacts_path.dexPath));
+
+  InstructionSet isa = GetInstructionSetFromString(artifacts_path.isa.c_str());
+  if (isa == InstructionSet::kNone) {
+    return Errorf("Instruction set '{}' is invalid", artifacts_path.isa.c_str());
+  }
+
+  std::string error_msg;
+  std::string path;
+  if (artifacts_path.isInDalvikCache) {
+    // Apps' OAT files are never in ART APEX data.
+    if (!OatFileAssistant::DexLocationToOatFilename(
+            artifacts_path.dexPath, isa, /*deny_art_apex_data_files=*/true, &path, &error_msg)) {
+      return Error() << error_msg;
+    }
+    return path;
+  } else {
+    if (!OatFileAssistant::DexLocationToOdexFilename(
+            artifacts_path.dexPath, isa, &path, &error_msg)) {
+      return Error() << error_msg;
+    }
+    return path;
+  }
+}
+
+std::string OatPathToVdexPath(const std::string& oat_path) {
+  return ReplaceFileExtension(oat_path, "vdex");
+}
+
+std::string OatPathToArtPath(const std::string& oat_path) {
+  return ReplaceFileExtension(oat_path, "art");
+}
+
+}  // namespace artd
+}  // namespace art
diff --git a/artd/path_utils.h b/artd/path_utils.h
new file mode 100644
index 0000000..970143a
--- /dev/null
+++ b/artd/path_utils.h
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+#ifndef ART_ARTD_PATH_UTILS_H_
+#define ART_ARTD_PATH_UTILS_H_
+
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/result.h"
+
+namespace art {
+namespace artd {
+
+// Returns the absolute path to the OAT file built from the `ArtifactsPath`.
+android::base::Result<std::string> BuildOatPath(
+    const aidl::com::android::server::art::ArtifactsPath& artifacts_path);
+
+// Returns the path to the VDEX file that corresponds to the OAT file.
+std::string OatPathToVdexPath(const std::string& oat_path);
+
+// Returns the path to the ART file that corresponds to the OAT file.
+std::string OatPathToArtPath(const std::string& oat_path);
+
+}  // namespace artd
+}  // namespace art
+
+#endif  // ART_ARTD_PATH_UTILS_H_
diff --git a/artd/path_utils_test.cc b/artd/path_utils_test.cc
new file mode 100644
index 0000000..9ce40c5
--- /dev/null
+++ b/artd/path_utils_test.cc
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+#include "path_utils.h"
+
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/result-gmock.h"
+#include "base/common_art_test.h"
+#include "gtest/gtest.h"
+
+namespace art {
+namespace artd {
+namespace {
+
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::android::base::testing::HasError;
+using ::android::base::testing::HasValue;
+using ::android::base::testing::WithMessage;
+
+class PathUtilsTest : public CommonArtTest {};
+
+TEST_F(PathUtilsTest, BuildOatPath) {
+  EXPECT_THAT(
+      BuildOatPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = false}),
+      HasValue("/a/oat/arm64/b.odex"));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathDalvikCache) {
+  EXPECT_THAT(
+      BuildOatPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = true}),
+      HasValue(android_data_ + "/dalvik-cache/arm64/a@b.apk@classes.dex"));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathEmptyDexPath) {
+  EXPECT_THAT(BuildOatPath(ArtifactsPath{.dexPath = "", .isa = "arm64", .isInDalvikCache = false}),
+              HasError(WithMessage("Path is empty")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathRelativeDexPath) {
+  EXPECT_THAT(
+      BuildOatPath(ArtifactsPath{.dexPath = "a/b.apk", .isa = "arm64", .isInDalvikCache = false}),
+      HasError(WithMessage("Path 'a/b.apk' is not an absolute path")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathNonNormalDexPath) {
+  EXPECT_THAT(BuildOatPath(ArtifactsPath{
+                  .dexPath = "/a/c/../b.apk", .isa = "arm64", .isInDalvikCache = false}),
+              HasError(WithMessage("Path '/a/c/../b.apk' is not in normal form")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathInvalidDexExtension) {
+  EXPECT_THAT(BuildOatPath(ArtifactsPath{
+                  .dexPath = "/a/b.invalid", .isa = "arm64", .isInDalvikCache = false}),
+              HasError(WithMessage("Dex path '/a/b.invalid' has an invalid extension")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathInvalidIsa) {
+  EXPECT_THAT(BuildOatPath(
+                  ArtifactsPath{.dexPath = "/a/b.apk", .isa = "invalid", .isInDalvikCache = false}),
+              HasError(WithMessage("Instruction set 'invalid' is invalid")));
+}
+
+TEST_F(PathUtilsTest, OatPathToVdexPath) {
+  EXPECT_EQ(OatPathToVdexPath("/a/oat/arm64/b.odex"), "/a/oat/arm64/b.vdex");
+}
+
+TEST_F(PathUtilsTest, OatPathToArtPath) {
+  EXPECT_EQ(OatPathToArtPath("/a/oat/arm64/b.odex"), "/a/oat/arm64/b.art");
+}
+
+}  // namespace
+}  // namespace artd
+}  // namespace art
diff --git a/build/Android.gtest.mk b/build/Android.gtest.mk
index 0080ae4..d383f64 100644
--- a/build/Android.gtest.mk
+++ b/build/Android.gtest.mk
@@ -133,11 +133,9 @@
     art_sigchain_tests \
 
 ART_TEST_MODULES_TARGET := $(ART_TEST_MODULES_COMMON) \
+    art_artd_tests \
     art_odrefresh_tests \
 
-# TODO(b/235464166): art_artd_tests doesn't work on master-art because of the dependency on
-# libbinder_ndk.
-
 ART_TEST_MODULES_HOST := $(ART_TEST_MODULES_COMMON)
 
 ART_TARGET_GTEST_NAMES := $(foreach tm,$(ART_TEST_MODULES_TARGET),\
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index 07fd103..9ad5eee 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -400,9 +400,7 @@
 
 // ART gtests with dependencies on internal ART APEX libraries.
 art_gtests = [
-    // TODO(b/235464166): art_artd_tests doesn't work on master-art because of
-    // the dependency on libbinder_ndk.
-    // "art_artd_tests",
+    "art_artd_tests",
     "art_cmdline_tests",
     "art_compiler_tests",
     "art_dex2oat_tests",
diff --git a/build/apex/art_apex_test.py b/build/apex/art_apex_test.py
index e180464..1813913 100755
--- a/build/apex/art_apex_test.py
+++ b/build/apex/art_apex_test.py
@@ -670,9 +670,7 @@
 
   def run(self):
     # Check ART test binaries.
-    # TODO(b/235464166): art_artd_tests doesn't work on master-art because of
-    # the dependency on libbinder_ndk.
-    # self._checker.check_art_test_executable('art_artd_tests')
+    self._checker.check_art_test_executable('art_artd_tests')
     self._checker.check_art_test_executable('art_cmdline_tests')
     self._checker.check_art_test_executable('art_compiler_tests')
     self._checker.check_art_test_executable('art_dex2oat_tests')
diff --git a/build/apex/manifest-art.json b/build/apex/manifest-art.json
index bf45076..c41469b 100644
--- a/build/apex/manifest-art.json
+++ b/build/apex/manifest-art.json
@@ -1,6 +1,6 @@
 {
   "name": "com.android.art",
-  "version": 339990000,
+  "version": 990090000,
   "provideNativeLibs": [
     "libjdwp.so"
   ],
diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp
index 8805430..7bd4c34 100644
--- a/libartservice/service/Android.bp
+++ b/libartservice/service/Android.bp
@@ -82,6 +82,8 @@
         "java/**/*.java",
     ],
     static_libs: [
+        "artd-aidl-java",
+        "modules-utils-shell-command-handler",
     ],
     plugins: ["java_api_finder"],
     jarjar_rules: "jarjar-rules.txt",
@@ -127,11 +129,15 @@
         "androidx.test.ext.junit",
         "androidx.test.ext.truth",
         "androidx.test.runner",
+        "artd-aidl-java",
         "mockito-target-minus-junit4",
         "service-art.impl",
+        // Statically link against system server to allow us to mock system
+        // server APIs. This won't work on master-art, but it's fine because we
+        // don't run this test on master-art.
+        "services.core",
     ],
 
-    sdk_version: "system_server_current",
     min_sdk_version: "31",
 
     test_suites: ["general-tests"],
diff --git a/libartservice/service/api/system-server-current.txt b/libartservice/service/api/system-server-current.txt
index c7844e0..d35f8c7 100644
--- a/libartservice/service/api/system-server-current.txt
+++ b/libartservice/service/api/system-server-current.txt
@@ -3,6 +3,38 @@
 
   public final class ArtManagerLocal {
     ctor public ArtManagerLocal();
+    method @NonNull public com.android.server.art.model.DeleteResult deleteOptimizedArtifacts(@NonNull com.android.server.pm.snapshot.PackageDataSnapshot, @NonNull String);
+    method @NonNull public com.android.server.art.model.DeleteResult deleteOptimizedArtifacts(@NonNull com.android.server.pm.snapshot.PackageDataSnapshot, @NonNull String, int);
+    method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.snapshot.PackageDataSnapshot, @NonNull String);
+    method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.snapshot.PackageDataSnapshot, @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[]);
+  }
+
+}
+
+package com.android.server.art.model {
+
+  public class ArtFlags {
+    method public static int defaultDeleteFlags();
+    method public static int defaultGetStatusFlags();
+    field public static final int FLAG_FOR_PRIMARY_DEX = 1; // 0x1
+    field public static final int FLAG_FOR_SECONDARY_DEX = 2; // 0x2
+  }
+
+  public class DeleteResult {
+    method public long getFreedBytes();
+  }
+
+  public class OptimizationStatus {
+    method @NonNull public java.util.List<com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus> getDexFileOptimizationStatuses();
+  }
+
+  public static class OptimizationStatus.DexFileOptimizationStatus {
+    method @NonNull public String getCompilationReason();
+    method @NonNull public String getCompilerFilter();
+    method @NonNull public String getDexFile();
+    method @NonNull public String getInstructionSet();
+    method @NonNull public String getLocationDebugString();
   }
 
 }
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 64aec7b..3a6bdc9 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -16,16 +16,251 @@
 
 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.ArtFlags.DeleteFlags;
+import static com.android.server.art.model.ArtFlags.GetStatusFlags;
+import static com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus;
+
+import android.annotation.NonNull;
 import android.annotation.SystemApi;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.art.IArtd;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.DeleteResult;
+import com.android.server.art.model.OptimizationStatus;
+import com.android.server.art.wrapper.AndroidPackageApi;
+import com.android.server.art.wrapper.PackageManagerLocal;
+import com.android.server.art.wrapper.PackageState;
+import com.android.server.pm.snapshot.PackageDataSnapshot;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * This class provides a system API for functionality provided by the ART module.
  *
+ * Note: Although this class is the entry point of ART services, this class is not a {@link
+ * SystemService}, and it does not publish a binder. Instead, it is a module loaded by the
+ * system_server process, registered in {@link LocalManagerRegistry}. {@link LocalManagerRegistry}
+ * specifies that in-process module interfaces should be named with the suffix {@code ManagerLocal}
+ * for consistency.
+ *
  * @hide
  */
 @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
 public final class ArtManagerLocal {
     private static final String TAG = "ArtService";
 
-    public ArtManagerLocal() {}
+    @NonNull private final Injector mInjector;
+
+    public ArtManagerLocal() {
+        this(new Injector());
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public ArtManagerLocal(Injector injector) {
+        mInjector = injector;
+    }
+
+    /**
+     * Handles `cmd package art` sub-command.
+     *
+     * For debugging purposes only. Intentionally enforces root access to limit the usage.
+     *
+     * Note: This method is not an override of {@link Binder#handleShellCommand} because ART
+     * services does not publish a binder. Instead, it handles the `art` sub-command forwarded by
+     * the `package` service. The semantics of the parameters are the same as {@link
+     * Binder#handleShellCommand}.
+     *
+     * @return zero on success, non-zero on internal error (e.g., I/O error)
+     * @throws SecurityException if the caller is not root
+     * @throws IllegalArgumentException if the arguments are illegal
+     * @see ArtShellCommand#onHelp()
+     */
+    public int handleShellCommand(@NonNull Binder target, @NonNull ParcelFileDescriptor in,
+            @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+            @NonNull String[] args) {
+        return new ArtShellCommand(this, mInjector.getPackageManagerLocal())
+                .exec(target, in.getFileDescriptor(), out.getFileDescriptor(),
+                        err.getFileDescriptor(), args);
+    }
+
+    /**
+     * Deletes optimized artifacts of a package.
+     *
+     * @throws IllegalArgumentException if the package is not found or the flags are illegal
+     * @throws IllegalStateException if an internal error occurs
+     */
+    @NonNull
+    public DeleteResult deleteOptimizedArtifacts(
+            @NonNull PackageDataSnapshot snapshot, @NonNull String packageName) {
+        return deleteOptimizedArtifacts(snapshot, packageName, ArtFlags.defaultDeleteFlags());
+    }
+
+    /**
+     * Same as above, but allows to specify flags.
+     *
+     * @see #deleteOptimizedArtifacts(PackageDataSnapshot, String)
+     */
+    @NonNull
+    public DeleteResult deleteOptimizedArtifacts(@NonNull PackageDataSnapshot snapshot,
+            @NonNull String packageName, @DeleteFlags int flags) {
+        if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) == 0
+                && (flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) == 0) {
+            throw new IllegalArgumentException("Nothing to delete");
+        }
+
+        PackageState pkgState = getPackageStateOrThrow(snapshot, packageName);
+        AndroidPackageApi pkg = getPackageOrThrow(pkgState);
+
+        try {
+            long freedBytes = 0;
+
+            if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) != 0) {
+                boolean isInDalvikCache = Utils.isInDalvikCache(pkgState);
+                for (PrimaryDexInfo dexInfo : PrimaryDexUtils.getDexInfo(pkg)) {
+                    if (!dexInfo.hasCode()) {
+                        continue;
+                    }
+                    for (String isa : Utils.getAllIsas(pkgState)) {
+                        freedBytes += mInjector.getArtd().deleteArtifacts(
+                                Utils.buildArtifactsPath(dexInfo.dexPath(), isa, isInDalvikCache));
+                    }
+                }
+            }
+
+            if ((flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) != 0) {
+                // TODO(jiakaiz): Implement this.
+                throw new UnsupportedOperationException(
+                        "Deleting artifacts of secondary dex'es is not implemented yet");
+            }
+
+            return new DeleteResult(freedBytes);
+        } catch (RemoteException e) {
+            throw new IllegalStateException("An error occurred when calling artd", e);
+        }
+    }
+
+    /**
+     * Returns the optimization status of a package.
+     *
+     * @throws IllegalArgumentException if the package is not found or the flags are illegal
+     * @throws IllegalStateException if an internal error occurs
+     */
+    @NonNull
+    public OptimizationStatus getOptimizationStatus(
+            @NonNull PackageDataSnapshot snapshot, @NonNull String packageName) {
+        return getOptimizationStatus(snapshot, packageName, ArtFlags.defaultGetStatusFlags());
+    }
+
+    /**
+     * Same as above, but allows to specify flags.
+     *
+     * @see #getOptimizationStatus(PackageDataSnapshot, String)
+     */
+    @NonNull
+    public OptimizationStatus getOptimizationStatus(@NonNull PackageDataSnapshot snapshot,
+            @NonNull String packageName, @GetStatusFlags int flags) {
+        if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) == 0
+                && (flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) == 0) {
+            throw new IllegalArgumentException("Nothing to check");
+        }
+
+        PackageState pkgState = getPackageStateOrThrow(snapshot, packageName);
+        AndroidPackageApi pkg = getPackageOrThrow(pkgState);
+
+        try {
+            List<DexFileOptimizationStatus> statuses = new ArrayList<>();
+
+            if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) != 0) {
+                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 ((flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) != 0) {
+                // 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
+     */
+    @VisibleForTesting
+    public static class Injector {
+        private final PackageManagerLocal mPackageManagerLocal;
+
+        Injector() {
+            PackageManagerLocal packageManagerLocal = null;
+            try {
+                packageManagerLocal = PackageManagerLocal.getInstance();
+            } catch (Exception e) {
+                // This is not a serious error. The reflection-based approach can be broken in some
+                // cases. This is fine because ART services is under development and no one depends
+                // on it.
+                // TODO(b/177273468): Make this a serious error when we switch to using the real
+                // APIs.
+                Log.w(TAG, "Unable to get fake PackageManagerLocal", e);
+            }
+            mPackageManagerLocal = packageManagerLocal;
+        }
+
+        public PackageManagerLocal getPackageManagerLocal() {
+            return mPackageManagerLocal;
+        }
+
+        public IArtd getArtd() {
+            IArtd artd = IArtd.Stub.asInterface(ServiceManager.waitForService("artd"));
+            if (artd == null) {
+                throw new IllegalStateException("Unable to connect to artd");
+            }
+            return artd;
+        }
+    }
 }
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
new file mode 100644
index 0000000..9a49aae
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -0,0 +1,108 @@
+/*
+ * 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;
+
+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.ArtFlags;
+import com.android.server.art.model.DeleteResult;
+import com.android.server.art.model.OptimizationStatus;
+import com.android.server.art.wrapper.PackageManagerLocal;
+import com.android.server.pm.snapshot.PackageDataSnapshot;
+
+import java.io.PrintWriter;
+
+/**
+ * This class handles ART shell commands.
+ *
+ * @hide
+ */
+public final class ArtShellCommand extends BasicShellCommandHandler {
+    private static final String TAG = "ArtShellCommand";
+
+    private final ArtManagerLocal mArtManagerLocal;
+    private final PackageManagerLocal mPackageManagerLocal;
+
+    public ArtShellCommand(
+            ArtManagerLocal artManagerLocal, PackageManagerLocal packageManagerLocal) {
+        mArtManagerLocal = artManagerLocal;
+        mPackageManagerLocal = packageManagerLocal;
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        enforceRoot();
+        PrintWriter pw = getOutPrintWriter();
+        PackageDataSnapshot snapshot = mPackageManagerLocal.snapshot();
+        switch (cmd) {
+            case "delete-optimized-artifacts":
+                DeleteResult result = mArtManagerLocal.deleteOptimizedArtifacts(
+                        snapshot, getNextArgRequired(), ArtFlags.defaultDeleteFlags());
+                pw.printf("Freed %d bytes\n", result.getFreedBytes());
+                return 0;
+            case "get-optimization-status":
+                OptimizationStatus optimizationStatus = mArtManagerLocal.getOptimizationStatus(
+                        snapshot, getNextArgRequired(), ArtFlags.defaultGetStatusFlags());
+                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);
+        }
+    }
+
+    @Override
+    public void onHelp() {
+        final PrintWriter pw = getOutPrintWriter();
+        pw.println("ART service commands.");
+        pw.println("Note: The commands are used for internal debugging purposes only. There are no "
+                + "stability guarantees for them.");
+        pw.println("");
+        pw.println("Usage: cmd package art [ARGS]...");
+        pw.println("");
+        pw.println("Supported commands:");
+        pw.println("  help or -h");
+        pw.println("    Print this help text.");
+        // TODO(jiakaiz): Also do operations for secondary dex'es by default.
+        pw.println("  delete-optimized-artifacts <package-name>");
+        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() {
+        final int uid = Binder.getCallingUid();
+        if (uid != Process.ROOT_UID) {
+            throw new SecurityException("ART service shell commands need root access");
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/LoggingArtd.java b/libartservice/service/java/com/android/server/art/LoggingArtd.java
new file mode 100644
index 0000000..811cb6f
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/LoggingArtd.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+import android.os.IBinder;
+import android.util.Log;
+
+/**
+ * An implementation of artd that logs the artd calls for debugging purposes.
+ *
+ * @hide
+ */
+public class LoggingArtd implements IArtd {
+    private static final String TAG = "LoggingArtd";
+
+    @Override
+    public IBinder asBinder() {
+        return null;
+    }
+
+    @Override
+    public boolean isAlive() {
+        return true;
+    }
+
+    @Override
+    public long deleteArtifacts(ArtifactsPath artifactsPath) {
+        Log.i(TAG, "deleteArtifacts " + artifactsPathToString(artifactsPath));
+        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,
+                String.valueOf(artifactsPath.isInDalvikCache));
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
new file mode 100644
index 0000000..d6b8a59
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
@@ -0,0 +1,375 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ApplicationInfo;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.Immutable;
+import com.android.server.art.wrapper.AndroidPackageApi;
+import com.android.server.art.wrapper.PackageState;
+import com.android.server.art.wrapper.SharedLibraryInfo;
+
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.DexClassLoader;
+import dalvik.system.PathClassLoader;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** @hide */
+public class PrimaryDexUtils {
+    private static final String SHARED_LIBRARY_LOADER_TYPE = PathClassLoader.class.getName();
+
+    /**
+     * Returns the basic information about all primary dex files belonging to the package. The
+     * return value is a list where the entry at index 0 is the information about the base APK, and
+     * the entry at index i is the information about the (i-1)-th split APK.
+     */
+    @NonNull
+    public static List<PrimaryDexInfo> getDexInfo(@NonNull AndroidPackageApi pkg) {
+        return getDexInfoImpl(pkg)
+                .stream()
+                .map(builder -> builder.build())
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Same as above, but requires {@link PackageState} in addition, and returns the detailed
+     * information, including the class loader context.
+     */
+    @NonNull
+    public static List<DetailedPrimaryDexInfo> getDetailedDexInfo(
+            @NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg) {
+        return getDetailedDexInfoImpl(pkgState, pkg)
+                .stream()
+                .map(builder -> builder.buildDetailed())
+                .collect(Collectors.toList());
+    }
+
+    @NonNull
+    private static List<PrimaryDexInfoBuilder> getDexInfoImpl(@NonNull AndroidPackageApi pkg) {
+        List<PrimaryDexInfoBuilder> dexInfos = new ArrayList<>();
+
+        PrimaryDexInfoBuilder baseInfo = new PrimaryDexInfoBuilder(pkg.getBaseApkPath());
+        baseInfo.mHasCode = pkg.isHasCode();
+        baseInfo.mIsBaseApk = true;
+        dexInfos.add(baseInfo);
+
+        String[] splitNames = pkg.getSplitNames();
+        String[] splitCodePaths = pkg.getSplitCodePaths();
+        int[] splitFlags = pkg.getSplitFlags();
+
+        for (int i = 0; i < splitNames.length; i++) {
+            PrimaryDexInfoBuilder splitInfo = new PrimaryDexInfoBuilder(splitCodePaths[i]);
+            splitInfo.mHasCode =
+                    splitFlags != null && (splitFlags[i] & ApplicationInfo.FLAG_HAS_CODE) != 0;
+            splitInfo.mSplitIndex = i;
+            splitInfo.mSplitName = splitNames[i];
+            dexInfos.add(splitInfo);
+        }
+
+        return dexInfos;
+    }
+
+    @NonNull
+    private static List<PrimaryDexInfoBuilder> getDetailedDexInfoImpl(
+            @NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg) {
+        List<PrimaryDexInfoBuilder> dexInfos = getDexInfoImpl(pkg);
+
+        PrimaryDexInfoBuilder baseApk = dexInfos.get(0);
+        assert baseApk.mIsBaseApk;
+        baseApk.mClassLoaderName = pkg.getClassLoaderName();
+        File baseDexFile = new File(baseApk.mDexPath);
+        baseApk.mRelativeDexPath = baseDexFile.getName();
+
+        // Shared libraries are the dependencies of the base APK.
+        baseApk.mSharedLibrariesContext = encodeSharedLibraries(pkgState.getUsesLibraryInfos());
+
+        String[] splitClassLoaderNames = pkg.getSplitClassLoaderNames();
+        SparseArray<int[]> splitDependencies = pkg.getSplitDependencies();
+        boolean isIsolatedSplitLoading =
+                pkg.isIsolatedSplitLoading() && !Utils.isEmpty(splitDependencies);
+
+        for (int i = 1; i < dexInfos.size(); i++) {
+            assert dexInfos.get(i).mSplitIndex == i - 1;
+            File splitDexFile = new File(dexInfos.get(i).mDexPath);
+            if (!splitDexFile.getParent().equals(baseDexFile.getParent())) {
+                throw new IllegalStateException(
+                        "Split APK and base APK are in different directories: "
+                        + splitDexFile.getParent() + " != " + baseDexFile.getParent());
+            }
+            dexInfos.get(i).mRelativeDexPath = splitDexFile.getName();
+            if (isIsolatedSplitLoading && dexInfos.get(i).mHasCode) {
+                dexInfos.get(i).mClassLoaderName =
+                        splitClassLoaderNames[dexInfos.get(i).mSplitIndex];
+
+                // Keys and values of `splitDependencies` are `split index + 1` for split APK or 0
+                // for base APK, so they can be regarded as indices to `dexInfos`.
+                int[] dependencies = splitDependencies.get(i);
+                if (!Utils.isEmpty(dependencies)) {
+                    // We only care about the first dependency because it is the parent split. The
+                    // rest are configuration splits, which we don't care.
+                    dexInfos.get(i).mSplitDependency = dexInfos.get(dependencies[0]);
+                }
+            }
+        }
+
+        if (isIsolatedSplitLoading) {
+            computeClassLoaderContextsIsolated(dexInfos);
+        } else {
+            computeClassLoaderContexts(dexInfos);
+        }
+
+        return dexInfos;
+    }
+
+    /**
+     * Computes class loader context for an app that didn't request isolated split loading. Stores
+     * the results in {@link PrimaryDexInfoBuilder#mClassLoaderContext}.
+     *
+     * In this case, all the splits will be loaded in the base apk class loader (in the order of
+     * their definition).
+     *
+     * The CLC for the base APK is `CLN[]{shared-libraries}`; the CLC for the n-th split APK is
+     * `CLN[base.apk, split_0.apk, ..., split_n-1.apk]{shared-libraries}`; where `CLN` is the
+     * class loader name for the base APK.
+     */
+    private static void computeClassLoaderContexts(@NonNull List<PrimaryDexInfoBuilder> dexInfos) {
+        String baseClassLoaderName = dexInfos.get(0).mClassLoaderName;
+        String sharedLibrariesContext = dexInfos.get(0).mSharedLibrariesContext;
+        List<String> classpath = new ArrayList<>();
+        for (PrimaryDexInfoBuilder dexInfo : dexInfos) {
+            if (dexInfo.mHasCode) {
+                dexInfo.mClassLoaderContext = encodeClassLoader(baseClassLoaderName, classpath,
+                        null /* parentContext */, sharedLibrariesContext);
+            }
+            // Note that the splits with no code are not removed from the classpath computation.
+            // I.e., split_n might get the split_n-1 in its classpath dependency even if split_n-1
+            // has no code.
+            // The splits with no code do not matter for the runtime which ignores APKs without code
+            // when doing the classpath checks. As such we could actually filter them but we don't
+            // do it in order to keep consistency with how the apps are loaded.
+            classpath.add(dexInfo.mRelativeDexPath);
+        }
+    }
+
+    /**
+     * Computes class loader context for an app that requested for isolated split loading. Stores
+     * the results in {@link PrimaryDexInfoBuilder#mClassLoaderContext}.
+     *
+     * In this case, each split will be loaded with a separate class loader, whose context is a
+     * chain formed from inter-split dependencies.
+     *
+     * The CLC for the base APK is `CLN[]{shared-libraries}`; the CLC for the n-th split APK that
+     * depends on the base APK is `CLN_n[];CLN[base.apk]{shared-libraries}`; the CLC for the n-th
+     * split APK that depends on the m-th split APK is
+     * `CLN_n[];CLN_m[split_m.apk];...;CLN[base.apk]{shared-libraries}`; where `CLN` is the base
+     * class loader name for the base APK, `CLN_i` is the class loader name for the i-th split APK,
+     * and `...` represents the ancestors along the dependency chain.
+     *
+     * Specially, if a split does not have any dependency, the CLC for it is `CLN_n[]`.
+     */
+    private static void computeClassLoaderContextsIsolated(
+            @NonNull List<PrimaryDexInfoBuilder> dexInfos) {
+        for (PrimaryDexInfoBuilder dexInfo : dexInfos) {
+            if (dexInfo.mHasCode) {
+                dexInfo.mClassLoaderContext = encodeClassLoader(dexInfo.mClassLoaderName,
+                        null /* classpath */, getParentContextRecursive(dexInfo),
+                        dexInfo.mSharedLibrariesContext);
+            }
+        }
+    }
+
+    /**
+     * Computes the parent class loader context, recursively. Caches results in {@link
+     * PrimaryDexInfoBuilder#mContextForChildren}.
+     */
+    @Nullable
+    private static String getParentContextRecursive(@NonNull PrimaryDexInfoBuilder dexInfo) {
+        if (dexInfo.mSplitDependency == null) {
+            return null;
+        }
+        PrimaryDexInfoBuilder parent = dexInfo.mSplitDependency;
+        if (parent.mContextForChildren == null) {
+            parent.mContextForChildren =
+                    encodeClassLoader(parent.mClassLoaderName, List.of(parent.mRelativeDexPath),
+                            getParentContextRecursive(parent), parent.mSharedLibrariesContext);
+        }
+        return parent.mContextForChildren;
+    }
+
+    /**
+     * Returns class loader context in the format of
+     * `CLN[classpath...]{share-libraries};parent-context`, where `CLN` is the class loader name.
+     */
+    @NonNull
+    private static String encodeClassLoader(@Nullable String classLoaderName,
+            @Nullable List<String> classpath, @Nullable String parentContext,
+            @Nullable String sharedLibrariesContext) {
+        StringBuilder classLoaderContext = new StringBuilder();
+
+        classLoaderContext.append(encodeClassLoaderName(classLoaderName));
+
+        classLoaderContext.append(
+                "[" + (classpath != null ? String.join(":", classpath) : "") + "]");
+
+        if (!TextUtils.isEmpty(sharedLibrariesContext)) {
+            classLoaderContext.append(sharedLibrariesContext);
+        }
+
+        if (!TextUtils.isEmpty(parentContext)) {
+            classLoaderContext.append(";" + parentContext);
+        }
+
+        return classLoaderContext.toString();
+    }
+
+    @NonNull
+    private static String encodeClassLoaderName(@Nullable String classLoaderName) {
+        // `PathClassLoader` and `DexClassLoader` are grouped together because they have the same
+        // behavior. For null values we default to "PCL". This covers the case where a package does
+        // not specify any value for its class loader.
+        if (classLoaderName == null || PathClassLoader.class.getName().equals(classLoaderName)
+                || DexClassLoader.class.getName().equals(classLoaderName)) {
+            return "PCL";
+        } else if (DelegateLastClassLoader.class.getName().equals(classLoaderName)) {
+            return "DLC";
+        } else {
+            throw new IllegalStateException("Unsupported classLoaderName: " + classLoaderName);
+        }
+    }
+
+    /**
+     * Returns shared libraries context in the format of
+     * `{PCL[library_1_dex_1.jar:library_1_dex_2.jar:...]{library_1-dependencies}#PCL[
+     *     library_1_dex_2.jar:library_2_dex_2.jar:...]{library_2-dependencies}#...}`.
+     */
+    @Nullable
+    private static String encodeSharedLibraries(@Nullable List<SharedLibraryInfo> sharedLibraries) {
+        if (Utils.isEmpty(sharedLibraries)) {
+            return null;
+        }
+        return sharedLibraries.stream()
+                .map(library
+                        -> encodeClassLoader(SHARED_LIBRARY_LOADER_TYPE, library.getAllCodePaths(),
+                                null /* parentContext */,
+                                encodeSharedLibraries(library.getDependencies())))
+                .collect(Collectors.joining("#", "{", "}"));
+    }
+
+    /** Basic information about a primary dex file (either the base APK or a split APK). */
+    @Immutable
+    public static class PrimaryDexInfo {
+        private final @NonNull String mDexPath;
+        private final boolean mHasCode;
+        private final boolean mIsBaseApk;
+        private final int mSplitIndex;
+        private final @Nullable String mSplitName;
+
+        PrimaryDexInfo(@NonNull String dexPath, boolean hasCode, boolean isBaseApk, int splitIndex,
+                @Nullable String splitName) {
+            mDexPath = dexPath;
+            mHasCode = hasCode;
+            mIsBaseApk = isBaseApk;
+            mSplitIndex = splitIndex;
+            mSplitName = splitName;
+        }
+
+        /** The path to the dex file. */
+        public @NonNull String dexPath() {
+            return mDexPath;
+        }
+
+        /** True if the dex file has code. */
+        public boolean hasCode() {
+            return mHasCode;
+        }
+
+        /** True if the dex file is the base APK. */
+        public boolean isBaseApk() {
+            return mIsBaseApk;
+        }
+
+        /** The index to {@link AndroidPackageApi#getSplitNames()}, or -1 for base APK. */
+        public int splitIndex() {
+            return mSplitIndex;
+        }
+
+        /** The name of the split, or null for base APK. */
+        public @Nullable String splitName() {
+            return mSplitName;
+        }
+    }
+
+    /**
+     * Detailed information about a primary dex file (either the base APK or a split APK). It
+     * contains the class loader context in addition to what is in {@link PrimaryDexInfo}, but
+     * producing it requires {@link PackageState}.
+     */
+    @Immutable
+    public static class DetailedPrimaryDexInfo extends PrimaryDexInfo {
+        private final @Nullable String mClassLoaderContext;
+
+        DetailedPrimaryDexInfo(@NonNull String dexPath, boolean hasCode, boolean isBaseApk,
+                int splitIndex, @Nullable String splitName, @Nullable String classLoaderContext) {
+            super(dexPath, hasCode, isBaseApk, splitIndex, splitName);
+            mClassLoaderContext = classLoaderContext;
+        }
+
+        /**
+         * A string describing the structure of the class loader that the dex file is loaded with.
+         */
+        public @Nullable String classLoaderContext() {
+            return mClassLoaderContext;
+        }
+    }
+
+    private static class PrimaryDexInfoBuilder {
+        @NonNull String mDexPath;
+        boolean mHasCode = false;
+        boolean mIsBaseApk = false;
+        int mSplitIndex = -1;
+        @Nullable String mSplitName = null;
+        @Nullable String mRelativeDexPath = null;
+        @Nullable String mClassLoaderContext = null;
+        @Nullable String mClassLoaderName = null;
+        @Nullable PrimaryDexInfoBuilder mSplitDependency = null;
+        /** The class loader context of the shared libraries. Only applicable for the base APK. */
+        @Nullable String mSharedLibrariesContext = null;
+        /** The class loader context for children to use when this dex file is used as a parent. */
+        @Nullable String mContextForChildren = null;
+
+        PrimaryDexInfoBuilder(@NonNull String dexPath) {
+            mDexPath = dexPath;
+        }
+
+        PrimaryDexInfo build() {
+            return new PrimaryDexInfo(mDexPath, mHasCode, mIsBaseApk, mSplitIndex, mSplitName);
+        }
+
+        DetailedPrimaryDexInfo buildDetailed() {
+            return new DetailedPrimaryDexInfo(
+                    mDexPath, mHasCode, mIsBaseApk, mSplitIndex, mSplitName, mClassLoaderContext);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
new file mode 100644
index 0000000..9c67a0f
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -0,0 +1,84 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+import com.android.server.art.ArtifactsPath;
+import com.android.server.art.wrapper.PackageManagerLocal;
+import com.android.server.art.wrapper.PackageState;
+
+import dalvik.system.VMRuntime;
+
+import java.util.Collection;
+import java.util.List;
+
+/** @hide */
+public final class Utils {
+    private Utils() {}
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static <T> boolean isEmpty(@Nullable Collection<T> array) {
+        return array == null || array.isEmpty();
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static <T> boolean isEmpty(@Nullable SparseArray<T> array) {
+        return array == null || array.size() == 0;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(@Nullable int[] array) {
+        return array == null || array.length == 0;
+    }
+
+    @NonNull
+    public static List<String> getAllIsas(@NonNull PackageState pkgState) {
+        String primaryCpuAbi = pkgState.getPrimaryCpuAbi();
+        String secondaryCpuAbi = pkgState.getSecondaryCpuAbi();
+        if (primaryCpuAbi != null) {
+            if (secondaryCpuAbi != null) {
+                return List.of(VMRuntime.getInstructionSet(primaryCpuAbi),
+                        VMRuntime.getInstructionSet(secondaryCpuAbi));
+            }
+            return List.of(VMRuntime.getInstructionSet(primaryCpuAbi));
+        }
+        return List.of();
+    }
+
+    @NonNull
+    public static ArtifactsPath buildArtifactsPath(
+            @NonNull String dexPath, @NonNull String isa, boolean isInDalvikCache) {
+        ArtifactsPath artifactsPath = new ArtifactsPath();
+        artifactsPath.dexPath = dexPath;
+        artifactsPath.isa = isa;
+        artifactsPath.isInDalvikCache = isInDalvikCache;
+        return artifactsPath;
+    }
+
+    public static boolean isInDalvikCache(@NonNull PackageState pkg) {
+        return pkg.isSystem() && !pkg.isUpdatedSystemApp();
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/model/ArtFlags.java b/libartservice/service/java/com/android/server/art/model/ArtFlags.java
new file mode 100644
index 0000000..0ecad74
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/ArtFlags.java
@@ -0,0 +1,80 @@
+/*
+ * 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.IntDef;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** @hide */
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+public class ArtFlags {
+    /** Whether the operation is applied for primary dex'es. */
+    public static final int FLAG_FOR_PRIMARY_DEX = 1 << 0;
+    /** Whether the operation is applied for secondary dex'es. */
+    public static final int FLAG_FOR_SECONDARY_DEX = 1 << 1;
+
+    /**
+     * Flags for {@link ArtManagerLocal#deleteOptimizedArtifacts(PackageDataSnapshot, String, int)}.
+     *
+     * @hide
+     */
+    // clang-format off
+    @IntDef(flag = true, prefix = "FLAG_", value = {
+        FLAG_FOR_PRIMARY_DEX,
+        FLAG_FOR_SECONDARY_DEX,
+    })
+    // clang-format on
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DeleteFlags {}
+
+    /**
+     * Default flags that are used when
+     * {@link ArtManagerLocal#deleteOptimizedArtifacts(PackageDataSnapshot, String)} is called.
+     * Value: {@link #FLAG_FOR_PRIMARY_DEX}.
+     */
+    public static @DeleteFlags int defaultDeleteFlags() {
+        return FLAG_FOR_PRIMARY_DEX;
+    }
+
+    /**
+     * Flags for {@link ArtManagerLocal#getOptimizationStatus(PackageDataSnapshot, String, int)}.
+     *
+     * @hide
+     */
+    // clang-format off
+    @IntDef(flag = true, prefix = "FLAG_", value = {
+        FLAG_FOR_PRIMARY_DEX,
+        FLAG_FOR_SECONDARY_DEX,
+    })
+    // clang-format on
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface GetStatusFlags {}
+
+    /**
+     * Default flags that are used when
+     * {@link ArtManagerLocal#getOptimizationStatus(PackageDataSnapshot, String)} is called.
+     * Value: {@link #FLAG_FOR_PRIMARY_DEX}.
+     */
+    public static @GetStatusFlags int defaultGetStatusFlags() {
+        return FLAG_FOR_PRIMARY_DEX;
+    }
+
+    private ArtFlags() {}
+}
diff --git a/libartservice/service/java/com/android/server/art/model/DeleteResult.java b/libartservice/service/java/com/android/server/art/model/DeleteResult.java
new file mode 100644
index 0000000..fc40cbc
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/DeleteResult.java
@@ -0,0 +1,35 @@
+/*
+ * 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.SystemApi;
+
+/** @hide */
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+public class DeleteResult {
+    private long mFreedBytes;
+
+    /** @hide */
+    public DeleteResult(long freedBytes) {
+        mFreedBytes = freedBytes;
+    }
+
+    /** The amount of the disk space freed by the deletion, in bytes. */
+    public long getFreedBytes() {
+        return mFreedBytes;
+    }
+}
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..724b0dd
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/OptimizationStatus.java
@@ -0,0 +1,120 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Describes the optimization status of a package.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+@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/java/com/android/server/art/wrapper/AndroidPackageApi.java b/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java
new file mode 100644
index 0000000..90adb49
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java
@@ -0,0 +1,117 @@
+/*
+ * 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.wrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+/** @hide */
+public class AndroidPackageApi {
+    private final Object mPkg;
+
+    AndroidPackageApi(@NonNull Object pkg) {
+        mPkg = pkg;
+    }
+
+    Object getRealInstance() {
+        return mPkg;
+    }
+
+    @NonNull
+    public String getBaseApkPath() {
+        try {
+            return (String) mPkg.getClass().getMethod("getBaseApkPath").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean isHasCode() {
+        try {
+            return (boolean) mPkg.getClass().getMethod("isHasCode").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public String[] getSplitNames() {
+        try {
+            return (String[]) mPkg.getClass().getMethod("getSplitNames").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public String[] getSplitCodePaths() {
+        try {
+            return (String[]) mPkg.getClass().getMethod("getSplitCodePaths").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public int[] getSplitFlags() {
+        try {
+            Class<?> parsingPackageImplClass =
+                    Class.forName("com.android.server.pm.pkg.parsing.ParsingPackageImpl");
+            return (int[]) parsingPackageImplClass.getMethod("getSplitFlags").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public String getClassLoaderName() {
+        try {
+            return (String) mPkg.getClass().getMethod("getClassLoaderName").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public String[] getSplitClassLoaderNames() {
+        try {
+            return (String[]) mPkg.getClass().getMethod("getSplitClassLoaderNames").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public SparseArray<int[]> getSplitDependencies() {
+        try {
+            return (SparseArray<int[]>) mPkg.getClass()
+                    .getMethod("getSplitDependencies")
+                    .invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean isIsolatedSplitLoading() {
+        try {
+            return (boolean) mPkg.getClass().getMethod("isIsolatedSplitLoading").invoke(mPkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/PackageManagerLocal.java b/libartservice/service/java/com/android/server/art/wrapper/PackageManagerLocal.java
new file mode 100644
index 0000000..650d29c
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/PackageManagerLocal.java
@@ -0,0 +1,79 @@
+/*
+ * 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.wrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.UserHandle;
+
+import com.android.server.pm.snapshot.PackageDataSnapshot;
+
+/** @hide */
+public class PackageManagerLocal {
+    private final Object mPackageManagerInternal;
+
+    /**
+     * Returns an instance this class, which is a reflection-based implementation of {@link
+     * com.android.server.pm.PackageManagerLocal}.
+     * Note: This is NOT a real system API! Use {@link LocalManagerRegistry} for getting a real
+     * instance.
+     */
+    @NonNull
+    public static PackageManagerLocal getInstance() throws Exception {
+        Class<?> localServicesClass = Class.forName("com.android.server.LocalServices");
+        Class<?> packageManagerInternalClass =
+                Class.forName("android.content.pm.PackageManagerInternal");
+        Object packageManagerInternal = localServicesClass.getMethod("getService", Class.class)
+                                                .invoke(null, packageManagerInternalClass);
+        if (packageManagerInternal == null) {
+            throw new Exception("Failed to get PackageManagerInternal");
+        }
+        return new PackageManagerLocal(packageManagerInternal);
+    }
+
+    private PackageManagerLocal(@NonNull Object packageManagerInternal) {
+        mPackageManagerInternal = packageManagerInternal;
+    }
+
+    @NonNull
+    public PackageDataSnapshot snapshot() {
+        try {
+            return (PackageDataSnapshot) mPackageManagerInternal.getClass()
+                    .getMethod("snapshot")
+                    .invoke(mPackageManagerInternal);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public PackageState getPackageState(@NonNull PackageDataSnapshot snapshot,
+            @NonNull int callingUid, @NonNull String packageName) {
+        try {
+            int userId = (int) UserHandle.class.getMethod("getUserId", int.class)
+                                 .invoke(null, callingUid);
+            Class<?> computerClass = Class.forName("com.android.server.pm.Computer");
+            Object packageState = computerClass
+                                          .getMethod("getPackageStateForInstalledAndFiltered",
+                                                  String.class, int.class, int.class)
+                                          .invoke(snapshot, packageName, callingUid, userId);
+            return packageState != null ? new PackageState(packageState) : null;
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/PackageState.java b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
new file mode 100644
index 0000000..2235514
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
@@ -0,0 +1,144 @@
+/*
+ * 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.wrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** @hide */
+public class PackageState {
+    private final Object mPkgState;
+
+    PackageState(@NonNull Object pkgState) {
+        mPkgState = pkgState;
+    }
+
+    @Nullable
+    public AndroidPackageApi getAndroidPackage() {
+        try {
+            Object pkg = mPkgState.getClass().getMethod("getAndroidPackage").invoke(mPkgState);
+            return pkg != null ? new AndroidPackageApi(pkg) : null;
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public String getPackageName() {
+        try {
+            return (String) mPkgState.getClass().getMethod("getPackageName").invoke(mPkgState);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NonNull
+    public List<SharedLibraryInfo> getUsesLibraryInfos() {
+        try {
+            Object packageStateUnserialized =
+                    mPkgState.getClass().getMethod("getTransientState").invoke(mPkgState);
+            var list = (List<?>) packageStateUnserialized.getClass()
+                               .getMethod("getUsesLibraryInfos")
+                               .invoke(packageStateUnserialized);
+            return list.stream()
+                    .map(obj -> new SharedLibraryInfo(obj))
+                    .collect(Collectors.toList());
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public String getPrimaryCpuAbi() {
+        try {
+            String abi =
+                    (String) mPkgState.getClass().getMethod("getPrimaryCpuAbi").invoke(mPkgState);
+            if (!TextUtils.isEmpty(abi)) {
+                return abi;
+            }
+
+            // Default to the information in `AndroidPackageApi`. The defaulting behavior will
+            // eventually be done by `PackageState` internally.
+            AndroidPackageApi pkg = getAndroidPackage();
+            if (pkg == null) {
+                // This should never happen because we check the existence of the package at the
+                // beginning of each ART Services method.
+                throw new IllegalStateException("Unable to get package " + getPackageName()
+                        + ". This should never happen.");
+            }
+
+            Class<?> androidPackageHiddenClass =
+                    Class.forName("com.android.server.pm.parsing.pkg.AndroidPackageHidden");
+            return (String) androidPackageHiddenClass.getMethod("getPrimaryCpuAbi")
+                    .invoke(pkg.getRealInstance());
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public String getSecondaryCpuAbi() {
+        try {
+            String abi =
+                    (String) mPkgState.getClass().getMethod("getSecondaryCpuAbi").invoke(mPkgState);
+            if (!TextUtils.isEmpty(abi)) {
+                return abi;
+            }
+
+            // Default to the information in `AndroidPackageApi`. The defaulting behavior will
+            // eventually be done by `PackageState` internally.
+            AndroidPackageApi pkg = getAndroidPackage();
+            if (pkg == null) {
+                // This should never happen because we check the existence of the package at the
+                // beginning of each ART Services method.
+                throw new IllegalStateException("Unable to get package " + getPackageName()
+                        + ". This should never happen.");
+            }
+
+            Class<?> androidPackageHiddenClass =
+                    Class.forName("com.android.server.pm.parsing.pkg.AndroidPackageHidden");
+            return (String) androidPackageHiddenClass.getMethod("getSecondaryCpuAbi")
+                    .invoke(pkg.getRealInstance());
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean isSystem() {
+        try {
+            return (boolean) mPkgState.getClass().getMethod("isSystem").invoke(mPkgState);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean isUpdatedSystemApp() {
+        try {
+            Object packageStateUnserialized =
+                    mPkgState.getClass().getMethod("getTransientState").invoke(mPkgState);
+            return (boolean) packageStateUnserialized.getClass()
+                    .getMethod("isUpdatedSystemApp")
+                    .invoke(packageStateUnserialized);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/README.md b/libartservice/service/java/com/android/server/art/wrapper/README.md
new file mode 100644
index 0000000..829fc1c
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/README.md
@@ -0,0 +1,11 @@
+This folder contains temporary wrappers that access system server internal
+classes using reflection. Having the wrappers is the workaround for the current
+time being where required system APIs are not finalized. The classes and methods
+correspond to system APIs planned to be exposed.
+
+The mappings are:
+
+- `AndroidPackageApi`: `com.android.server.pm.pkg.AndroidPackageApi`
+- `PackageManagerLocal`: `com.android.server.pm.PackageManagerLocal`
+- `PackageState`: `com.android.server.pm.pkg.PackageState`
+- `SharedLibraryInfo`: `android.content.pm.SharedLibraryInfo`
diff --git a/libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.java b/libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.java
new file mode 100644
index 0000000..f2bde16
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.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.wrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** @hide */
+public class SharedLibraryInfo {
+    private final Object mInfo;
+
+    SharedLibraryInfo(@NonNull Object info) {
+        mInfo = info;
+    }
+
+    @NonNull
+    public List<String> getAllCodePaths() {
+        try {
+            return (List<String>) mInfo.getClass().getMethod("getAllCodePaths").invoke(mInfo);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Nullable
+    public List<SharedLibraryInfo> getDependencies() {
+        try {
+            var list = (List<?>) mInfo.getClass().getMethod("getDependencies").invoke(mInfo);
+            if (list == null) {
+                return null;
+            }
+            return list.stream()
+                    .map(obj -> new SharedLibraryInfo(obj))
+                    .collect(Collectors.toList());
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index a27dfa5..0e958ba 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -16,29 +16,217 @@
 
 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;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.ApplicationInfo;
+
 import androidx.test.filters.SmallTest;
 
-import com.android.server.art.ArtManagerLocal;
+import com.android.server.art.model.DeleteResult;
+import com.android.server.art.model.OptimizationStatus;
+import com.android.server.art.wrapper.AndroidPackageApi;
+import com.android.server.art.wrapper.PackageManagerLocal;
+import com.android.server.art.wrapper.PackageState;
+import com.android.server.pm.snapshot.PackageDataSnapshot;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+
+import java.util.List;
 
 @SmallTest
-@RunWith(MockitoJUnitRunner.class)
+@RunWith(Parameterized.class)
 public class ArtManagerLocalTest {
+    private static final String PKG_NAME = "com.example.foo";
+
+    @Rule public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+    @Mock private ArtManagerLocal.Injector mInjector;
+    @Mock private PackageManagerLocal mPackageManagerLocal;
+    @Mock private IArtd mArtd;
+    private PackageState mPkgState;
+
+    // True if the primary dex'es are in a readonly partition.
+    @Parameter(0) public boolean mIsInReadonlyPartition;
+
     private ArtManagerLocal mArtManagerLocal;
 
+    @Parameters(name = "isInReadonlyPartition={0}")
+    public static Iterable<? extends Object> data() {
+        return List.of(false, true);
+    }
+
     @Before
-    public void setUp() {
-        mArtManagerLocal = new ArtManagerLocal();
+    public void setUp() throws Exception {
+        lenient().when(mInjector.getPackageManagerLocal()).thenReturn(mPackageManagerLocal);
+        lenient().when(mInjector.getArtd()).thenReturn(mArtd);
+
+        mPkgState = createPackageState();
+        lenient()
+                .when(mPackageManagerLocal.getPackageState(any(), anyInt(), eq(PKG_NAME)))
+                .thenReturn(mPkgState);
+
+        mArtManagerLocal = new ArtManagerLocal(mInjector);
     }
 
     @Test
-    public void testScaffolding() {
-        assertThat(true).isTrue();
+    public void testDeleteOptimizedArtifacts() throws Exception {
+        when(mArtd.deleteArtifacts(any())).thenReturn(1l);
+
+        DeleteResult result = mArtManagerLocal.deleteOptimizedArtifacts(
+                mock(PackageDataSnapshot.class), PKG_NAME);
+        assertThat(result.getFreedBytes()).isEqualTo(4);
+
+        verify(mArtd).deleteArtifacts(argThat(artifactsPath
+                -> artifactsPath.dexPath.equals("/data/app/foo/base.apk")
+                        && artifactsPath.isa.equals("arm64")
+                        && artifactsPath.isInDalvikCache == mIsInReadonlyPartition));
+        verify(mArtd).deleteArtifacts(argThat(artifactsPath
+                -> artifactsPath.dexPath.equals("/data/app/foo/base.apk")
+                        && artifactsPath.isa.equals("arm")
+                        && artifactsPath.isInDalvikCache == mIsInReadonlyPartition));
+        verify(mArtd).deleteArtifacts(argThat(artifactsPath
+                -> artifactsPath.dexPath.equals("/data/app/foo/split_0.apk")
+                        && artifactsPath.isa.equals("arm64")
+                        && artifactsPath.isInDalvikCache == mIsInReadonlyPartition));
+        verify(mArtd).deleteArtifacts(argThat(artifactsPath
+                -> artifactsPath.dexPath.equals("/data/app/foo/split_0.apk")
+                        && artifactsPath.isa.equals("arm")
+                        && artifactsPath.isInDalvikCache == mIsInReadonlyPartition));
+        verifyNoMoreInteractions(mArtd);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testDeleteOptimizedArtifactsPackageNotFound() throws Exception {
+        when(mPackageManagerLocal.getPackageState(any(), anyInt(), eq(PKG_NAME))).thenReturn(null);
+
+        mArtManagerLocal.deleteOptimizedArtifacts(mock(PackageDataSnapshot.class), PKG_NAME);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testDeleteOptimizedArtifactsNoPackage() throws Exception {
+        when(mPkgState.getAndroidPackage()).thenReturn(null);
+
+        mArtManagerLocal.deleteOptimizedArtifacts(mock(PackageDataSnapshot.class), PKG_NAME);
+    }
+
+    @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);
+
+        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);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testGetOptimizationStatusNoPackage() throws Exception {
+        when(mPkgState.getAndroidPackage()).thenReturn(null);
+
+        mArtManagerLocal.getOptimizationStatus(mock(PackageDataSnapshot.class), PKG_NAME);
+    }
+
+    private AndroidPackageApi createPackage() {
+        AndroidPackageApi pkg = mock(AndroidPackageApi.class);
+
+        lenient().when(pkg.getBaseApkPath()).thenReturn("/data/app/foo/base.apk");
+        lenient().when(pkg.isHasCode()).thenReturn(true);
+
+        // split_0 has code while split_1 doesn't.
+        lenient().when(pkg.getSplitNames()).thenReturn(new String[] {"split_0", "split_1"});
+        lenient()
+                .when(pkg.getSplitCodePaths())
+                .thenReturn(
+                        new String[] {"/data/app/foo/split_0.apk", "/data/app/foo/split_1.apk"});
+        lenient()
+                .when(pkg.getSplitFlags())
+                .thenReturn(new int[] {ApplicationInfo.FLAG_HAS_CODE, 0});
+
+        return pkg;
+    }
+
+    private PackageState createPackageState() {
+        PackageState pkgState = mock(PackageState.class);
+
+        lenient().when(pkgState.getPackageName()).thenReturn(PKG_NAME);
+        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);
+        AndroidPackageApi pkg = createPackage();
+        lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
+
+        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;
     }
 }
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java
new file mode 100644
index 0000000..5a81e01
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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;
+
+import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo;
+import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.ApplicationInfo;
+import android.util.SparseArray;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.art.PrimaryDexUtils;
+import com.android.server.art.wrapper.AndroidPackageApi;
+import com.android.server.art.wrapper.PackageState;
+import com.android.server.art.wrapper.SharedLibraryInfo;
+
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.DexClassLoader;
+import dalvik.system.PathClassLoader;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class PrimaryDexUtilsTest {
+    @Before
+    public void setUp() {}
+
+    @Test
+    public void testGetDexInfo() {
+        List<PrimaryDexInfo> infos =
+                PrimaryDexUtils.getDexInfo(createPackage(false /* isIsolatedSplitLoading */));
+        checkBasicInfo(infos);
+    }
+
+    @Test
+    public void testGetDetailedDexInfo() {
+        List<DetailedPrimaryDexInfo> infos = PrimaryDexUtils.getDetailedDexInfo(
+                createPackageState(), createPackage(false /* isIsolatedSplitLoading */));
+        checkBasicInfo(infos);
+
+        String sharedLibrariesContext = "{"
+                + "PCL[library_2.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}#"
+                + "PCL[library_3.jar]#"
+                + "PCL[library_4.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}"
+                + "}";
+
+        assertThat(infos.get(0).classLoaderContext()).isEqualTo("PCL[]" + sharedLibrariesContext);
+        assertThat(infos.get(1).classLoaderContext())
+                .isEqualTo("PCL[base.apk]" + sharedLibrariesContext);
+        assertThat(infos.get(2).classLoaderContext()).isEqualTo(null);
+        assertThat(infos.get(3).classLoaderContext())
+                .isEqualTo("PCL[base.apk:split_0.apk:split_1.apk]" + sharedLibrariesContext);
+        assertThat(infos.get(4).classLoaderContext())
+                .isEqualTo("PCL[base.apk:split_0.apk:split_1.apk:split_2.apk]"
+                        + sharedLibrariesContext);
+    }
+
+    @Test
+    public void testGetDetailedDexInfoIsolated() {
+        List<DetailedPrimaryDexInfo> infos = PrimaryDexUtils.getDetailedDexInfo(
+                createPackageState(), createPackage(true /* isIsolatedSplitLoading */));
+        checkBasicInfo(infos);
+
+        String sharedLibrariesContext = "{"
+                + "PCL[library_2.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}#"
+                + "PCL[library_3.jar]#"
+                + "PCL[library_4.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}"
+                + "}";
+
+        assertThat(infos.get(0).classLoaderContext()).isEqualTo("PCL[]" + sharedLibrariesContext);
+        assertThat(infos.get(1).classLoaderContext())
+                .isEqualTo("PCL[];DLC[split_2.apk];PCL[base.apk]" + sharedLibrariesContext);
+        assertThat(infos.get(2).classLoaderContext()).isEqualTo(null);
+        assertThat(infos.get(3).classLoaderContext())
+                .isEqualTo("DLC[];PCL[base.apk]" + sharedLibrariesContext);
+        assertThat(infos.get(4).classLoaderContext()).isEqualTo("PCL[]");
+        assertThat(infos.get(5).classLoaderContext()).isEqualTo("PCL[];PCL[split_3.apk]");
+    }
+
+    private <T extends PrimaryDexInfo> void checkBasicInfo(List<T> infos) {
+        assertThat(infos.get(0).dexPath()).isEqualTo("/data/app/foo/base.apk");
+        assertThat(infos.get(0).hasCode()).isTrue();
+        assertThat(infos.get(0).isBaseApk()).isTrue();
+        assertThat(infos.get(0).splitIndex()).isEqualTo(-1);
+        assertThat(infos.get(0).splitName()).isNull();
+
+        assertThat(infos.get(1).dexPath()).isEqualTo("/data/app/foo/split_0.apk");
+        assertThat(infos.get(1).hasCode()).isTrue();
+        assertThat(infos.get(1).isBaseApk()).isFalse();
+        assertThat(infos.get(1).splitIndex()).isEqualTo(0);
+        assertThat(infos.get(1).splitName()).isEqualTo("split_0");
+
+        assertThat(infos.get(2).dexPath()).isEqualTo("/data/app/foo/split_1.apk");
+        assertThat(infos.get(2).hasCode()).isFalse();
+        assertThat(infos.get(2).isBaseApk()).isFalse();
+        assertThat(infos.get(2).splitIndex()).isEqualTo(1);
+        assertThat(infos.get(2).splitName()).isEqualTo("split_1");
+
+        assertThat(infos.get(3).dexPath()).isEqualTo("/data/app/foo/split_2.apk");
+        assertThat(infos.get(3).hasCode()).isTrue();
+        assertThat(infos.get(3).isBaseApk()).isFalse();
+        assertThat(infos.get(3).splitIndex()).isEqualTo(2);
+        assertThat(infos.get(3).splitName()).isEqualTo("split_2");
+
+        assertThat(infos.get(4).dexPath()).isEqualTo("/data/app/foo/split_3.apk");
+        assertThat(infos.get(4).hasCode()).isTrue();
+        assertThat(infos.get(4).isBaseApk()).isFalse();
+        assertThat(infos.get(4).splitIndex()).isEqualTo(3);
+        assertThat(infos.get(4).splitName()).isEqualTo("split_3");
+
+        assertThat(infos.get(5).dexPath()).isEqualTo("/data/app/foo/split_4.apk");
+        assertThat(infos.get(5).hasCode()).isTrue();
+        assertThat(infos.get(5).isBaseApk()).isFalse();
+        assertThat(infos.get(5).splitIndex()).isEqualTo(4);
+        assertThat(infos.get(5).splitName()).isEqualTo("split_4");
+    }
+
+    private AndroidPackageApi createPackage(boolean isIsolatedSplitLoading) {
+        AndroidPackageApi pkg = mock(AndroidPackageApi.class);
+
+        when(pkg.getBaseApkPath()).thenReturn("/data/app/foo/base.apk");
+        when(pkg.isHasCode()).thenReturn(true);
+        when(pkg.getClassLoaderName()).thenReturn(PathClassLoader.class.getName());
+
+        when(pkg.getSplitNames())
+                .thenReturn(new String[] {"split_0", "split_1", "split_2", "split_3", "split_4"});
+        when(pkg.getSplitCodePaths())
+                .thenReturn(new String[] {
+                        "/data/app/foo/split_0.apk",
+                        "/data/app/foo/split_1.apk",
+                        "/data/app/foo/split_2.apk",
+                        "/data/app/foo/split_3.apk",
+                        "/data/app/foo/split_4.apk",
+                });
+        when(pkg.getSplitFlags())
+                .thenReturn(new int[] {
+                        ApplicationInfo.FLAG_HAS_CODE,
+                        0,
+                        ApplicationInfo.FLAG_HAS_CODE,
+                        ApplicationInfo.FLAG_HAS_CODE,
+                        ApplicationInfo.FLAG_HAS_CODE,
+                });
+
+        if (isIsolatedSplitLoading) {
+            // split_0: PCL(PathClassLoader), depends on split_2.
+            // split_1: no code.
+            // split_2: DLC(DelegateLastClassLoader), depends on base.
+            // split_3: PCL(DexClassLoader), no dependency.
+            // split_4: PCL(null), depends on split_3.
+            when(pkg.isIsolatedSplitLoading()).thenReturn(true);
+            when(pkg.getSplitClassLoaderNames())
+                    .thenReturn(new String[] {
+                            PathClassLoader.class.getName(),
+                            null,
+                            DelegateLastClassLoader.class.getName(),
+                            DexClassLoader.class.getName(),
+                            null,
+                    });
+            SparseArray<int[]> splitDependencies = new SparseArray<>();
+            splitDependencies.set(1, new int[] {3});
+            splitDependencies.set(3, new int[] {0});
+            splitDependencies.set(5, new int[] {4});
+            when(pkg.getSplitDependencies()).thenReturn(splitDependencies);
+        } else {
+            when(pkg.isIsolatedSplitLoading()).thenReturn(false);
+        }
+
+        return pkg;
+    }
+
+    private PackageState createPackageState() {
+        PackageState pkgState = mock(PackageState.class);
+
+        when(pkgState.getPackageName()).thenReturn("com.example.foo");
+
+        // Base depends on library 2, 3, 4.
+        // Library 2, 4 depends on library 1.
+        List<SharedLibraryInfo> usesLibraryInfos = new ArrayList<>();
+
+        SharedLibraryInfo library1 = mock(SharedLibraryInfo.class);
+        when(library1.getAllCodePaths())
+                .thenReturn(List.of("library_1_dex_1.jar", "library_1_dex_2.jar"));
+        when(library1.getDependencies()).thenReturn(null);
+
+        SharedLibraryInfo library2 = mock(SharedLibraryInfo.class);
+        when(library2.getAllCodePaths()).thenReturn(List.of("library_2.jar"));
+        when(library2.getDependencies()).thenReturn(List.of(library1));
+        usesLibraryInfos.add(library2);
+
+        SharedLibraryInfo library3 = mock(SharedLibraryInfo.class);
+        when(library3.getAllCodePaths()).thenReturn(List.of("library_3.jar"));
+        when(library3.getDependencies()).thenReturn(null);
+        usesLibraryInfos.add(library3);
+
+        SharedLibraryInfo library4 = mock(SharedLibraryInfo.class);
+        when(library4.getAllCodePaths()).thenReturn(List.of("library_4.jar"));
+        when(library4.getDependencies()).thenReturn(List.of(library1));
+        usesLibraryInfos.add(library4);
+
+        when(pkgState.getUsesLibraryInfos()).thenReturn(usesLibraryInfos);
+
+        return pkgState;
+    }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/UtilsTest.java b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
new file mode 100644
index 0000000..da39eec
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.SparseArray;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.art.Utils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.List;
+
+@SmallTest
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class UtilsTest {
+    @Test
+    public void testCollectionIsEmptyTrue() {
+        assertThat(Utils.isEmpty(List.of())).isTrue();
+    }
+
+    @Test
+    public void testCollectionIsEmptyFalse() {
+        assertThat(Utils.isEmpty(List.of(1))).isFalse();
+    }
+
+    @Test
+    public void testSparseArrayIsEmptyTrue() {
+        assertThat(Utils.isEmpty(new SparseArray<Integer>())).isTrue();
+    }
+
+    @Test
+    public void testSparseArrayIsEmptyFalse() {
+        SparseArray<Integer> array = new SparseArray<>();
+        array.put(1, 1);
+        assertThat(Utils.isEmpty(array)).isFalse();
+    }
+
+    @Test
+    public void testArrayIsEmptyTrue() {
+        assertThat(Utils.isEmpty(new int[0])).isTrue();
+    }
+
+    @Test
+    public void testArrayIsEmptyFalse() {
+        assertThat(Utils.isEmpty(new int[] {1})).isFalse();
+    }
+}
diff --git a/runtime/oat_file_assistant.cc b/runtime/oat_file_assistant.cc
index cf0155f..f7c9e03 100644
--- a/runtime/oat_file_assistant.cc
+++ b/runtime/oat_file_assistant.cc
@@ -22,6 +22,7 @@
 #include <sstream>
 
 #include "android-base/file.h"
+#include "android-base/logging.h"
 #include "android-base/stringprintf.h"
 #include "android-base/strings.h"
 #include "arch/instruction_set.h"
@@ -234,6 +235,47 @@
   }
 }
 
+std::unique_ptr<OatFileAssistant> OatFileAssistant::Create(
+    const std::string& filename,
+    const std::string& isa_str,
+    const std::string& context_str,
+    bool load_executable,
+    bool only_load_trusted_executable,
+    std::unique_ptr<RuntimeOptions> runtime_options,
+    std::string* error_msg) {
+  InstructionSet isa = GetInstructionSetFromString(isa_str.c_str());
+  if (isa == InstructionSet::kNone) {
+    *error_msg = StringPrintf("Instruction set '%s' is invalid", isa_str.c_str());
+    return nullptr;
+  }
+
+  std::unique_ptr<ClassLoaderContext> context = ClassLoaderContext::Create(context_str.c_str());
+  if (context == nullptr) {
+    *error_msg = StringPrintf("Class loader context '%s' is invalid", context_str.c_str());
+    return nullptr;
+  }
+
+  if (!context->OpenDexFiles(android::base::Dirname(filename.c_str()),
+                             /*context_fds=*/{},
+                             /*only_read_checksums=*/true)) {
+    *error_msg =
+        StringPrintf("Failed to load class loader context files for '%s' with context '%s'",
+                     filename.c_str(),
+                     context_str.c_str());
+    return nullptr;
+  }
+
+  auto assistant = std::make_unique<OatFileAssistant>(filename.c_str(),
+                                                      isa,
+                                                      context.get(),
+                                                      load_executable,
+                                                      only_load_trusted_executable,
+                                                      std::move(runtime_options));
+
+  assistant->owned_context_ = std::move(context);
+  return assistant;
+}
+
 bool OatFileAssistant::UseFdToReadFiles() {
   return zip_fd_ >= 0;
 }
@@ -1141,57 +1183,6 @@
       &out_odex_status);
 }
 
-bool OatFileAssistant::GetOptimizationStatus(const std::string& filename,
-                                             const std::string& isa_str,
-                                             const std::string& context_str,
-                                             std::unique_ptr<RuntimeOptions> runtime_options,
-                                             /*out*/ std::string* compiler_filter,
-                                             /*out*/ std::string* compilation_reason,
-                                             /*out*/ std::string* odex_location,
-                                             /*out*/ std::string* error_msg) {
-  InstructionSet isa = GetInstructionSetFromString(isa_str.c_str());
-  if (isa == InstructionSet::kNone) {
-    *error_msg = StringPrintf("Instruction set '%s' is invalid", isa_str.c_str());
-    return false;
-  }
-
-  std::unique_ptr<ClassLoaderContext> context = ClassLoaderContext::Create(context_str.c_str());
-  if (context == nullptr) {
-    *error_msg = StringPrintf("Class loader context '%s' is invalid", context_str.c_str());
-    return false;
-  }
-
-  std::vector<int> context_fds;
-  if (!context->OpenDexFiles(android::base::Dirname(filename.c_str()),
-                             context_fds,
-                             /*only_read_checksums=*/true)) {
-    *error_msg =
-        StringPrintf("Failed to load class loader context files for '%s' with context '%s'",
-                     filename.c_str(),
-                     context_str.c_str());
-    return false;
-  }
-
-  OatFileAssistant oat_file_assistant(filename.c_str(),
-                                      isa,
-                                      context.get(),
-                                      /*load_executable=*/false,
-                                      /*only_load_trusted_executable=*/true,
-                                      std::move(runtime_options));
-
-  // We ignore the odex_status because it is not meaningful. It can never be
-  // "boot-image-more-recent" or "context-mismatch". In the case where the boot image has changed or
-  // there is a context mismatch, the value is "up-to-date" because the vdex file is still usable.
-  // I.e., it can only be either "up-to-date" or "apk-more-recent", which means it doesn't give us
-  // information in addition to what we can learn from compiler_filter.
-  std::string ignored_odex_status;
-
-  oat_file_assistant.GetOptimizationStatus(
-      odex_location, compiler_filter, compilation_reason, &ignored_odex_status);
-
-  return true;
-}
-
 void OatFileAssistant::GetOptimizationStatus(
     std::string* out_odex_location,
     std::string* out_compilation_filter,
@@ -1218,28 +1209,25 @@
   OatStatus status = oat_file_info.Status();
   const char* reason = oat_file->GetCompilationReason();
   *out_compilation_reason = reason == nullptr ? "unknown" : reason;
+
+  // If the oat file is invalid, the vdex file will be picked, so the status is `kOatUpToDate`. If
+  // the vdex file is also invalid, then either `oat_file` is nullptr, or `status` is
+  // `kOatDexOutOfDate`.
+  DCHECK(status == kOatUpToDate || status == kOatDexOutOfDate);
+
   switch (status) {
     case kOatUpToDate:
       *out_compilation_filter = CompilerFilter::NameOfFilter(oat_file->GetCompilerFilter());
       *out_odex_status = "up-to-date";
       return;
 
-    case kOatCannotOpen:  // This should never happen, but be robust.
-      *out_compilation_filter = "error";
-      *out_compilation_reason = "error";
-      // This mostly happens when we cannot open the vdex file,
-      // or the file is corrupt.
-      *out_odex_status = "io-error-or-corruption";
-      return;
-
+    case kOatCannotOpen:
     case kOatBootImageOutOfDate:
-      *out_compilation_filter = "run-from-apk-fallback";
-      *out_odex_status = "boot-image-more-recent";
-      return;
-
     case kOatContextOutOfDate:
-      *out_compilation_filter = "run-from-apk-fallback";
-      *out_odex_status = "context-mismatch";
+      // These should never happen, but be robust.
+      *out_compilation_filter = "unexpected";
+      *out_compilation_reason = "unexpected";
+      *out_odex_status = "unexpected";
       return;
 
     case kOatDexOutOfDate:
diff --git a/runtime/oat_file_assistant.h b/runtime/oat_file_assistant.h
index 527f473..1198832 100644
--- a/runtime/oat_file_assistant.h
+++ b/runtime/oat_file_assistant.h
@@ -156,6 +156,17 @@
                    int oat_fd,
                    int zip_fd);
 
+  // A convenient factory function that accepts ISA, class loader context, and compiler filter in
+  // strings. Returns the created instance on success, or returns nullptr and outputs an error
+  // message if it fails to parse the input strings.
+  static std::unique_ptr<OatFileAssistant> Create(const std::string& filename,
+                                                  const std::string& isa_str,
+                                                  const std::string& context_str,
+                                                  bool load_executable,
+                                                  bool only_load_trusted_executable,
+                                                  std::unique_ptr<RuntimeOptions> runtime_options,
+                                                  std::string* error_msg);
+
   // Returns true if the dex location refers to an element of the boot class
   // path.
   bool IsInBootClassPath();
@@ -204,7 +215,7 @@
   //   - out_compilation_reason: the optimization reason. The reason might
   //        be "unknown" if the compiler artifacts were not annotated during optimizations.
   //   - out_odex_status: a human readable refined status of the validity of the odex file.
-  //        E.g. up-to-date, apk-more-recent.
+  //        Possible values are: "up-to-date", "apk-more-recent", and "io-error-no-oat".
   //
   // This method will try to mimic the runtime effect of loading the dex file.
   // For example, if there is no usable oat file, the compiler filter will be set
@@ -220,18 +231,6 @@
                                     std::string* out_compilation_reason,
                                     std::unique_ptr<RuntimeOptions> runtime_options = nullptr);
 
-  // A convenient version of `GetOptimizationStatus` that accepts ISA and class loader context in
-  // strings. Returns true on success, or returns false and outputs an error message if it fails to
-  // parse the input strings.
-  static bool GetOptimizationStatus(const std::string& filename,
-                                    const std::string& isa_str,
-                                    const std::string& context_str,
-                                    std::unique_ptr<RuntimeOptions> runtime_options,
-                                    /*out*/ std::string* compiler_filter,
-                                    /*out*/ std::string* compilation_reason,
-                                    /*out*/ std::string* odex_location,
-                                    /*out*/ std::string* error_msg);
-
   // Open and returns an image space associated with the oat file.
   static std::unique_ptr<gc::space::ImageSpace> OpenImageSpace(const OatFile* oat_file);
 
@@ -472,6 +471,7 @@
   std::string dex_location_;
 
   ClassLoaderContext* context_;
+  std::unique_ptr<ClassLoaderContext> owned_context_;
 
   // Whether or not the parent directory of the dex file is writable.
   bool dex_parent_writable_ = false;
diff --git a/runtime/oat_file_assistant_test.cc b/runtime/oat_file_assistant_test.cc
index c0662aa..e414e4e 100644
--- a/runtime/oat_file_assistant_test.cc
+++ b/runtime/oat_file_assistant_test.cc
@@ -88,26 +88,7 @@
       ASSERT_EQ(expected_reason, compilation_reason1);
     }
 
-    // Verify the static method (called from artd).
-    std::string compilation_filter2;
-    std::string compilation_reason2;
-    std::string odex_location2;  // ignored
-    std::string error_msg;       // ignored
-
-    ASSERT_TRUE(
-        OatFileAssistant::GetOptimizationStatus(file,
-                                                GetInstructionSetString(kRuntimeISA),
-                                                context->EncodeContextForDex2oat(/*base_dir=*/""),
-                                                MaybeCreateRuntimeOptions(),
-                                                &compilation_filter2,
-                                                &compilation_reason2,
-                                                &odex_location2,
-                                                &error_msg));
-
-    ASSERT_EQ(expected_filter_name, compilation_filter2);
-    ASSERT_EQ(expected_reason, compilation_reason2);
-
-    // Verify the instance methods (called at runtime).
+    // Verify the instance methods (called at runtime and from artd).
     OatFileAssistant assistant = CreateOatFileAssistant(file.c_str(), context);
 
     std::string odex_location3;  // ignored
@@ -1796,6 +1777,29 @@
   }
 }
 
+TEST_P(OatFileAssistantTest, Create) {
+  std::string dex_location = GetScratchDir() + "/OdexUpToDate.jar";
+  std::string odex_location = GetOdexDir() + "/OdexUpToDate.odex";
+  Copy(GetDexSrc1(), dex_location);
+  GenerateOdexForTest(dex_location, odex_location, CompilerFilter::kSpeed, "install");
+
+  auto scoped_maybe_without_runtime = ScopedMaybeWithoutRuntime();
+
+  std::string error_msg;
+  std::unique_ptr<OatFileAssistant> oat_file_assistant =
+      OatFileAssistant::Create(dex_location,
+                               GetInstructionSetString(kRuntimeISA),
+                               default_context_->EncodeContextForDex2oat(/*base_dir=*/""),
+                               /*load_executable=*/false,
+                               /*only_load_trusted_executable=*/true,
+                               MaybeCreateRuntimeOptions(),
+                               &error_msg);
+  ASSERT_NE(oat_file_assistant, nullptr);
+
+  // Verify that the created instance is usable.
+  VerifyOptimizationStatus(dex_location, default_context_.get(), "speed", "install", "up-to-date");
+}
+
 TEST_P(OatFileAssistantTest, ErrorOnInvalidIsaString) {
   std::string dex_location = GetScratchDir() + "/OdexUpToDate.jar";
   std::string odex_location = GetOdexDir() + "/OdexUpToDate.odex";
@@ -1804,19 +1808,15 @@
 
   auto scoped_maybe_without_runtime = ScopedMaybeWithoutRuntime();
 
-  std::string ignored_compilation_filter;
-  std::string ignored_compilation_reason;
-  std::string ignored_odex_location;
   std::string error_msg;
-  EXPECT_FALSE(OatFileAssistant::GetOptimizationStatus(
-      dex_location,
-      /*isa_str=*/"foo",
-      default_context_->EncodeContextForDex2oat(/*base_dir=*/""),
-      MaybeCreateRuntimeOptions(),
-      &ignored_compilation_filter,
-      &ignored_compilation_reason,
-      &ignored_odex_location,
-      &error_msg));
+  EXPECT_EQ(OatFileAssistant::Create(dex_location,
+                                     /*isa_str=*/"foo",
+                                     default_context_->EncodeContextForDex2oat(/*base_dir=*/""),
+                                     /*load_executable=*/false,
+                                     /*only_load_trusted_executable=*/true,
+                                     MaybeCreateRuntimeOptions(),
+                                     &error_msg),
+            nullptr);
   EXPECT_EQ(error_msg, "Instruction set 'foo' is invalid");
 }
 
@@ -1828,18 +1828,15 @@
 
   auto scoped_maybe_without_runtime = ScopedMaybeWithoutRuntime();
 
-  std::string ignored_compilation_filter;
-  std::string ignored_compilation_reason;
-  std::string ignored_odex_location;
   std::string error_msg;
-  EXPECT_FALSE(OatFileAssistant::GetOptimizationStatus(dex_location,
-                                                       GetInstructionSetString(kRuntimeISA),
-                                                       /*context_str=*/"foo",
-                                                       MaybeCreateRuntimeOptions(),
-                                                       &ignored_compilation_filter,
-                                                       &ignored_compilation_reason,
-                                                       &ignored_odex_location,
-                                                       &error_msg));
+  EXPECT_EQ(OatFileAssistant::Create(dex_location,
+                                     GetInstructionSetString(kRuntimeISA),
+                                     /*context_str=*/"foo",
+                                     /*load_executable=*/false,
+                                     /*only_load_trusted_executable=*/true,
+                                     MaybeCreateRuntimeOptions(),
+                                     &error_msg),
+            nullptr);
   EXPECT_EQ(error_msg, "Class loader context 'foo' is invalid");
 }
 
@@ -1856,19 +1853,15 @@
 
   auto scoped_maybe_without_runtime = ScopedMaybeWithoutRuntime();
 
-  std::string ignored_compilation_filter;
-  std::string ignored_compilation_reason;
-  std::string ignored_odex_location;
   std::string error_msg;
-  EXPECT_FALSE(
-      OatFileAssistant::GetOptimizationStatus(dex_location,
-                                              GetInstructionSetString(kRuntimeISA),
-                                              /*context_str=*/"PCL[" + context_location + "]",
-                                              MaybeCreateRuntimeOptions(),
-                                              &ignored_compilation_filter,
-                                              &ignored_compilation_reason,
-                                              &ignored_odex_location,
-                                              &error_msg));
+  EXPECT_EQ(OatFileAssistant::Create(dex_location,
+                                     GetInstructionSetString(kRuntimeISA),
+                                     /*context_str=*/"PCL[" + context_location + "]",
+                                     /*load_executable=*/false,
+                                     /*only_load_trusted_executable=*/true,
+                                     MaybeCreateRuntimeOptions(),
+                                     &error_msg),
+            nullptr);
   EXPECT_EQ(error_msg,
             "Failed to load class loader context files for '" + dex_location +
                 "' with context 'PCL[" + context_location + "]'");
diff --git a/test/utils/regen-test-files b/test/utils/regen-test-files
index fc7c360..481b9ba 100755
--- a/test/utils/regen-test-files
+++ b/test/utils/regen-test-files
@@ -217,9 +217,7 @@
 # ART gtests that do not need root access to the device.
 art_gtest_user_module_names = [
     "art_libnativebridge_cts_tests",
-    # TODO(b/235464166): art_artd_tests doesn't work on master-art because of
-    # the dependency on libbinder_ndk.
-    # "art_standalone_artd_tests",
+    "art_standalone_artd_tests",
     "art_standalone_cmdline_tests",
     "art_standalone_compiler_tests",
     "art_standalone_dex2oat_tests",