Update on-device AOT compilation to cover all cases.

After the change, odrefresh can properly determine whether to do
compilation when:
1. There is a mainline update (either a normal one or a samegrade one) that updates the ART module.
2. There is a mainline update (either a normal one or a samegrade one) that updates a module other than ART.
3. There is an OTA that updates a boot classpath jar.
4. There is an OTA that updates a system server jar.
5. There is no change since the last run.

Test: manual - 1. Install a mainline module without changing its
contents.
    2. Reboot the device.
    3. See if system_server components are recompiled.
Test: manual - 1. Do nothing and reboot the device.
    2. See if nothing is recompiled.
Test: atest odsign_e2e_tests
Test: atest art_odrefresh_tests
Bug: 189467174

Change-Id: I43dea5380fcd221d5d4e34753d64d46be4dbc27e
diff --git a/odrefresh/CacheInfo.xsd b/odrefresh/CacheInfo.xsd
index 485c6b6..44f733f 100644
--- a/odrefresh/CacheInfo.xsd
+++ b/odrefresh/CacheInfo.xsd
@@ -25,40 +25,36 @@
   <xs:element name="cacheInfo">
     <xs:complexType>
     <xs:sequence>
-      <xs:element name="artModuleInfo" minOccurs="1" maxOccurs="1" type="t:artModuleInfo" />
-      <xs:element name="bootClasspath" type="t:bootClasspath" />
-      <xs:element name="dex2oatBootClasspath" type="t:dex2oatBootClasspath" />
-      <xs:element name="systemServerClasspath" type="t:systemServerClasspath" />
+      <xs:element name="artModuleInfo" minOccurs="1" maxOccurs="1" type="t:moduleInfo" />
+      <xs:element name="moduleInfoList" minOccurs="1" maxOccurs="1" type="t:moduleInfoList" />
+      <xs:element name="bootClasspath" minOccurs="1" maxOccurs="1" type="t:classpath" />
+      <xs:element name="dex2oatBootClasspath" minOccurs="1" maxOccurs="1" type="t:classpath" />
+      <xs:element name="systemServerClasspath" minOccurs="1" maxOccurs="1" type="t:classpath" />
     </xs:sequence>
     </xs:complexType>
   </xs:element>
 
+  <!-- List of modules. -->
+  <xs:complexType name="moduleInfoList">
+    <xs:sequence>
+      <xs:element name="moduleInfo" type="t:moduleInfo" />
+    </xs:sequence>
+  </xs:complexType>
+
   <!-- Data type representing the provenance of the AOT artifacts in the cache. -->
-  <xs:complexType name="artModuleInfo">
-    <!-- Module versionCode for the active ART APEX from `/apex/apex-info-list.xml`. -->
+  <xs:complexType name="moduleInfo">
+    <!-- Module name for the active APEX from `/apex/apex-info-list.xml`. -->
+    <xs:attribute name="name" type="xs:string" use="required" />
+    <!-- Module versionCode for the active APEX from `/apex/apex-info-list.xml`. -->
     <xs:attribute name="versionCode" type="xs:long" use="required" />
-    <!-- Module versionName for the active ART APEX from `/apex/apex-info-list.xml`. -->
+    <!-- Module versionName for the active APEX from `/apex/apex-info-list.xml`. -->
     <xs:attribute name="versionName" type="xs:string" use="required" />
-    <!-- Module lastUpdateMillis for the active ART APEX from `/apex/apex-info-list.xml`. -->
+    <!-- Module lastUpdateMillis for the active APEX from `/apex/apex-info-list.xml`. -->
     <xs:attribute name="lastUpdateMillis" type="xs:long" use="required" />
   </xs:complexType>
 
-  <!-- Components of the `BOOTCLASSPATH`. -->
-  <xs:complexType name="bootClasspath">
-    <xs:sequence>
-      <xs:element name="component" type="t:component" />
-    </xs:sequence>
-  </xs:complexType>
-
-  <!-- Components of the `DEX2OATBOOTCLASSPATH`. -->
-  <xs:complexType name="dex2oatBootClasspath">
-    <xs:sequence>
-      <xs:element name="component" type="t:component" />
-    </xs:sequence>
-  </xs:complexType>
-
-  <!-- Components of the `SYSTEMSERVERCLASSPATH`. -->
-  <xs:complexType name="systemServerClasspath">
+  <!-- Components of a classpath. -->
+  <xs:complexType name="classpath">
     <xs:sequence>
       <xs:element name="component" type="t:component" />
     </xs:sequence>
diff --git a/odrefresh/odrefresh.cc b/odrefresh/odrefresh.cc
index bf76010..5bcb904 100644
--- a/odrefresh/odrefresh.cc
+++ b/odrefresh/odrefresh.cc
@@ -35,6 +35,7 @@
 #include <initializer_list>
 #include <iosfwd>
 #include <iostream>
+#include <iterator>
 #include <memory>
 #include <optional>
 #include <ostream>
@@ -42,6 +43,7 @@
 #include <string>
 #include <string_view>
 #include <type_traits>
+#include <unordered_map>
 #include <utility>
 #include <vector>
 
@@ -65,14 +67,13 @@
 #include "dexoptanalyzer.h"
 #include "exec_utils.h"
 #include "log/log.h"
-#include "palette/palette.h"
-#include "palette/palette_types.h"
-
 #include "odr_artifacts.h"
 #include "odr_compilation_log.h"
 #include "odr_config.h"
 #include "odr_fs_utils.h"
 #include "odr_metrics.h"
+#include "palette/palette.h"
+#include "palette/palette_types.h"
 
 namespace art {
 namespace odrefresh {
@@ -285,19 +286,29 @@
     return std::max(GetExecutionTimeRemaining(), kMaxChildProcessSeconds);
   }
 
-  // Gets the `ApexInfo` associated with the currently active ART APEX.
-  std::optional<apex::ApexInfo> GetArtApexInfo() const {
-    auto info_list = apex::readApexInfoList(config_.GetApexInfoListFile().c_str());
+  // Gets the `ApexInfo` for active APEXes.
+  std::optional<std::vector<apex::ApexInfo>> GetApexInfoList() const {
+    std::optional<apex::ApexInfoList> info_list =
+        apex::readApexInfoList(config_.GetApexInfoListFile().c_str());
     if (!info_list.has_value()) {
-      return {};
+      return std::nullopt;
     }
 
-    for (const apex::ApexInfo& info : info_list->getApexInfo()) {
-      if (info.getIsActive() && info.getModuleName() == "com.android.art") {
-        return info;
-      }
-    }
-    return {};
+    std::vector<apex::ApexInfo> filtered_info_list;
+    std::copy_if(info_list->getApexInfo().begin(),
+                 info_list->getApexInfo().end(),
+                 std::back_inserter(filtered_info_list),
+                 [](const apex::ApexInfo& info) { return info.getIsActive(); });
+    return filtered_info_list;
+  }
+
+  // Gets the `ApexInfo` associated with the currently active ART APEX.
+  static std::optional<apex::ApexInfo> GetArtApexInfo(
+      const std::vector<apex::ApexInfo>& info_list) {
+    auto it = std::find_if(info_list.begin(), info_list.end(), [](const apex::ApexInfo& info) {
+      return info.getModuleName() == "com.android.art";
+    });
+    return it != info_list.end() ? std::make_optional(*it) : std::nullopt;
   }
 
   // Reads the ART APEX cache information (if any) found in `kOdrefreshArtifactDirectory`.
@@ -319,15 +330,21 @@
       return;
     }
 
-    std::optional<art_apex::ArtModuleInfo> art_module_info = GenerateArtModuleInfo();
-    if (!art_module_info.has_value()) {
-      LOG(ERROR) << "Unable to generate cache provenance";
+    std::optional<std::vector<apex::ApexInfo>> apex_info_list = GetApexInfoList();
+    if (!apex_info_list.has_value()) {
+      LOG(ERROR) << "Could not update " << QuotePath(cache_info_filename_) << " : no APEX info";
       return;
     }
 
-    // There can be only one CacheProvence in the XML file, but `xsdc` does not have
-    // minOccurs/maxOccurs in the xsd schema.
-    const std::vector<art_apex::ArtModuleInfo> art_module_infos { art_module_info.value() };
+    std::optional<apex::ApexInfo> art_apex_info = GetArtApexInfo(apex_info_list.value());
+    if (!art_apex_info.has_value()) {
+      LOG(ERROR) << "Could not update " << QuotePath(cache_info_filename_) << " : no ART APEX info";
+      return;
+    }
+
+    art_apex::ModuleInfo art_module_info = GenerateModuleInfo(art_apex_info.value());
+    std::vector<art_apex::ModuleInfo> module_info_list =
+        GenerateModuleInfoList(apex_info_list.value());
 
     std::optional<std::vector<art_apex::Component>> bcp_components =
         GenerateBootClasspathComponents();
@@ -351,26 +368,36 @@
     }
 
     std::ofstream out(cache_info_filename_.c_str());
-    art_apex::CacheInfo info{art_module_infos,
-                             {{art_apex::BootClasspath{bcp_components.value()}}},
-                             {{art_apex::Dex2oatBootClasspath{bcp_compilable_components.value()}}},
-                             {{art_apex::SystemServerClasspath{system_server_components.value()}}}};
+    art_apex::CacheInfo info({art_module_info},
+                             {art_apex::ModuleInfoList(module_info_list)},
+                             {art_apex::Classpath(bcp_components.value())},
+                             {art_apex::Classpath(bcp_compilable_components.value())},
+                             {art_apex::Classpath(system_server_components.value())});
 
     art_apex::write(out, info);
   }
 
-  // Returns cache provenance information based on the current ART APEX version and filesystem
+  // Returns cache provenance information based on the current APEX version and filesystem
   // information.
-  std::optional<art_apex::ArtModuleInfo> GenerateArtModuleInfo() const {
-    auto info = GetArtApexInfo();
-    if (!info.has_value()) {
-      LOG(ERROR) << "Could not update " << QuotePath(cache_info_filename_) << " : no ART Apex info";
-      return {};
-    }
+  static art_apex::ModuleInfo GenerateModuleInfo(const apex::ApexInfo& apex_info) {
     // The lastUpdateMillis is an addition to ApexInfoList.xsd to support samegrade installs.
-    int64_t last_update_millis = info->hasLastUpdateMillis() ? info->getLastUpdateMillis() : 0;
-    return art_apex::ArtModuleInfo{
-        info->getVersionCode(), info->getVersionName(), last_update_millis};
+    int64_t last_update_millis =
+        apex_info.hasLastUpdateMillis() ? apex_info.getLastUpdateMillis() : 0;
+    return art_apex::ModuleInfo {apex_info.getModuleName(),
+                                 apex_info.getVersionCode(),
+                                 apex_info.getVersionName(),
+                                 last_update_millis};
+  }
+
+  // Returns cache provenance information for all APEXes.
+  static std::vector<art_apex::ModuleInfo> GenerateModuleInfoList(
+      const std::vector<apex::ApexInfo>& apex_info_list) {
+    std::vector<art_apex::ModuleInfo> module_info_list;
+    std::transform(apex_info_list.begin(),
+                   apex_info_list.end(),
+                   std::back_inserter(module_info_list),
+                   GenerateModuleInfo);
+    return module_info_list;
   }
 
   bool CheckComponents(const std::vector<art_apex::Component>& expected_components,
@@ -392,11 +419,11 @@
         return false;
       }
       if (expected.getSize() != actual.getSize()) {
-        *error_msg = android::base::StringPrintf("Component %zu size differs (%" PRIu64
-                                                 " != %" PRIu64 ")",
-                                                 i,
-                                                 expected.getSize(),
-                                                 actual.getSize());
+        *error_msg =
+            android::base::StringPrintf("Component %zu size differs (%" PRIu64 " != %" PRIu64 ")",
+                                        i,
+                                        expected.getSize(),
+                                        actual.getSize());
         return false;
       }
       if (expected.getChecksums() != actual.getChecksums()) {
@@ -459,9 +486,13 @@
   }
 
   // Checks whether a group of artifacts exists. Returns true if all are present, false otherwise.
-  static bool ArtifactsExist(const OdrArtifacts& artifacts, /*out*/ std::string* error_msg) {
-    const auto paths = {
-        artifacts.ImagePath().c_str(), artifacts.OatPath().c_str(), artifacts.VdexPath().c_str()};
+  static bool ArtifactsExist(const OdrArtifacts& artifacts,
+                             bool check_art_file,
+                             /*out*/ std::string* error_msg) {
+    std::vector<const char*> paths{artifacts.OatPath().c_str(), artifacts.VdexPath().c_str()};
+    if (check_art_file) {
+      paths.push_back(artifacts.ImagePath().c_str());
+    }
     for (const char* path : paths) {
       if (!OS::FileExists(path)) {
         if (errno == EACCES) {
@@ -474,112 +505,106 @@
     return true;
   }
 
-  // Checks whether all boot extension artifacts are present on /data. Returns true if all are
-  // present, false otherwise.
-  WARN_UNUSED bool BootExtensionArtifactsExistOnData(const InstructionSet isa,
-                                                     /*out*/ std::string* error_msg) const {
-    const std::string apexdata_image_location = GetBootImageExtensionImagePath(isa);
+  // Checks whether all boot extension artifacts are present. Returns true if all are present, false
+  // otherwise.
+  WARN_UNUSED bool BootExtensionArtifactsExist(bool on_system,
+                                               const InstructionSet isa,
+                                               /*out*/ std::string* error_msg) const {
+    const std::string apexdata_image_location = GetBootImageExtensionImagePath(on_system, isa);
     const OdrArtifacts artifacts = OdrArtifacts::ForBootImageExtension(apexdata_image_location);
-    return ArtifactsExist(artifacts, error_msg);
+    return ArtifactsExist(artifacts, /*check_art_file=*/true, error_msg);
   }
 
-  // Checks whether all system_server artifacts are present on /data. The artifacts are checked in
-  // their order of compilation. Returns true if all are present, false otherwise.
-  WARN_UNUSED bool SystemServerArtifactsExistOnData(/*out*/ std::string* error_msg) const {
+  // Checks whether all system_server artifacts are present. The artifacts are checked in their
+  // order of compilation. Returns true if all are present, false otherwise.
+  WARN_UNUSED bool SystemServerArtifactsExist(bool on_system,
+                                              /*out*/ std::string* error_msg) const {
     for (const std::string& jar_path : systemserver_compilable_jars_) {
-      const std::string image_location = GetSystemServerImagePath(/*on_system=*/false, jar_path);
+      // Temporarily skip checking APEX jar artifacts on system to prevent compilation on the first
+      // boot. Currently, APEX jar artifacts can never be found on system because we don't preopt
+      // them.
+      // TODO(b/194150908): Preopt APEX jars for system server and put the artifacts on /system.
+      if (on_system && StartsWith(jar_path, "/apex")) {
+        continue;
+      }
+      const std::string image_location = GetSystemServerImagePath(on_system, jar_path);
       const OdrArtifacts artifacts = OdrArtifacts::ForSystemServer(image_location);
-      if (!ArtifactsExist(artifacts, error_msg)) {
+      // .art files are optional and are not generated for all jars by the build system.
+      const bool check_art_file = !on_system;
+      if (!ArtifactsExist(artifacts, check_art_file, error_msg)) {
         return false;
       }
     }
     return true;
   }
 
-  WARN_UNUSED ExitCode CheckArtifactsAreUpToDate(OdrMetrics& metrics) {
-    metrics.SetStage(OdrMetrics::Stage::kCheck);
+  WARN_UNUSED bool CheckBootExtensionArtifactsAreUpToDate(
+      OdrMetrics& metrics,
+      const InstructionSet isa,
+      const apex::ApexInfo& art_apex_info,
+      const std::optional<art_apex::CacheInfo>& cache_info,
+      /*out*/ bool* cleanup_required) {
+    std::string error_msg;
 
-    // Clean-up helper used to simplify clean-ups and handling failures there.
-    auto cleanup_return = [this](ExitCode exit_code) {
-      return RemoveArtifactsDirectory() ? exit_code : ExitCode::kCleanupFailed;
-    };
+    if (art_apex_info.getIsFactory()) {
+      LOG(INFO) << "Factory ART APEX mounted.";
 
-    const auto apex_info = GetArtApexInfo();
-    if (!apex_info.has_value()) {
-      // This should never happen, further up-to-date checks are not possible if it does.
-      LOG(ERROR) << "Could not get ART APEX info.";
-      metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
-      return cleanup_return(ExitCode::kCompilationRequired);
+      // ART is not updated, so we can use the artifacts on /system. Check if they exist.
+      if (BootExtensionArtifactsExist(/*on_system=*/true, isa, &error_msg)) {
+        // We don't need the artifacts on /data since we can use those on /system.
+        *cleanup_required = true;
+        return true;
+      }
+
+      LOG(INFO) << "Incomplete boot extension artifacts on /system. " << error_msg;
+      LOG(INFO) << "Checking cache.";
     }
 
-    // Generate current module info for the current ART APEX.
-    const auto current_info = GenerateArtModuleInfo();
-    if (!current_info.has_value()) {
-      // This should never happen, further up-to-date checks are not possible if it does.
-      LOG(ERROR) << "Failed to generate cache provenance.";
-      metrics.SetTrigger(OdrMetrics::Trigger::kUnknown);
-      return cleanup_return(ExitCode::kCompilationRequired);
-    }
-
-    // Record ART APEX version for metrics reporting.
-    metrics.SetArtApexVersion(current_info->getVersionCode());
-
-    // Record ART APEX last update milliseconds (used in compilation log).
-    metrics.SetArtApexLastUpdateMillis(current_info->getLastUpdateMillis());
-
-    if (apex_info->getIsFactory()) {
-      // Remove any artifacts on /data as they are not necessary and no compilation is necessary.
-      LOG(INFO) << "Factory APEX mounted.";
-      return cleanup_return(ExitCode::kOkay);
-    }
-
-    if (!OS::FileExists(cache_info_filename_.c_str())) {
-      // If the cache info file does not exist, assume compilation is required because the
-      // file is missing and because the current ART APEX is not factory installed.
-      PLOG(ERROR) << "No prior cache-info file: " << QuotePath(cache_info_filename_);
-      metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
-      return cleanup_return(ExitCode::kCompilationRequired);
-    }
-
-    // Get and parse the ART APEX cache info file.
-    std::optional<art_apex::CacheInfo> cache_info = ReadCacheInfo();
     if (!cache_info.has_value()) {
-      // This should never happen, further up-to-date checks are not possible if it does.
-      PLOG(ERROR) << "Failed to read cache-info file: " << QuotePath(cache_info_filename_);
-      metrics.SetTrigger(OdrMetrics::Trigger::kUnknown);
-      return cleanup_return(ExitCode::kCompilationRequired);
+      // If the cache info file does not exist, it means on-device compilation has not been done
+      // before.
+      PLOG(INFO) << "No prior cache-info file: " << QuotePath(cache_info_filename_);
+      metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
+      *cleanup_required = true;
+      return false;
     }
 
     // Check whether the current cache ART module info differs from the current ART module info.
-    // Always check APEX version.
-    const auto cached_info = cache_info->getFirstArtModuleInfo();
+    const art_apex::ModuleInfo* cached_art_info = cache_info->getFirstArtModuleInfo();
 
-    if (cached_info->getVersionCode() != current_info->getVersionCode()) {
-      LOG(INFO) << "ART APEX version code mismatch ("
-                << cached_info->getVersionCode()
-                << " != " << current_info->getVersionCode() << ").";
+    if (cached_art_info == nullptr) {
+      LOG(INFO) << "Missing ART APEX info from cache-info.";
       metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
-      return cleanup_return(ExitCode::kCompilationRequired);
+      *cleanup_required = true;
+      return false;
     }
 
-    if (cached_info->getVersionName() != current_info->getVersionName()) {
-      LOG(INFO) << "ART APEX version name mismatch ("
-                << cached_info->getVersionName()
-                << " != " << current_info->getVersionName() << ").";
+    if (cached_art_info->getVersionCode() != art_apex_info.getVersionCode()) {
+      LOG(INFO) << "ART APEX version code mismatch (" << cached_art_info->getVersionCode()
+                << " != " << art_apex_info.getVersionCode() << ").";
       metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
-      return cleanup_return(ExitCode::kCompilationRequired);
+      *cleanup_required = true;
+      return false;
     }
 
-    // Check lastUpdateMillis for samegrade installs. If `cached_info` is missing lastUpdateMillis
-    // then it is not current with the schema used by this binary so treat it as a samegrade
-    // update. Otherwise check whether the lastUpdateMillis changed.
-    if (!cached_info->hasLastUpdateMillis() ||
-        cached_info->getLastUpdateMillis() != current_info->getLastUpdateMillis()) {
-      LOG(INFO) << "ART APEX last update time mismatch ("
-                << cached_info->getLastUpdateMillis()
-                << " != " << current_info->getLastUpdateMillis() << ").";
+    if (cached_art_info->getVersionName() != art_apex_info.getVersionName()) {
+      LOG(INFO) << "ART APEX version name mismatch (" << cached_art_info->getVersionName()
+                << " != " << art_apex_info.getVersionName() << ").";
       metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
-      return cleanup_return(ExitCode::kCompilationRequired);
+      *cleanup_required = true;
+      return false;
+    }
+
+    // Check lastUpdateMillis for samegrade installs. If `cached_art_info` is missing
+    // lastUpdateMillis then it is not current with the schema used by this binary so treat it as a
+    // samegrade update. Otherwise check whether the lastUpdateMillis changed.
+    if (!cached_art_info->hasLastUpdateMillis() ||
+        cached_art_info->getLastUpdateMillis() != art_apex_info.getLastUpdateMillis()) {
+      LOG(INFO) << "ART APEX last update time mismatch (" << cached_art_info->getLastUpdateMillis()
+                << " != " << art_apex_info.getLastUpdateMillis() << ").";
+      metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
+      *cleanup_required = true;
+      return false;
     }
 
     // Check boot class components.
@@ -597,17 +622,124 @@
          !cache_info->getFirstDex2oatBootClasspath()->hasComponent())) {
       LOG(INFO) << "Missing Dex2oatBootClasspath components.";
       metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
-      return cleanup_return(ExitCode::kCompilationRequired);
+      *cleanup_required = true;
+      return false;
     }
 
-    std::string error_msg;
     const std::vector<art_apex::Component>& bcp_compilable_components =
         cache_info->getFirstDex2oatBootClasspath()->getComponent();
     if (!CheckComponents(
             expected_bcp_compilable_components, bcp_compilable_components, &error_msg)) {
       LOG(INFO) << "Dex2OatClasspath components mismatch: " << error_msg;
       metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
-      return cleanup_return(ExitCode::kCompilationRequired);
+      *cleanup_required = true;
+      return false;
+    }
+
+    // Cache info looks good, check all compilation artifacts exist.
+    if (!BootExtensionArtifactsExist(/*on_system=*/false, isa, &error_msg)) {
+      LOG(INFO) << "Incomplete boot extension artifacts. " << error_msg;
+      metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
+      *cleanup_required = true;
+      return false;
+    }
+
+    *cleanup_required = false;
+    return true;
+  }
+
+  WARN_UNUSED bool CheckSystemServerArtifactsAreUpToDate(
+      OdrMetrics& metrics,
+      const std::vector<apex::ApexInfo>& apex_info_list,
+      const std::optional<art_apex::CacheInfo>& cache_info,
+      /*out*/ bool* cleanup_required) {
+    std::string error_msg;
+
+    if (std::all_of(apex_info_list.begin(),
+                    apex_info_list.end(),
+                    [](const apex::ApexInfo& apex_info) { return apex_info.getIsFactory(); })) {
+      LOG(INFO) << "Factory APEXes mounted.";
+
+      // APEXes are not updated, so we can use the artifacts on /system. Check if they exist.
+      if (SystemServerArtifactsExist(/*on_system=*/true, &error_msg)) {
+        // We don't need the artifacts on /data since we can use those on /system.
+        *cleanup_required = true;
+        return true;
+      }
+
+      LOG(INFO) << "Incomplete system server artifacts on /system. " << error_msg;
+      LOG(INFO) << "Checking cache.";
+    }
+
+    if (!cache_info.has_value()) {
+      // If the cache info file does not exist, it means on-device compilation has not been done
+      // before.
+      PLOG(INFO) << "No prior cache-info file: " << QuotePath(cache_info_filename_);
+      metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
+      *cleanup_required = true;
+      return false;
+    }
+
+    // Check whether the current cached module info differs from the current module info.
+    const art_apex::ModuleInfoList* cached_module_info_list = cache_info->getFirstModuleInfoList();
+
+    if (cached_module_info_list == nullptr) {
+      LOG(INFO) << "Missing APEX info list from cache-info.";
+      metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
+      *cleanup_required = true;
+      return false;
+    }
+
+    std::unordered_map<std::string, const art_apex::ModuleInfo*> cached_module_info_map;
+    for (const art_apex::ModuleInfo& module_info : cached_module_info_list->getModuleInfo()) {
+      if (!module_info.hasName()) {
+        LOG(INFO) << "Unexpected module info from cache-info. Missing module name.";
+        metrics.SetTrigger(OdrMetrics::Trigger::kUnknown);
+        *cleanup_required = true;
+        return false;
+      }
+      cached_module_info_map[module_info.getName()] = &module_info;
+    }
+
+    for (const apex::ApexInfo& current_apex_info : apex_info_list) {
+      auto it = cached_module_info_map.find(current_apex_info.getModuleName());
+      if (it == cached_module_info_map.end()) {
+        LOG(INFO) << "Missing APEX info from cache-info (" << current_apex_info.getModuleName()
+                  << ").";
+        metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
+        *cleanup_required = true;
+        return false;
+      }
+
+      const art_apex::ModuleInfo* cached_module_info = it->second;
+
+      if (cached_module_info->getVersionCode() != current_apex_info.getVersionCode()) {
+        LOG(INFO) << "APEX (" << current_apex_info.getModuleName() << ") version code mismatch ("
+                  << cached_module_info->getVersionCode()
+                  << " != " << current_apex_info.getVersionCode() << ").";
+        metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
+        *cleanup_required = true;
+        return false;
+      }
+
+      if (cached_module_info->getVersionName() != current_apex_info.getVersionName()) {
+        LOG(INFO) << "APEX (" << current_apex_info.getModuleName() << ") version name mismatch ("
+                  << cached_module_info->getVersionName()
+                  << " != " << current_apex_info.getVersionName() << ").";
+        metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
+        *cleanup_required = true;
+        return false;
+      }
+
+      if (!cached_module_info->hasLastUpdateMillis() ||
+          cached_module_info->getLastUpdateMillis() != current_apex_info.getLastUpdateMillis()) {
+        LOG(INFO) << "APEX (" << current_apex_info.getModuleName()
+                  << ") last update time mismatch (" << cached_module_info->getLastUpdateMillis()
+                  << " != " << current_apex_info.getLastUpdateMillis() << ").";
+        metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
+        *cleanup_required = true;
+        return false;
+      }
     }
 
     // Check system server components.
@@ -619,10 +751,6 @@
     //
     // The system_server components may change unexpectedly, for example an OTA could update
     // services.jar.
-    auto cleanup_system_server_return = [this](ExitCode exit_code) {
-      return RemoveSystemServerArtifactsFromData() ? exit_code : ExitCode::kCleanupFailed;
-    };
-
     const std::vector<art_apex::Component> expected_system_server_components =
         GenerateSystemServerComponents();
     if (expected_system_server_components.size() != 0 &&
@@ -630,7 +758,8 @@
          !cache_info->getFirstSystemServerClasspath()->hasComponent())) {
       LOG(INFO) << "Missing SystemServerClasspath components.";
       metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
-      return cleanup_system_server_return(ExitCode::kCompilationRequired);
+      *cleanup_required = true;
+      return false;
     }
 
     const std::vector<art_apex::Component>& system_server_components =
@@ -638,7 +767,8 @@
     if (!CheckComponents(expected_system_server_components, system_server_components, &error_msg)) {
       LOG(INFO) << "SystemServerClasspath components mismatch: " << error_msg;
       metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
-      return cleanup_system_server_return(ExitCode::kCompilationRequired);
+      *cleanup_required = true;
+      return false;
     }
 
     const std::vector<art_apex::Component> expected_bcp_components =
@@ -647,7 +777,8 @@
         (!cache_info->hasBootClasspath() || !cache_info->getFirstBootClasspath()->hasComponent())) {
       LOG(INFO) << "Missing BootClasspath components.";
       metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
-      return cleanup_system_server_return(ExitCode::kCompilationRequired);
+      *cleanup_required = true;
+      return false;
     }
 
     const std::vector<art_apex::Component>& bcp_components =
@@ -657,33 +788,110 @@
       metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
       // Boot classpath components can be dependencies of system_server components, so system_server
       // components need to be recompiled if boot classpath components are changed.
-      return cleanup_system_server_return(ExitCode::kCompilationRequired);
+      *cleanup_required = true;
+      return false;
     }
 
-    // Cache info looks good, check all compilation artifacts exist.
-    auto cleanup_boot_extensions_return = [this](ExitCode exit_code, InstructionSet isa) {
-      return RemoveBootExtensionArtifactsFromData(isa) ? exit_code : ExitCode::kCleanupFailed;
-    };
-
-    for (const InstructionSet isa : config_.GetBootExtensionIsas()) {
-      if (!BootExtensionArtifactsExistOnData(isa, &error_msg)) {
-        LOG(INFO) << "Incomplete boot extension artifacts. " << error_msg;
-        metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
-        return cleanup_boot_extensions_return(ExitCode::kCompilationRequired, isa);
-      }
-    }
-
-    if (!SystemServerArtifactsExistOnData(&error_msg)) {
+    if (!SystemServerArtifactsExist(/*on_system=*/false, &error_msg)) {
       LOG(INFO) << "Incomplete system_server artifacts. " << error_msg;
       // No clean-up is required here: we have boot extension artifacts. The method
       // `SystemServerArtifactsExistOnData()` checks in compilation order so it is possible some of
       // the artifacts are here. We likely ran out of space compiling the system_server artifacts.
       // Any artifacts present are usable.
       metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
-      return ExitCode::kCompilationRequired;
+      *cleanup_required = false;
+      return false;
     }
 
-    return ExitCode::kOkay;
+    *cleanup_required = false;
+    return true;
+  }
+
+  // Returns the exit code, a list of ISAs that boot extensions should be compiled for, and a
+  // boolean indicating whether the system server should be compiled.
+  WARN_UNUSED ExitCode
+  CheckArtifactsAreUpToDate(OdrMetrics& metrics,
+                            /*out*/ std::vector<InstructionSet>* compile_boot_extensions,
+                            /*out*/ bool* compile_system_server) {
+    metrics.SetStage(OdrMetrics::Stage::kCheck);
+
+    // Clean-up helper used to simplify clean-ups and handling failures there.
+    auto cleanup_and_compile_all = [&, this]() {
+      *compile_boot_extensions = config_.GetBootExtensionIsas();
+      *compile_system_server = true;
+      return RemoveArtifactsDirectory() ? ExitCode::kCompilationRequired : ExitCode::kCleanupFailed;
+    };
+
+    std::optional<std::vector<apex::ApexInfo>> apex_info_list = GetApexInfoList();
+    if (!apex_info_list.has_value()) {
+      // This should never happen, further up-to-date checks are not possible if it does.
+      LOG(ERROR) << "Could not get APEX info.";
+      metrics.SetTrigger(OdrMetrics::Trigger::kUnknown);
+      return cleanup_and_compile_all();
+    }
+
+    std::optional<apex::ApexInfo> art_apex_info = GetArtApexInfo(apex_info_list.value());
+    if (!art_apex_info.has_value()) {
+      // This should never happen, further up-to-date checks are not possible if it does.
+      LOG(ERROR) << "Could not get ART APEX info.";
+      metrics.SetTrigger(OdrMetrics::Trigger::kUnknown);
+      return cleanup_and_compile_all();
+    }
+
+    // Record ART APEX version for metrics reporting.
+    metrics.SetArtApexVersion(art_apex_info->getVersionCode());
+
+    // Record ART APEX last update milliseconds (used in compilation log).
+    metrics.SetArtApexLastUpdateMillis(art_apex_info->getLastUpdateMillis());
+
+    std::optional<art_apex::CacheInfo> cache_info = ReadCacheInfo();
+    if (!cache_info.has_value() && OS::FileExists(cache_info_filename_.c_str())) {
+      // This should not happen unless odrefresh is updated to a new version that is not
+      // compatible with an old cache-info file. Further up-to-date checks are not possible if it
+      // does.
+      PLOG(ERROR) << "Failed to parse cache-info file: " << QuotePath(cache_info_filename_);
+      metrics.SetTrigger(OdrMetrics::Trigger::kUnknown);
+      return cleanup_and_compile_all();
+    }
+
+    InstructionSet system_server_isa = config_.GetSystemServerIsa();
+    bool cleanup_required;
+
+    for (const InstructionSet isa : config_.GetBootExtensionIsas()) {
+      cleanup_required = false;
+      if (!CheckBootExtensionArtifactsAreUpToDate(
+              metrics, isa, art_apex_info.value(), cache_info, &cleanup_required)) {
+        compile_boot_extensions->push_back(isa);
+        // system_server artifacts are invalid without valid boot extension artifacts.
+        if (isa == system_server_isa) {
+          *compile_system_server = true;
+          if (!RemoveSystemServerArtifactsFromData()) {
+            return ExitCode::kCleanupFailed;
+          }
+        }
+      }
+      if (cleanup_required) {
+        if (!RemoveBootExtensionArtifactsFromData(isa)) {
+          return ExitCode::kCleanupFailed;
+        }
+      }
+    }
+
+    cleanup_required = false;
+    if (!*compile_system_server &&
+        !CheckSystemServerArtifactsAreUpToDate(
+            metrics, apex_info_list.value(), cache_info, &cleanup_required)) {
+      *compile_system_server = true;
+    }
+    if (cleanup_required) {
+      if (!RemoveSystemServerArtifactsFromData()) {
+        return ExitCode::kCleanupFailed;
+      }
+    }
+
+    return (!compile_boot_extensions->empty() || *compile_system_server)
+               ? ExitCode::kCompilationRequired
+               : ExitCode::kOkay;
   }
 
   static void AddDex2OatCommonOptions(/*inout*/ std::vector<std::string>* args) {
@@ -934,8 +1142,7 @@
 
     bool success = true;
     for (const std::string& jar_path : systemserver_compilable_jars_) {
-      const std::string image_location =
-          GetSystemServerImagePath(/*on_system=*/false, jar_path);
+      const std::string image_location = GetSystemServerImagePath(/*on_system=*/false, jar_path);
       const OdrArtifacts artifacts = OdrArtifacts::ForSystemServer(image_location);
       LOG(INFO) << "Removing system_server artifacts on /data for " << QuotePath(jar_path);
       success &= RemoveArtifacts(artifacts);
@@ -998,17 +1205,11 @@
       return true;
     }
 
-    bool success = true;
-    if (isa == config_.GetSystemServerIsa()) {
-      // system_server artifacts are invalid without boot extension artifacts.
-      success &= RemoveSystemServerArtifactsFromData();
-    }
-
-    const std::string apexdata_image_location = GetBootImageExtensionImagePath(isa);
+    const std::string apexdata_image_location =
+        GetBootImageExtensionImagePath(/*on_system=*/false, isa);
     LOG(INFO) << "Removing boot class path artifacts on /data for "
               << QuotePath(apexdata_image_location);
-    success &= RemoveArtifacts(OdrArtifacts::ForBootImageExtension(apexdata_image_location));
-    return success;
+    return RemoveArtifacts(OdrArtifacts::ForBootImageExtension(apexdata_image_location));
   }
 
   // Verify whether boot extension artifacts for `isa` are valid on system partition or in apexdata.
@@ -1029,13 +1230,13 @@
   //
   // Returns ExitCode::kOkay if artifacts are up-to-date, ExitCode::kCompilationRequired otherwise.
   //
-  // NB This is the main function used by the --check command-line option. When invoked with
-  // --compile, we only recompile the out-of-date artifacts, not all (see `Odrefresh::Compile`).
+  // NB This is the main function used by the --verify command-line option.
   WARN_UNUSED ExitCode VerifyArtifactsAreUpToDate() {
     ExitCode exit_code = ExitCode::kOkay;
     for (const InstructionSet isa : config_.GetBootExtensionIsas()) {
       if (!VerifyBootExtensionArtifactsAreUpToDate(isa)) {
-        if (!RemoveBootExtensionArtifactsFromData(isa)) {
+        // system_server artifacts are invalid without valid boot extension artifacts.
+        if (!RemoveSystemServerArtifactsFromData() || !RemoveBootExtensionArtifactsFromData(isa)) {
           return ExitCode::kCleanupFailed;
         }
         exit_code = ExitCode::kCompilationRequired;
@@ -1095,13 +1296,14 @@
     }
   }
 
-  std::string GetBootImageExtensionImagePath(const InstructionSet isa) const {
+  std::string GetBootImageExtensionImagePath(bool on_system, const InstructionSet isa) const {
     // Typically "/data/misc/apexdata/com.android.art/dalvik-cache/<isa>/boot-framework.art".
-    return GetSystemImageFilename(GetBootImageExtensionImage(/*on_system=*/false).c_str(), isa);
+    return GetSystemImageFilename(GetBootImageExtensionImage(on_system).c_str(), isa);
   }
 
   std::string GetSystemServerImagePath(bool on_system, const std::string& jar_path) const {
     if (on_system) {
+      // TODO(b/194150908): Define a path for "preopted" APEX artifacts.
       const std::string jar_name = android::base::Basename(jar_path);
       const std::string image_name = ReplaceFileExtension(jar_name, "art");
       const char* isa_str = GetInstructionSetString(config_.GetSystemServerIsa());
@@ -1181,7 +1383,7 @@
       return false;
     }
 
-    const std::string image_location = GetBootImageExtensionImagePath(isa);
+    const std::string image_location = GetBootImageExtensionImagePath(/*on_system=*/false, isa);
     const OdrArtifacts artifacts = OdrArtifacts::ForBootImageExtension(image_location);
     CHECK_EQ(GetApexDataOatFilename(boot_extension_compilable_jars_.front().c_str(), isa),
              artifacts.OatPath());
@@ -1342,7 +1544,8 @@
           return false;
         }
         std::unique_ptr<File> file(OS::OpenFileForReading(bcp_packages.c_str()));
-        args.emplace_back(android::base::StringPrintf("--updatable-bcp-packages-fd=%d", file->Fd()));
+        args.emplace_back(
+            android::base::StringPrintf("--updatable-bcp-packages-fd=%d", file->Fd()));
         readonly_files_raii.push_back(std::move(file));
       }
 
@@ -1453,14 +1656,11 @@
     return true;
   }
 
-  WARN_UNUSED ExitCode Compile(OdrMetrics& metrics, bool force_compile) const {
+  WARN_UNUSED ExitCode Compile(OdrMetrics& metrics,
+                               const std::vector<InstructionSet>& compile_boot_extensions,
+                               bool compile_system_server) const {
     const char* staging_dir = nullptr;
     metrics.SetStage(OdrMetrics::Stage::kPreparation);
-    // Clean-up existing files.
-    if (force_compile && !RemoveArtifactsDirectory()) {
-      metrics.SetStatus(OdrMetrics::Status::kIoError);
-      return ExitCode::kCleanupFailed;
-    }
 
     // Create staging area and assign label for generating compilation artifacts.
     if (PaletteCreateOdrefreshStagingDirectory(&staging_dir) != PALETTE_STATUS_OK) {
@@ -1478,36 +1678,28 @@
 
     const auto& bcp_instruction_sets = config_.GetBootExtensionIsas();
     DCHECK(!bcp_instruction_sets.empty() && bcp_instruction_sets.size() <= 2);
-    for (const InstructionSet isa : bcp_instruction_sets) {
+    for (const InstructionSet isa : compile_boot_extensions) {
       auto stage = (isa == bcp_instruction_sets.front()) ?
                        OdrMetrics::Stage::kPrimaryBootClasspath :
                        OdrMetrics::Stage::kSecondaryBootClasspath;
       metrics.SetStage(stage);
-      if (force_compile || !BootExtensionArtifactsExistOnData(isa, &error_msg)) {
-        // Remove artifacts we are about to generate. Ordinarily these are removed in the checking
-        // step, but this is not always run (e.g. during manual testing).
-        if (!RemoveBootExtensionArtifactsFromData(isa)) {
-            return ExitCode::kCleanupFailed;
-        }
+      if (!CheckCompilationSpace()) {
+        metrics.SetStatus(OdrMetrics::Status::kNoSpace);
+        // Return kOkay so odsign will keep and sign whatever we have been able to compile.
+        return ExitCode::kOkay;
+      }
 
-        if (!CheckCompilationSpace()) {
-          metrics.SetStatus(OdrMetrics::Status::kNoSpace);
-          // Return kOkay so odsign will keep and sign whatever we have been able to compile.
-          return ExitCode::kOkay;
+      if (!CompileBootExtensionArtifacts(
+              isa, staging_dir, metrics, &dex2oat_invocation_count, &error_msg)) {
+        LOG(ERROR) << "Compilation of BCP failed: " << error_msg;
+        if (!config_.GetDryRun() && !RemoveDirectory(staging_dir)) {
+          return ExitCode::kCleanupFailed;
         }
-
-        if (!CompileBootExtensionArtifacts(
-                isa, staging_dir, metrics, &dex2oat_invocation_count, &error_msg)) {
-          LOG(ERROR) << "Compilation of BCP failed: " << error_msg;
-          if (!config_.GetDryRun() && !RemoveDirectory(staging_dir)) {
-            return ExitCode::kCleanupFailed;
-          }
-          return ExitCode::kCompilationFailed;
-        }
+        return ExitCode::kCompilationFailed;
       }
     }
 
-    if (force_compile || !SystemServerArtifactsExistOnData(&error_msg)) {
+    if (compile_system_server) {
       metrics.SetStage(OdrMetrics::Stage::kSystemServerClasspath);
 
       if (!CheckCompilationSpace()) {
@@ -1654,11 +1846,15 @@
     OnDeviceRefresh odr(config);
     for (int i = 0; i < argc; ++i) {
       std::string_view action(argv[i]);
+      std::vector<InstructionSet> compile_boot_extensions;
+      bool compile_system_server;
       if (action == "--check") {
         // Fast determination of whether artifacts are up to date.
-        return odr.CheckArtifactsAreUpToDate(metrics);
+        return odr.CheckArtifactsAreUpToDate(
+            metrics, &compile_boot_extensions, &compile_system_server);
       } else if (action == "--compile") {
-        const ExitCode exit_code = odr.CheckArtifactsAreUpToDate(metrics);
+        const ExitCode exit_code = odr.CheckArtifactsAreUpToDate(
+            metrics, &compile_boot_extensions, &compile_system_server);
         if (exit_code != ExitCode::kCompilationRequired) {
           return exit_code;
         }
@@ -1668,14 +1864,22 @@
                                                   metrics.GetTrigger())) {
           return ExitCode::kOkay;
         }
-        ExitCode compile_result = odr.Compile(metrics, /*force_compile=*/false);
+        ExitCode compile_result =
+            odr.Compile(metrics, compile_boot_extensions, compile_system_server);
         compilation_log.Log(metrics.GetArtApexVersion(),
                             metrics.GetArtApexLastUpdateMillis(),
                             metrics.GetTrigger(),
                             compile_result);
         return compile_result;
       } else if (action == "--force-compile") {
-        return odr.Compile(metrics, /*force_compile=*/true);
+        // Clean-up existing files.
+        if (!odr.RemoveArtifactsDirectory()) {
+          metrics.SetStatus(OdrMetrics::Status::kIoError);
+          return ExitCode::kCleanupFailed;
+        }
+        return odr.Compile(metrics,
+                           /*compile_boot_extensions=*/config.GetBootExtensionIsas(),
+                           /*compile_system_server=*/true);
       } else if (action == "--verify") {
         // Slow determination of whether artifacts are up to date. These are too slow for checking
         // during boot (b/181689036).
diff --git a/odrefresh/schema/current.txt b/odrefresh/schema/current.txt
index 9a38400..4757c87 100644
--- a/odrefresh/schema/current.txt
+++ b/odrefresh/schema/current.txt
@@ -1,32 +1,24 @@
 // Signature format: 2.0
 package com.android.art {
 
-  public class ArtModuleInfo {
-    ctor public ArtModuleInfo();
-    method public long getLastUpdateMillis();
-    method public long getVersionCode();
-    method public String getVersionName();
-    method public void setLastUpdateMillis(long);
-    method public void setVersionCode(long);
-    method public void setVersionName(String);
-  }
-
-  public class BootClasspath {
-    ctor public BootClasspath();
-    method public com.android.art.Component getComponent();
-    method public void setComponent(com.android.art.Component);
-  }
-
   public class CacheInfo {
     ctor public CacheInfo();
-    method public com.android.art.ArtModuleInfo getArtModuleInfo();
-    method public com.android.art.BootClasspath getBootClasspath();
-    method public com.android.art.Dex2oatBootClasspath getDex2oatBootClasspath();
-    method public com.android.art.SystemServerClasspath getSystemServerClasspath();
-    method public void setArtModuleInfo(com.android.art.ArtModuleInfo);
-    method public void setBootClasspath(com.android.art.BootClasspath);
-    method public void setDex2oatBootClasspath(com.android.art.Dex2oatBootClasspath);
-    method public void setSystemServerClasspath(com.android.art.SystemServerClasspath);
+    method public com.android.art.ModuleInfo getArtModuleInfo();
+    method public com.android.art.Classpath getBootClasspath();
+    method public com.android.art.Classpath getDex2oatBootClasspath();
+    method public com.android.art.ModuleInfoList getModuleInfoList();
+    method public com.android.art.Classpath getSystemServerClasspath();
+    method public void setArtModuleInfo(com.android.art.ModuleInfo);
+    method public void setBootClasspath(com.android.art.Classpath);
+    method public void setDex2oatBootClasspath(com.android.art.Classpath);
+    method public void setModuleInfoList(com.android.art.ModuleInfoList);
+    method public void setSystemServerClasspath(com.android.art.Classpath);
+  }
+
+  public class Classpath {
+    ctor public Classpath();
+    method public com.android.art.Component getComponent();
+    method public void setComponent(com.android.art.Component);
   }
 
   public class Component {
@@ -39,16 +31,22 @@
     method public void setSize(java.math.BigInteger);
   }
 
-  public class Dex2oatBootClasspath {
-    ctor public Dex2oatBootClasspath();
-    method public com.android.art.Component getComponent();
-    method public void setComponent(com.android.art.Component);
+  public class ModuleInfo {
+    ctor public ModuleInfo();
+    method public long getLastUpdateMillis();
+    method public String getName();
+    method public long getVersionCode();
+    method public String getVersionName();
+    method public void setLastUpdateMillis(long);
+    method public void setName(String);
+    method public void setVersionCode(long);
+    method public void setVersionName(String);
   }
 
-  public class SystemServerClasspath {
-    ctor public SystemServerClasspath();
-    method public com.android.art.Component getComponent();
-    method public void setComponent(com.android.art.Component);
+  public class ModuleInfoList {
+    ctor public ModuleInfoList();
+    method public com.android.art.ModuleInfo getModuleInfo();
+    method public void setModuleInfo(com.android.art.ModuleInfo);
   }
 
   public class XmlParser {
diff --git a/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java b/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java
index b1fb4a2..d5ea545 100644
--- a/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java
+++ b/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java
@@ -72,6 +72,10 @@
 
     private final String[] ZYGOTE_NAMES = new String[] {"zygote", "zygote64"};
 
+    private Set<String> mZygoteArtifacts;
+    private Set<String> mSystemServerArtifacts;
+    private long mBootTimeMs;
+
     @Before
     public void setUp() throws Exception {
         assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported());
@@ -79,6 +83,13 @@
         mInstallUtils.installApexes(APEX_FILENAME);
         removeCompilationLogToAvoidBackoff();
         reboot();
+
+        mZygoteArtifacts = new HashSet<>();
+        for (String zygoteName : ZYGOTE_NAMES) {
+            mZygoteArtifacts.addAll(getZygoteLoadedArtifacts(zygoteName).orElse(new HashSet<>()));
+        }
+        mSystemServerArtifacts = getSystemServerLoadedArtifacts();
+        mBootTimeMs = getDevice().getDeviceDate();
     }
 
     @After
@@ -125,18 +136,18 @@
      * process does not exist.
      */
     private Optional<Set<String>> getZygoteLoadedArtifacts(String zygoteName) throws Exception {
-        final CommandResult pgrepResult = getDevice().executeShellV2Command("pgrep " + zygoteName);
+        final CommandResult pgrepResult = getDevice().executeShellV2Command("pidof " + zygoteName);
         if (pgrepResult.getExitCode() != 0) {
             return Optional.empty();
         }
         final String zygotePid = pgrepResult.getStdout();
 
-        final String bootExtensionName = "boot-framework";
-        return Optional.of(getMappedArtifacts(zygotePid, bootExtensionName));
+        final String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + ".*boot-framework";
+        return Optional.of(getMappedArtifacts(zygotePid, grepPattern));
     }
 
     private Set<String> getSystemServerLoadedArtifacts() throws Exception {
-        String systemServerPid = getDevice().executeShellCommand("pgrep system_server");
+        String systemServerPid = getDevice().executeShellCommand("pidof system_server");
         assertTrue(systemServerPid != null);
 
         // system_server artifacts are in the APEX data dalvik cache and names all contain
@@ -243,116 +254,146 @@
         verifyGeneratedArtifactsLoaded();
     }
 
-    @Test
-    public void verifyGeneratedArtifactsLoadedForSamegradeUpdate() throws Exception {
-        // Install the same APEX effecting a samegrade update. The setUp method has installed it
-        // before us.
-        mInstallUtils.installApexes(APEX_FILENAME);
-        reboot();
-
-        final boolean adbEnabled = getDevice().enableAdbRoot();
-        assertTrue("ADB root failed and required to get odrefresh compilation log", adbEnabled);
-
-        // Check that odrefresh logged a compilation attempt due to samegrade ART APEX install.
-        String[] logLines = getDevice().pullFileContents(ODREFRESH_COMPILATION_LOG).split("\n");
-        assertTrue(
-                "Expected 3 lines in " + ODREFRESH_COMPILATION_LOG + ", found " + logLines.length,
-                logLines.length == 3);
-
-        // Check that the compilation log entries are reasonable, ie times move forward.
-        // The first line of the log is the log format version number.
-        String[] firstUpdateEntry = logLines[1].split(" ");
-        String[] secondUpdateEntry = logLines[2].split(" ");
-        final int LOG_ENTRY_FIELDS = 5;
-        assertTrue(
-                "Unexpected number of fields: " + firstUpdateEntry.length + " != " +
-                LOG_ENTRY_FIELDS,
-                firstUpdateEntry.length == LOG_ENTRY_FIELDS);
-        assertTrue(firstUpdateEntry.length == secondUpdateEntry.length);
-
-        final int LAST_UPDATE_MILLIS_INDEX = 1;
-        final int COMPILATION_TIME_INDEX = 3;
-        for (int i = 0; i < firstUpdateEntry.length; ++i) {
-            final long firstField = Long.parseLong(firstUpdateEntry[i]);
-            final long secondField = Long.parseLong(secondUpdateEntry[i]);
-            if (i == LAST_UPDATE_MILLIS_INDEX) {
-                // The second APEX lastUpdateMillis should be after the first, but a clock
-                // adjustment might reverse the order so we can't assert this (b/194365586).
-                assertTrue(
-                        "Last update times are expected to differ, but they are equal " +
-                        firstField + " == " + secondField,
-                        firstField != secondField);
-            } else if (i == COMPILATION_TIME_INDEX) {
-                // The second compilation time should be after the first compilation time, but
-                // a clock adjustment might reverse the order so we can't assert this
-                // (b/194365586).
-                assertTrue(
-                        "Compilation times are expected to differ, but they are equal " +
-                        firstField + " == " + secondField,
-                        firstField != secondField);
-            } else {
-                // The remaining fields should be the same, ie trigger for compilation.
-                assertTrue(
-                        "Compilation entries differ for position " + i + ": " +
-                        firstField + " != " + secondField,
-                        firstField == secondField);
-            }
-        }
-
-        verifyGeneratedArtifactsLoaded();
-    }
-
     /**
-     * A workaround to simulate that an APEX has been upgraded. We could install a real APEX, but
-     * that would introduce an extra dependency to this test, which we want to avoid.
+     * Checks the input line by line and replaces all lines that match the regex with the given
+     * replacement.
      */
-    void mutateCacheInfo() throws Exception {
-        String cacheInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
+    String replaceLine(String input, String regex, String replacement) {
         StringBuffer output = new StringBuffer();
-        // com.android.wifi is a module in $BOOTCLASSPATH, but not in $DEX2OATBOOTCLASSPATH.
-        Pattern p = Pattern.compile("(.*/apex/com\\.android\\.wifi.*checksums=\\\").*?(\\\".*)");
-        for (String line : cacheInfo.split("\n")) {
+        Pattern p = Pattern.compile(regex);
+        for (String line : input.split("\n")) {
             Matcher m = p.matcher(line);
             if (m.matches()) {
-                m.appendReplacement(output, "$1aaaaaaaa$2");
+                m.appendReplacement(output, replacement);
                 output.append("\n");
             } else {
                 output.append(line + "\n");
             }
         }
-        getDevice().pushString(output.toString(), CACHE_INFO_FILE);
+        return output.toString();
     }
 
-    long getModifiedTimeSec(String filename) throws Exception {
+    /**
+     * Simulates that there is an OTA that updates a boot classpath jar.
+     */
+    void simulateBootClasspathOta() throws Exception {
+        String cacheInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
+        // Replace the cached checksum of /system/framework/framework.jar with "aaaaaaaa".
+        cacheInfo = replaceLine(
+                cacheInfo,
+                "(.*/system/framework/framework\\.jar.*checksums=\").*?(\".*)",
+                "$1aaaaaaaa$2");
+        getDevice().pushString(cacheInfo, CACHE_INFO_FILE);
+    }
+
+    /**
+     * Simulates that there is an OTA that updates a system server jar.
+     */
+    void simulateSystemServerOta() throws Exception {
+        String cacheInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
+        // Replace the cached checksum of /system/framework/services.jar with "aaaaaaaa".
+        cacheInfo = replaceLine(
+                cacheInfo,
+                "(.*/system/framework/services\\.jar.*checksums=\").*?(\".*)",
+                "$1aaaaaaaa$2");
+        getDevice().pushString(cacheInfo, CACHE_INFO_FILE);
+    }
+
+    /**
+     * Simulates that an ART APEX has been upgraded.
+     */
+    void simulateArtApexUpgrade() throws Exception {
+        String apexInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
+        // Replace the lastUpdateMillis of com.android.art with "1".
+        apexInfo = replaceLine(
+                apexInfo,
+                "(.*com\\.android\\.art.*lastUpdateMillis=\").*?(\".*)",
+                "$11$2");
+        getDevice().pushString(apexInfo, CACHE_INFO_FILE);
+    }
+
+    /**
+     * Simulates that an APEX has been upgraded. We could install a real APEX, but that would
+     * introduce an extra dependency to this test, which we want to avoid.
+     */
+    void simulateApexUpgrade() throws Exception {
+        String apexInfo = getDevice().pullFileContents(CACHE_INFO_FILE);
+        // Replace the lastUpdateMillis of com.android.wifi with "1".
+        apexInfo = replaceLine(
+                apexInfo,
+                "(.*com\\.android\\.wifi.*lastUpdateMillis=\").*?(\".*)",
+                "$11$2");
+        getDevice().pushString(apexInfo, CACHE_INFO_FILE);
+    }
+
+    long getModifiedTimeMs(String filename) throws Exception {
         String timeStr = getDevice()
-                .executeShellCommand(String.format("stat -c '%%Y' '%s'", filename))
+                .executeShellCommand(String.format("stat -c '%%.3Y' '%s'", filename))
                 .trim();
-        return Long.parseLong(timeStr);
+        return (long)(Double.parseDouble(timeStr) * 1000);
+    }
+
+    void assertArtifactsModifiedAfter(Set<String> artifacts, long timeMs) throws Exception {
+        for (String artifact : artifacts) {
+            assertTrue("Artifact " + artifact + " is not re-compiled",
+                    getModifiedTimeMs(artifact) > timeMs);
+        }
+    }
+
+    void assertArtifactsNotModifiedAfter(Set<String> artifacts, long timeMs) throws Exception {
+        for (String artifact : artifacts) {
+            assertTrue("Artifact " + artifact + " is unexpectedly re-compiled",
+                    getModifiedTimeMs(artifact) < timeMs);
+        }
     }
 
     @Test
-    public void verifyApexUpgradeTriggersCompilation() throws Exception {
-        Set<String> zygoteArtifacts = new HashSet<>();
-        for (String zygoteName : ZYGOTE_NAMES) {
-            zygoteArtifacts.addAll(getZygoteLoadedArtifacts(zygoteName).orElse(new HashSet<>()));
-        }
-        Set<String> systemServerArtifacts = getSystemServerLoadedArtifacts();
-        long timeSec = Math.floorDiv(getDevice().getDeviceDate(), 1000);
-
-        mutateCacheInfo();
+    public void verifyArtSamegradeUpdateTriggersCompilation() throws Exception {
+        simulateArtApexUpgrade();
         removeCompilationLogToAvoidBackoff();
-        CommandResult result =
-                getDevice().executeShellV2Command("odrefresh --compile");
+        getDevice().executeShellV2Command("odrefresh --compile");
 
-        for (String artifact : zygoteArtifacts) {
-            assertTrue("Boot classpath artifact " + artifact + " is re-compiled",
-                    getModifiedTimeSec(artifact) < timeSec);
-        }
+        assertArtifactsModifiedAfter(mZygoteArtifacts, mBootTimeMs);
+        assertArtifactsModifiedAfter(mSystemServerArtifacts, mBootTimeMs);
+    }
 
-        for (String artifact : systemServerArtifacts) {
-            assertTrue("System server artifact " + artifact + " is not re-compiled",
-                    getModifiedTimeSec(artifact) >= timeSec);
-        }
+    @Test
+    public void verifyOtherApexSamegradeUpdateTriggersCompilation() throws Exception {
+        simulateApexUpgrade();
+        removeCompilationLogToAvoidBackoff();
+        getDevice().executeShellV2Command("odrefresh --compile");
+
+        assertArtifactsNotModifiedAfter(mZygoteArtifacts, mBootTimeMs);
+        assertArtifactsModifiedAfter(mSystemServerArtifacts, mBootTimeMs);
+    }
+
+    @Test
+    public void verifyBootClasspathOtaTriggersCompilation() throws Exception {
+        simulateBootClasspathOta();
+        removeCompilationLogToAvoidBackoff();
+        getDevice().executeShellV2Command("odrefresh --compile");
+
+        assertArtifactsModifiedAfter(mZygoteArtifacts, mBootTimeMs);
+        assertArtifactsModifiedAfter(mSystemServerArtifacts, mBootTimeMs);
+    }
+
+    @Test
+    public void verifySystemServerOtaTriggersCompilation() throws Exception {
+        simulateSystemServerOta();
+        removeCompilationLogToAvoidBackoff();
+        getDevice().executeShellV2Command("odrefresh --compile");
+
+        assertArtifactsNotModifiedAfter(mZygoteArtifacts, mBootTimeMs);
+        assertArtifactsModifiedAfter(mSystemServerArtifacts, mBootTimeMs);
+    }
+
+    @Test
+    public void verifyNoCompilationWhenCacheIsGood() throws Exception {
+        removeCompilationLogToAvoidBackoff();
+        getDevice().executeShellV2Command("odrefresh --compile");
+
+        assertArtifactsNotModifiedAfter(mZygoteArtifacts, mBootTimeMs);
+        assertArtifactsNotModifiedAfter(mSystemServerArtifacts, mBootTimeMs);
     }
 
     private boolean haveCompilationLog() throws Exception {