odrefresh: add support for lastUpdateMillis as part of version check

This is to support samegrade updates for module evaluation.

(cherry picked from commit 79f874d287cb314dcf3fb8a78122d4296e91bd42)

Bug: 192647837
Test: atest art_standalone_odrefresh_tests
Test: atest odsign_e2e_tests
Merged-In: Ied43ebdcc4b2ec57e337e709970fab948cf5f992
Change-Id: I10d8e63cefe2e010f0856e0be71a5afe73b6f76f
diff --git a/odrefresh/CacheInfo.xsd b/odrefresh/CacheInfo.xsd
index a0c00af..b58453a 100644
--- a/odrefresh/CacheInfo.xsd
+++ b/odrefresh/CacheInfo.xsd
@@ -38,6 +38,8 @@
     <xs:attribute name="versionCode" type="xs:long" use="required" />
     <!-- Module versionName for the active ART 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`. -->
+    <xs:attribute name="lastUpdateMillis" type="xs:long" use="required" />
   </xs:complexType>
 
   <!-- Components of the `DEX2OATBOOTCLASSPATH`. -->
diff --git a/odrefresh/odr_compilation_log.cc b/odrefresh/odr_compilation_log.cc
index 55432f4..37804a2 100644
--- a/odrefresh/odr_compilation_log.cc
+++ b/odrefresh/odr_compilation_log.cc
@@ -41,7 +41,9 @@
   auto saved_exceptions = is.exceptions();
   is.exceptions(std::ios_base::iostate {});
 
+  // Write log entry. NB update OdrCompilationLog::kLogVersion if changing the format here.
   is >> entry.apex_version >> std::ws;
+  is >> entry.last_update_millis >> std::ws;
   is >> entry.trigger >> std::ws;
   is >> entry.when >> std::ws;
   is >> entry.exit_code >> std::ws;
@@ -59,6 +61,7 @@
   os.exceptions(std::ios_base::iostate {});
 
   os << entry.apex_version << kSpace;
+  os << entry.last_update_millis << kSpace;
   os << entry.trigger << kSpace;
   os << entry.when << kSpace;
   os << entry.exit_code << std::endl;
@@ -69,8 +72,8 @@
 }
 
 bool operator==(const OdrCompilationLogEntry& lhs, const OdrCompilationLogEntry& rhs) {
-  return lhs.apex_version == rhs.apex_version && lhs.trigger == rhs.trigger &&
-         lhs.when == rhs.when && lhs.exit_code == rhs.exit_code;
+  return lhs.apex_version == rhs.apex_version && lhs.last_update_millis == rhs.last_update_millis &&
+         lhs.trigger == rhs.trigger && lhs.when == rhs.when && lhs.exit_code == rhs.exit_code;
 }
 
 bool operator!=(const OdrCompilationLogEntry& lhs, const OdrCompilationLogEntry& rhs) {
@@ -98,6 +101,12 @@
     return false;
   }
 
+  std::string log_version;
+  ifs >> log_version >> std::ws;
+  if (log_version != kLogVersion) {
+    return false;
+  }
+
   while (!ifs.eof()) {
     OdrCompilationLogEntry entry;
     ifs >> entry;
@@ -117,6 +126,7 @@
     return false;
   }
 
+  ofs << kLogVersion << std::endl;
   for (const auto& entry : entries_) {
     ofs << entry;
     if (ofs.fail()) {
@@ -148,23 +158,29 @@
 }
 
 void OdrCompilationLog::Log(int64_t apex_version,
+                            int64_t last_update_millis,
                             OdrMetrics::Trigger trigger,
                             ExitCode compilation_result) {
   time_t now;
   time(&now);
-  Log(apex_version, trigger, now, compilation_result);
+  Log(apex_version, last_update_millis, trigger, now, compilation_result);
 }
 
 void OdrCompilationLog::Log(int64_t apex_version,
+                            int64_t last_update_millis,
                             OdrMetrics::Trigger trigger,
                             time_t when,
                             ExitCode compilation_result) {
-  entries_.push_back(OdrCompilationLogEntry{
-      apex_version, static_cast<int32_t>(trigger), when, static_cast<int32_t>(compilation_result)});
+  entries_.push_back(OdrCompilationLogEntry{apex_version,
+                                            last_update_millis,
+                                            static_cast<int32_t>(trigger),
+                                            when,
+                                            static_cast<int32_t>(compilation_result)});
   Truncate();
 }
 
 bool OdrCompilationLog::ShouldAttemptCompile(int64_t apex_version,
+                                             int64_t last_update_millis,
                                              OdrMetrics::Trigger trigger,
                                              time_t now) const {
   if (entries_.size() == 0) {
@@ -173,7 +189,12 @@
   }
 
   if (apex_version != entries_.back().apex_version) {
-    // There is a new ART APEX, we should use compile right away.
+    // There is a new ART APEX, we should compile right away.
+    return true;
+  }
+
+    if (last_update_millis != entries_.back().last_update_millis) {
+    // There is a samegrade ART APEX update, we should compile right away.
     return true;
   }
 
diff --git a/odrefresh/odr_compilation_log.h b/odrefresh/odr_compilation_log.h
index 6f13c97..894079c 100644
--- a/odrefresh/odr_compilation_log.h
+++ b/odrefresh/odr_compilation_log.h
@@ -32,6 +32,7 @@
 // OdrCompilationLogEntry represents the result of a compilation attempt by odrefresh.
 struct OdrCompilationLogEntry {
   int64_t apex_version;
+  int64_t last_update_millis;
   int32_t trigger;
   time_t when;
   int32_t exit_code;
@@ -53,6 +54,11 @@
   // directory is only used by odrefresh whereas the ART apexdata directory is also used by odsign
   // and others which may lead to the deletion (or rollback) of the log file.
   static constexpr const char* kCompilationLogFile = "/data/misc/odrefresh/compilation-log.txt";
+
+  // Version string that appears on the first line of the compilation log.
+  static constexpr const char kLogVersion[] = "CompilationLog/1.0";
+
+  // Number of log entries in the compilation log.
   static constexpr const size_t kMaxLoggedEntries = 4;
 
   explicit OdrCompilationLog(const char* compilation_log_path = kCompilationLogFile);
@@ -60,6 +66,7 @@
 
   // Applies policy to compilation log to determine whether to recompile.
   bool ShouldAttemptCompile(int64_t apex_version,
+                            int64_t last_update_millis,
                             OdrMetrics::Trigger trigger,
                             time_t now = 0) const;
 
@@ -69,9 +76,13 @@
   // Returns the entry at position `index` or nullptr if `index` is out of bounds.
   const OdrCompilationLogEntry* Peek(size_t index) const;
 
-  void Log(int64_t apex_version, OdrMetrics::Trigger trigger, ExitCode compilation_result);
+  void Log(int64_t apex_version,
+           int64_t last_update_millis,
+           OdrMetrics::Trigger trigger,
+           ExitCode compilation_result);
 
   void Log(int64_t apex_version,
+           int64_t last_update_millis,
            OdrMetrics::Trigger trigger,
            time_t when,
            ExitCode compilation_result);
diff --git a/odrefresh/odr_compilation_log_test.cc b/odrefresh/odr_compilation_log_test.cc
index c5c9555..d99b4d9 100644
--- a/odrefresh/odr_compilation_log_test.cc
+++ b/odrefresh/odr_compilation_log_test.cc
@@ -28,8 +28,8 @@
 #include <string>
 #include <vector>
 
+#include "android-base/file.h"
 #include "base/common_art_test.h"
-
 #include "odrefresh/odrefresh.h"
 #include "odr_metrics.h"
 
@@ -41,28 +41,31 @@
 class OdrCompilationLogTest : public CommonArtTest {};
 
 TEST(OdrCompilationLogEntry, Equality) {
-  OdrCompilationLogEntry a{1, 2, 3, 4};
+  OdrCompilationLogEntry a{1, 2, 3, 4, 5};
 
-  ASSERT_EQ(a, (OdrCompilationLogEntry{1, 2, 3, 4}));
-  ASSERT_NE(a, (OdrCompilationLogEntry{9, 2, 3, 4}));
-  ASSERT_NE(a, (OdrCompilationLogEntry{1, 9, 3, 4}));
-  ASSERT_NE(a, (OdrCompilationLogEntry{1, 2, 9, 4}));
-  ASSERT_NE(a, (OdrCompilationLogEntry{2, 2, 3, 9}));
+  ASSERT_EQ(a, (OdrCompilationLogEntry{1, 2, 3, 4, 5}));
+  ASSERT_NE(a, (OdrCompilationLogEntry{9, 2, 3, 4, 5}));
+  ASSERT_NE(a, (OdrCompilationLogEntry{1, 9, 3, 4, 5}));
+  ASSERT_NE(a, (OdrCompilationLogEntry{1, 2, 9, 4, 5}));
+  ASSERT_NE(a, (OdrCompilationLogEntry{2, 2, 3, 9, 5}));
+  ASSERT_NE(a, (OdrCompilationLogEntry{2, 2, 3, 5, 9}));
 }
 
 TEST(OdrCompilationLogEntry, InputOutput) {
   const OdrCompilationLogEntry entries[] = {
-      {1, 2, 3, 4},
+      {1, 2, 3, 4, 5},
       {std::numeric_limits<int64_t>::min(),
+       std::numeric_limits<int64_t>::min(),
        std::numeric_limits<int32_t>::min(),
        std::numeric_limits<time_t>::min(),
        std::numeric_limits<int32_t>::min()},
       {std::numeric_limits<int64_t>::max(),
+       std::numeric_limits<int64_t>::max(),
        std::numeric_limits<int32_t>::max(),
        std::numeric_limits<time_t>::max(),
        std::numeric_limits<int32_t>::max()},
-       {0, 0, 0, 0},
-      {0x7fedcba9'87654321, 0x12345678, 0x2346789, 0x76543210}
+       {0, 0, 0, 0, 0},
+      {0x7fedcba9'87654321, 0x5a5a5a5a'5a5a5a5a, 0x12345678, 0x2346789, 0x76543210}
   };
   for (const auto& entry : entries) {
     std::stringstream ss;
@@ -86,12 +89,12 @@
 
 TEST(OdrCompilationLogEntry, ReadMultiple) {
   std::stringstream ss;
-  ss << "1 2 3 4\n5 6 7 8\n";
+  ss << "0 1 2 3 4\n5 6 7 8 9\n";
 
   OdrCompilationLogEntry entry0, entry1;
   ss >> entry0 >> entry1;
-  ASSERT_EQ(entry0, (OdrCompilationLogEntry{1, 2, 3, 4}));
-  ASSERT_EQ(entry1, (OdrCompilationLogEntry{5, 6, 7, 8}));
+  ASSERT_EQ(entry0, (OdrCompilationLogEntry{0, 1, 2, 3, 4}));
+  ASSERT_EQ(entry1, (OdrCompilationLogEntry{5, 6, 7, 8, 9}));
 
   ASSERT_FALSE(ss.fail());
   ASSERT_FALSE(ss.bad());
@@ -100,14 +103,24 @@
 TEST(OdrCompilationLog, ShouldAttemptCompile) {
   OdrCompilationLog ocl(/*compilation_log_path=*/nullptr);
 
-  ASSERT_TRUE(ocl.ShouldAttemptCompile(1, OdrMetrics::Trigger::kMissingArtifacts, 0));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1, /*last_update_millis=*/762, OdrMetrics::Trigger::kMissingArtifacts, 0));
 
   ocl.Log(
-      /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, ExitCode::kCompilationSuccess);
-  ASSERT_TRUE(ocl.ShouldAttemptCompile(2, OdrMetrics::Trigger::kApexVersionMismatch));
-  ASSERT_FALSE(ocl.ShouldAttemptCompile(1, OdrMetrics::Trigger::kApexVersionMismatch));
-  ASSERT_TRUE(ocl.ShouldAttemptCompile(1, OdrMetrics::Trigger::kDexFilesChanged));
-  ASSERT_FALSE(ocl.ShouldAttemptCompile(1, OdrMetrics::Trigger::kUnknown));
+      /*apex_version=*/1,
+      /*last_update_millis=*/762,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      ExitCode::kCompilationSuccess);
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/2, /*last_update_millis=*/762, OdrMetrics::Trigger::kApexVersionMismatch));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1, /*last_update_millis=*/10000, OdrMetrics::Trigger::kApexVersionMismatch));
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1, /*last_update_millis=*/762, OdrMetrics::Trigger::kApexVersionMismatch));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1, /*last_update_millis=*/762, OdrMetrics::Trigger::kDexFilesChanged));
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1, /*last_update_millis=*/762, OdrMetrics::Trigger::kUnknown));
 }
 
 TEST(OdrCompilationLog, BackOffNoHistory) {
@@ -117,63 +130,81 @@
   OdrCompilationLog ocl(/*compilation_log_path=*/nullptr);
 
   ASSERT_TRUE(ocl.ShouldAttemptCompile(
-      /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+      /*apex_version=*/1,
+      /*last_update_millis=*/0,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time));
 
   // Start log
   ocl.Log(/*apex_version=*/1,
+          /*last_update_millis=*/0,
           OdrMetrics::Trigger::kApexVersionMismatch,
           start_time,
           ExitCode::kCompilationFailed);
   ASSERT_FALSE(ocl.ShouldAttemptCompile(
-      /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+      /*apex_version=*/1,
+      /*last_update_millis=*/0,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time));
   ASSERT_FALSE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + kSecondsPerDay / 2));
   ASSERT_TRUE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + kSecondsPerDay));
 
   // Add one more log entry
   ocl.Log(/*apex_version=*/1,
+          /*last_update_millis=*/0,
           OdrMetrics::Trigger::kApexVersionMismatch,
           start_time,
           ExitCode::kCompilationFailed);
   ASSERT_FALSE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + kSecondsPerDay));
   ASSERT_TRUE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + 2 * kSecondsPerDay));
 
   // One more.
   ocl.Log(/*apex_version=*/1,
+          /*last_update_millis=*/0,
           OdrMetrics::Trigger::kApexVersionMismatch,
           start_time,
           ExitCode::kCompilationFailed);
   ASSERT_FALSE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + 3 * kSecondsPerDay));
   ASSERT_TRUE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + 4 * kSecondsPerDay));
 
   // And one for the road.
   ocl.Log(/*apex_version=*/1,
+          /*last_update_millis=*/0,
           OdrMetrics::Trigger::kApexVersionMismatch,
           start_time,
           ExitCode::kCompilationFailed);
   ASSERT_FALSE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + 7 * kSecondsPerDay));
   ASSERT_TRUE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + 8 * kSecondsPerDay));
 }
@@ -186,31 +217,40 @@
 
   // Start log with a successful entry.
   ocl.Log(/*apex_version=*/1,
+          /*last_update_millis=*/0,
           OdrMetrics::Trigger::kApexVersionMismatch,
           start_time,
           ExitCode::kCompilationSuccess);
   ASSERT_FALSE(ocl.ShouldAttemptCompile(
-      /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+      /*apex_version=*/1,
+      /*last_update_millis=*/0,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time));
   ASSERT_FALSE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + kSecondsPerDay / 4));
   ASSERT_TRUE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + kSecondsPerDay / 2));
 
-    // Add a log entry for a failed compilation.
+  // Add a log entry for a failed compilation.
   ocl.Log(/*apex_version=*/1,
+          /*last_update_millis=*/0,
           OdrMetrics::Trigger::kApexVersionMismatch,
           start_time,
           ExitCode::kCompilationFailed);
   ASSERT_FALSE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + kSecondsPerDay / 2));
   ASSERT_TRUE(ocl.ShouldAttemptCompile(
       /*apex_version=*/1,
+      /*last_update_millis=*/0,
       OdrMetrics::Trigger::kApexVersionMismatch,
       start_time + kSecondsPerDay));
 }
@@ -219,18 +259,19 @@
   OdrCompilationLog ocl(/*compilation_log_path=*/nullptr);
 
   std::vector<OdrCompilationLogEntry> entries = {
-    { 0, 1, 2, 3 },
-    { 1, 2, 3, 4 },
-    { 2, 3, 4, 5 },
-    { 3, 4, 5, 6 },
-    { 4, 5, 6, 7 },
-    { 5, 6, 7, 8 },
-    { 6, 7, 8, 9 }
+    { 0, 1, 2, 3, 4 },
+    { 1, 2, 3, 4, 5 },
+    { 2, 3, 4, 5, 6 },
+    { 3, 4, 5, 6, 7 },
+    { 4, 5, 6, 7, 8 },
+    { 5, 6, 7, 8, 9 },
+    { 6, 7, 8, 9, 10 }
   };
 
   for (size_t i = 0; i < entries.size(); ++i) {
     OdrCompilationLogEntry& e = entries[i];
     ocl.Log(e.apex_version,
+            e.last_update_millis,
             static_cast<OdrMetrics::Trigger>(e.trigger),
             e.when,
             static_cast<ExitCode>(e.exit_code));
@@ -251,13 +292,13 @@
 
 TEST_F(OdrCompilationLogTest, LogReadWrite) {
   std::vector<OdrCompilationLogEntry> entries = {
-    { 0, 1, 2, 3 },
-    { 1, 2, 3, 4 },
-    { 2, 3, 4, 5 },
-    { 3, 4, 5, 6 },
-    { 4, 5, 6, 7 },
-    { 5, 6, 7, 8 },
-    { 6, 7, 8, 9 }
+    { 0, 1, 2, 3, 4 },
+    { 1, 2, 3, 4, 5 },
+    { 2, 3, 4, 5, 6 },
+    { 3, 4, 5, 6, 7 },
+    { 4, 5, 6, 7, 8 },
+    { 5, 6, 7, 8, 9 },
+    { 6, 7, 8, 9, 10 }
   };
 
   ScratchFile scratch_file;
@@ -268,6 +309,7 @@
       OdrCompilationLog ocl(scratch_file.GetFilename().c_str());
       OdrCompilationLogEntry& e = entries[i];
       ocl.Log(e.apex_version,
+              e.last_update_millis,
               static_cast<OdrMetrics::Trigger>(e.trigger),
               e.when,
               static_cast<ExitCode>(e.exit_code));
@@ -303,7 +345,10 @@
     OdrCompilationLog ocl(log_path);
 
     ASSERT_TRUE(ocl.ShouldAttemptCompile(
-        /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+        /*apex_version=*/1,
+        /*last_update_millis=*/0,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time));
   }
 
   {
@@ -311,6 +356,7 @@
 
     // Start log
     ocl.Log(/*apex_version=*/1,
+            /*last_update_millis=*/0,
             OdrMetrics::Trigger::kApexVersionMismatch,
             start_time,
             ExitCode::kCompilationFailed);
@@ -319,13 +365,18 @@
   {
     OdrCompilationLog ocl(log_path);
     ASSERT_FALSE(ocl.ShouldAttemptCompile(
-        /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+        /*apex_version=*/1,
+        /*last_update_millis=*/0,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time));
     ASSERT_FALSE(ocl.ShouldAttemptCompile(
         /*apex_version=*/1,
+        /*last_update_millis=*/0,
         OdrMetrics::Trigger::kApexVersionMismatch,
         start_time + kSecondsPerDay / 2));
     ASSERT_TRUE(ocl.ShouldAttemptCompile(
         /*apex_version=*/1,
+        /*last_update_millis=*/0,
         OdrMetrics::Trigger::kApexVersionMismatch,
         start_time + kSecondsPerDay));
   }
@@ -334,6 +385,7 @@
     // Add one more log entry
     OdrCompilationLog ocl(log_path);
     ocl.Log(/*apex_version=*/1,
+            /*last_update_millis=*/0,
             OdrMetrics::Trigger::kApexVersionMismatch,
             start_time,
             ExitCode::kCompilationFailed);
@@ -344,10 +396,12 @@
 
     ASSERT_FALSE(ocl.ShouldAttemptCompile(
         /*apex_version=*/1,
+        /*last_update_millis=*/0,
         OdrMetrics::Trigger::kApexVersionMismatch,
         start_time + kSecondsPerDay));
     ASSERT_TRUE(ocl.ShouldAttemptCompile(
         /*apex_version=*/1,
+        /*last_update_millis=*/0,
         OdrMetrics::Trigger::kApexVersionMismatch,
         start_time + 2 * kSecondsPerDay));
   }
@@ -356,27 +410,7 @@
     // One more log entry.
     OdrCompilationLog ocl(log_path);
     ocl.Log(/*apex_version=*/1,
-          OdrMetrics::Trigger::kApexVersionMismatch,
-          start_time,
-          ExitCode::kCompilationFailed);
-  }
-
-  {
-    OdrCompilationLog ocl(log_path);
-    ASSERT_FALSE(ocl.ShouldAttemptCompile(
-        /*apex_version=*/1,
-        OdrMetrics::Trigger::kApexVersionMismatch,
-        start_time + 3 * kSecondsPerDay));
-    ASSERT_TRUE(ocl.ShouldAttemptCompile(
-        /*apex_version=*/1,
-        OdrMetrics::Trigger::kApexVersionMismatch,
-        start_time + 4 * kSecondsPerDay));
-  }
-
-  {
-    // And one for the road.
-    OdrCompilationLog ocl(log_path);
-    ocl.Log(/*apex_version=*/1,
+            /*last_update_millis=*/0,
             OdrMetrics::Trigger::kApexVersionMismatch,
             start_time,
             ExitCode::kCompilationFailed);
@@ -386,14 +420,125 @@
     OdrCompilationLog ocl(log_path);
     ASSERT_FALSE(ocl.ShouldAttemptCompile(
         /*apex_version=*/1,
+        /*last_update_millis=*/0,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + 3 * kSecondsPerDay));
+    ASSERT_TRUE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        /*last_update_millis=*/0,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + 4 * kSecondsPerDay));
+  }
+
+  {
+    // And one for the road.
+    OdrCompilationLog ocl(log_path);
+    ocl.Log(/*apex_version=*/1,
+            /*last_update_millis=*/0,
+            OdrMetrics::Trigger::kApexVersionMismatch,
+            start_time,
+            ExitCode::kCompilationFailed);
+  }
+
+  {
+    OdrCompilationLog ocl(log_path);
+    ASSERT_FALSE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        /*last_update_millis=*/0,
         OdrMetrics::Trigger::kApexVersionMismatch,
         start_time + 7 * kSecondsPerDay));
     ASSERT_TRUE(ocl.ShouldAttemptCompile(
         /*apex_version=*/1,
+        /*last_update_millis=*/0,
         OdrMetrics::Trigger::kApexVersionMismatch,
         start_time + 8 * kSecondsPerDay));
   }
 }
 
+TEST(OdrCompilationLog, LastUpdateMillisChangeTriggersCompilation) {
+  time_t start_time;
+  time(&start_time);
+
+  OdrCompilationLog ocl(/*compilation_log_path=*/nullptr);
+
+  for (int64_t last_update_millis = 0; last_update_millis < 10000; last_update_millis += 1000) {
+    static const int64_t kApexVersion = 19999;
+    ASSERT_TRUE(ocl.ShouldAttemptCompile(
+        kApexVersion, last_update_millis, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+    ocl.Log(kApexVersion,
+            last_update_millis,
+            OdrMetrics::Trigger::kApexVersionMismatch,
+            start_time,
+            ExitCode::kCompilationSuccess);
+    ASSERT_FALSE(ocl.ShouldAttemptCompile(kApexVersion,
+                                          last_update_millis,
+                                          OdrMetrics::Trigger::kApexVersionMismatch,
+                                          start_time + 1));
+  }
+}
+
+TEST(OdrCompilationLog, ApexVersionChangeTriggersCompilation) {
+  time_t start_time;
+  time(&start_time);
+
+  OdrCompilationLog ocl(/*compilation_log_path=*/nullptr);
+
+  for (int64_t apex_version = 0; apex_version < 10000; apex_version += 1000) {
+    static const int64_t kLastUpdateMillis = 777;
+    ASSERT_TRUE(ocl.ShouldAttemptCompile(apex_version,
+                                         kLastUpdateMillis,
+                                         OdrMetrics::Trigger::kApexVersionMismatch,
+                                         start_time + 8 * kSecondsPerDay));
+    ocl.Log(apex_version,
+            kLastUpdateMillis,
+            OdrMetrics::Trigger::kApexVersionMismatch,
+            start_time,
+            ExitCode::kCompilationSuccess);
+    ASSERT_FALSE(ocl.ShouldAttemptCompile(apex_version,
+                                          kLastUpdateMillis,
+                                          OdrMetrics::Trigger::kApexVersionMismatch,
+                                          start_time + 1));
+  }
+}
+
+TEST_F(OdrCompilationLogTest, NewLogVersionTriggersCompilation) {
+  static const int64_t kApexVersion = 1066;
+  static const int64_t kLastUpdateMillis = 777;
+  time_t start_time;
+  time(&start_time);
+
+  ScratchFile scratch_file;
+  scratch_file.Close();
+
+  // Generate a compilation log.
+  {
+    OdrCompilationLog ocl(scratch_file.GetFilename().c_str());
+    for (size_t i = 0; i < OdrCompilationLog::kMaxLoggedEntries; ++i) {
+      ocl.Log(kApexVersion,
+              kLastUpdateMillis,
+              OdrMetrics::Trigger::kApexVersionMismatch,
+              start_time,
+              ExitCode::kCompilationSuccess);
+      ASSERT_FALSE(ocl.ShouldAttemptCompile(
+          kApexVersion, kLastUpdateMillis, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+    }
+  }
+
+  // Replace version string in the compilation log.
+  std::string log_text;
+  ASSERT_TRUE(android::base::ReadFileToString(scratch_file.GetFilename(), &log_text));
+  std::string new_log_version = std::string(OdrCompilationLog::kLogVersion) + "a";
+  log_text.replace(0, new_log_version.size() - 1, new_log_version);
+  ASSERT_TRUE(android::base::WriteStringToFile(log_text, scratch_file.GetFilename()));
+
+  // Read log with updated version entry, check it is treated as out-of-date.
+  {
+    OdrCompilationLog ocl(scratch_file.GetFilename().c_str());
+    ASSERT_TRUE(ocl.ShouldAttemptCompile(
+        kApexVersion, kLastUpdateMillis, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+    ASSERT_EQ(0u, ocl.NumberOfEntries());
+  }
+}
+
 }  // namespace odrefresh
 }  // namespace art
diff --git a/odrefresh/odr_metrics.h b/odrefresh/odr_metrics.h
index 5ff9df2..cd80bef 100644
--- a/odrefresh/odr_metrics.h
+++ b/odrefresh/odr_metrics.h
@@ -75,7 +75,7 @@
   ~OdrMetrics();
 
   // Gets the ART APEX that metrics are being collected on behalf of.
-  int64_t GetApexVersion() const {
+  int64_t GetArtApexVersion() const {
     return art_apex_version_;
   }
 
@@ -84,6 +84,16 @@
     art_apex_version_ = version;
   }
 
+  // Gets the ART APEX last update time in milliseconds.
+  int64_t GetArtApexLastUpdateMillis() const {
+    return art_apex_last_update_millis_;
+  }
+
+  // Sets the ART APEX last update time in milliseconds.
+  void SetArtApexLastUpdateMillis(int64_t last_update_millis) {
+    art_apex_last_update_millis_ = last_update_millis;
+  }
+
   // Gets the trigger for metrics collection. The trigger is the reason why odrefresh considers
   // compilation necessary.
   Trigger GetTrigger() const {
@@ -122,6 +132,7 @@
   const std::string metrics_file_;
 
   int64_t art_apex_version_ = 0;
+  int64_t art_apex_last_update_millis_ = 0;
   std::optional<Trigger> trigger_ = {};  // metrics are only logged if compilation is triggered.
   Stage stage_ = Stage::kUnknown;
   Status status_ = Status::kUnknown;
diff --git a/odrefresh/odrefresh.cc b/odrefresh/odrefresh.cc
index cccf2b3..d3af247 100644
--- a/odrefresh/odrefresh.cc
+++ b/odrefresh/odrefresh.cc
@@ -360,7 +360,10 @@
       LOG(ERROR) << "Could not update " << QuotePath(cache_info_filename_) << " : no ART Apex info";
       return {};
     }
-    return art_apex::ArtModuleInfo{info->getVersionCode(), info->getVersionName()};
+    // 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};
   }
 
   bool CheckComponents(const std::vector<art_apex::Component>& expected_components,
@@ -530,9 +533,12 @@
       return cleanup_return(ExitCode::kCompilationRequired);
     }
 
-    // Record ART Apex version for metrics reporting.
+    // 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());
+
     // 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();
@@ -546,13 +552,25 @@
     }
 
     if (cached_info->getVersionName() != current_info->getVersionName()) {
-      LOG(INFO) << "ART APEX version code mismatch ("
+      LOG(INFO) << "ART APEX version name mismatch ("
                 << cached_info->getVersionName()
                 << " != " << current_info->getVersionName() << ").";
       metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
       return cleanup_return(ExitCode::kCompilationRequired);
     }
 
+    // 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() << ").";
+      metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
+      return cleanup_return(ExitCode::kCompilationRequired);
+    }
+
     // Check boot class components.
     //
     // This checks the size and checksums of odrefresh compilable files on the DEX2OATBOOTCLASSPATH
@@ -1449,11 +1467,16 @@
           return exit_code;
         }
         OdrCompilationLog compilation_log;
-        if (!compilation_log.ShouldAttemptCompile(metrics.GetApexVersion(), metrics.GetTrigger())) {
+        if (!compilation_log.ShouldAttemptCompile(metrics.GetArtApexVersion(),
+                                                  metrics.GetArtApexLastUpdateMillis(),
+                                                  metrics.GetTrigger())) {
           return ExitCode::kOkay;
         }
         ExitCode compile_result = odr.Compile(metrics, /*force_compile=*/false);
-        compilation_log.Log(metrics.GetApexVersion(), metrics.GetTrigger(), compile_result);
+        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);
diff --git a/odrefresh/schema/current.txt b/odrefresh/schema/current.txt
index ae10712..e6933f6 100644
--- a/odrefresh/schema/current.txt
+++ b/odrefresh/schema/current.txt
@@ -3,8 +3,10 @@
 
   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);
   }