Add logging of the log list timestamp

This CL adds the parsed log list timestamp from the JSON log list, as
well as logging a SUCCESS event (with timestamp and signature) when the
log list has been successfully updated.

Fixes: 391327942
Fixes: 394278886
Bug: 378626065
Flag: com.android.net.ct.flags.certificate_transparency_service
Test: atest NetworkSecurityUnitTests

Change-Id: I8f0d96ca96cd89dce5da588cb478f9332537df48
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index 1fbb3f3..fb42c03 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -199,7 +199,6 @@
         }
 
         LogListUpdateStatus updateStatus = mSignatureVerifier.verify(contentUri, metadataUri);
-        // TODO(b/391327942): parse file and log the timestamp of the log list
 
         if (!updateStatus.isSignatureVerified()) {
             Log.w(TAG, "Log list did not pass verification");
@@ -209,42 +208,30 @@
             return;
         }
 
-        boolean success = false;
-
         try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
-            success = compatVersion.install(inputStream);
+            updateStatus = compatVersion.install(inputStream, updateStatus.toBuilder());
         } catch (IOException e) {
             Log.e(TAG, "Could not install new content", e);
             return;
         }
 
-        if (success) {
-            // Reset the number of consecutive log list failure updates back to zero.
-            mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
-            mDataStore.store();
-        } else {
-            mLogger.logCTLogListUpdateStateChangedEvent(
-                    updateStatus
-                            .toBuilder()
-                            .setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS)
-                            .build());
-            }
-        }
+        mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
+    }
 
     private void handleDownloadFailed(DownloadStatus status) {
         Log.e(TAG, "Download failed with " + status);
 
-        LogListUpdateStatus.Builder updateStatus = LogListUpdateStatus.builder();
+        LogListUpdateStatus.Builder updateStatusBuilder = LogListUpdateStatus.builder();
         if (status.isHttpError()) {
-            updateStatus
+            updateStatusBuilder
                     .setState(CTLogListUpdateState.HTTP_ERROR)
                     .setHttpErrorStatusCode(status.reason());
         } else {
             // TODO(b/384935059): handle blocked domain logging
-            updateStatus.setDownloadStatus(Optional.of(status.reason()));
+            updateStatusBuilder.setDownloadStatus(Optional.of(status.reason()));
         }
 
-        mLogger.logCTLogListUpdateStateChangedEvent(updateStatus.build());
+        mLogger.logCTLogListUpdateStateChangedEvent(updateStatusBuilder.build());
     }
 
     private long download(String url) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
index 967a04b..2a37d8f 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -35,10 +35,12 @@
     enum CTLogListUpdateState {
         UNKNOWN_STATE,
         HTTP_ERROR,
+        LOG_LIST_INVALID,
         PUBLIC_KEY_NOT_FOUND,
         SIGNATURE_INVALID,
         SIGNATURE_NOT_FOUND,
         SIGNATURE_VERIFICATION_FAILED,
+        SUCCESS,
         VERSION_ALREADY_EXISTS
     }
 }
\ No newline at end of file
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
index 9c3210d..f617523 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -20,6 +20,7 @@
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
@@ -29,6 +30,7 @@
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
 
 import android.app.DownloadManager;
 
@@ -43,6 +45,12 @@
 
     @Override
     public void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus) {
+        if (updateStatus.isSuccessful()) {
+            resetFailureCount();
+        } else {
+            updateFailureCount();
+        }
+
         int updateState =
                 updateStatus
                         .downloadStatus()
@@ -56,20 +64,31 @@
                 updateState,
                 failureCount,
                 updateStatus.httpErrorStatusCode(),
-                updateStatus.signature());
+                updateStatus.signature(),
+                updateStatus.logListTimestamp());
     }
 
     private void logCTLogListUpdateStateChangedEvent(
-            int updateState, int failureCount, int httpErrorStatusCode, String signature) {
-        updateFailureCount();
-
+            int updateState,
+            int failureCount,
+            int httpErrorStatusCode,
+            String signature,
+            long logListTimestamp) {
         CertificateTransparencyStatsLog.write(
                 CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED,
                 updateState,
                 failureCount,
                 httpErrorStatusCode,
                 signature,
-                /* logListTimestampMs= */ 0);
+                logListTimestamp);
+    }
+
+    /**
+     * Resets the number of consecutive log list update failures in the data store back to zero.
+     */
+    private void resetFailureCount() {
+        mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
+        mDataStore.store();
     }
 
     /**
@@ -112,6 +131,8 @@
         switch (updateState) {
             case HTTP_ERROR:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+            case LOG_LIST_INVALID:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
             case PUBLIC_KEY_NOT_FOUND:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
             case SIGNATURE_INVALID:
@@ -120,6 +141,8 @@
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
             case SIGNATURE_VERIFICATION_FAILED:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+            case SUCCESS:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
             case VERSION_ALREADY_EXISTS:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
             case UNKNOWN_STATE:
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index 9d60163..e8a6e64 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -23,6 +23,8 @@
 import android.system.Os;
 import android.util.Log;
 
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -69,22 +71,29 @@
      * Installs a log list within this compatibility version directory.
      *
      * @param newContent an input stream providing the log list
+     * @param statusBuilder status obj builder containing details of the log list update process
      * @return true if the log list was installed successfully, false otherwise.
      * @throws IOException if the list cannot be saved in the CT directory.
      */
-    boolean install(InputStream newContent) throws IOException {
+    LogListUpdateStatus install(
+            InputStream newContent, LogListUpdateStatus.Builder statusBuilder) throws IOException {
         String content = new String(newContent.readAllBytes(), UTF_8);
         try {
+            JSONObject contentJson = new JSONObject(content);
             return install(
                     new ByteArrayInputStream(content.getBytes()),
-                    new JSONObject(content).getString("version"));
+                    contentJson.getString("version"),
+                    statusBuilder.setLogListTimestamp(contentJson.getLong("log_list_timestamp")));
         } catch (JSONException e) {
             Log.e(TAG, "invalid log list format", e);
-            return false;
+
+            return statusBuilder.setState(CTLogListUpdateState.LOG_LIST_INVALID).build();
         }
     }
 
-    private boolean install(InputStream newContent, String version) throws IOException {
+    LogListUpdateStatus install(
+            InputStream newContent, String version, LogListUpdateStatus.Builder statusBuilder)
+            throws IOException {
         // To support atomically replacing the old configuration directory with the new
         // there's a bunch of steps. We create a new directory with the logs and then do
         // an atomic update of the current symlink to point to the new directory.
@@ -100,7 +109,7 @@
             if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
                 Log.i(TAG, newLogsDir + " already exists, skipping install.");
                 deleteOldLogDirectories();
-                return false;
+                return statusBuilder.setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS).build();
             }
             // If the symlink has not been updated then the previous installation failed and
             // this is a re-attempt. Clean-up leftover files and try again.
@@ -134,7 +143,7 @@
         // 7. Cleanup
         Log.i(TAG, "New logs installed at " + newLogsDir);
         deleteOldLogDirectories();
-        return true;
+        return statusBuilder.setState(CTLogListUpdateState.SUCCESS).build();
     }
 
     String getCompatVersion() {
diff --git a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
index 3d05857..3f9b762 100644
--- a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
+++ b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
@@ -19,6 +19,7 @@
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
 
 import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
 
@@ -52,6 +53,14 @@
         return signature() != null && signature().length() > 0;
     }
 
+    boolean isSuccessful() {
+        return state() == SUCCESS;
+    }
+
+    static LogListUpdateStatus getDefaultInstance() {
+        return builder().build();
+    }
+
     @AutoValue.Builder
     abstract static class Builder {
         abstract Builder setState(CTLogListUpdateState updateState);
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 08704d1..5443298 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -84,6 +84,7 @@
     private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
 
     private long mNextDownloadId = 666;
+    private static final long LOG_LIST_TIMESTAMP = 123456789L;
 
     @Before
     public void setUp() throws IOException, GeneralSecurityException {
@@ -426,7 +427,7 @@
         verify(mLogger, times(1))
                 .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
         LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
-        assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.VERSION_ALREADY_EXISTS);
+        assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.LOG_LIST_INVALID);
         assertThat(statusValue.signature())
                 .isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
     }
@@ -466,7 +467,8 @@
     }
 
     @Test
-    public void testDownloader_endToEndSuccess_installNewVersion() throws Exception {
+    public void testDownloader_endToEndSuccess_installNewVersion_andLogsSuccess() throws Exception {
+        // Arrange
         String newVersion = "456";
         File logListFile = makeLogListFile(newVersion);
         File metadataFile = sign(logListFile);
@@ -474,6 +476,7 @@
 
         assertNoVersionIsInstalled();
 
+        // Act
         // 1. Start download of public key.
         mCertificateTransparencyDownloader.startPublicKeyDownload();
 
@@ -491,7 +494,19 @@
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
 
+        // Assert
         assertInstallSuccessful(newVersion);
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+
+        LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+        assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.SUCCESS);
+        assertThat(statusValue.signature())
+                .isEqualTo(
+                        new String(
+                                Base64.getDecoder().decode(toByteArray(metadataFile)),
+                                StandardCharsets.UTF_8));
+        assertThat(statusValue.logListTimestamp()).isEqualTo(LOG_LIST_TIMESTAMP);
     }
 
     private void assertNoVersionIsInstalled() {
@@ -600,7 +615,11 @@
         File logListFile = File.createTempFile("log_list", "json");
 
         try (OutputStream outputStream = new FileOutputStream(logListFile)) {
-            outputStream.write(new JSONObject().put("version", version).toString().getBytes(UTF_8));
+            JSONObject contentJson =
+                    new JSONObject()
+                            .put("version", version)
+                            .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
+            outputStream.write(contentJson.toString().getBytes(UTF_8));
         }
 
         return logListFile;
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
index 38fff48..2b8b3cd 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -15,6 +15,11 @@
  */
 package com.android.server.net.ct;
 
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.LOG_LIST_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.UNKNOWN_STATE;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.VERSION_ALREADY_EXISTS;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -37,6 +42,8 @@
 public class CompatibilityVersionTest {
 
     private static final String TEST_VERSION = "v123";
+    private static final long LOG_LIST_TIMESTAMP = 123456789L;
+    private static final String SIGNATURE = "fake_signature";
 
     private final File mTestDir =
             InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
@@ -52,6 +59,7 @@
     @Test
     public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
         File versionDir = mCompatVersion.getVersionDir();
+
         assertThat(versionDir.exists()).isFalse();
         assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
         assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
@@ -60,6 +68,7 @@
     @Test
     public void testCompatibilityVersion_symlink_setupSuccessful() {
         File dirSymlink = mCompatVersion.getLogsDirSymlink();
+
         assertThat(dirSymlink.exists()).isFalse();
         assertThat(dirSymlink.getAbsolutePath())
                 .startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
@@ -68,18 +77,44 @@
     @Test
     public void testCompatibilityVersion_logsFile_setupSuccessful() {
         File logsFile = mCompatVersion.getLogsFile();
+
         assertThat(logsFile.exists()).isFalse();
         assertThat(logsFile.getAbsolutePath())
                 .startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
     }
 
     @Test
+    public void testCompatibilityVersion_installSuccessful_keepsStatusDetails() throws Exception {
+        String version = "i_am_version";
+        JSONObject logList = makeLogList(version, "i_am_content");
+
+        try (InputStream inputStream = asStream(logList)) {
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream,
+                                    LogListUpdateStatus.builder()
+                                            .setSignature(SIGNATURE)
+                                            .setState(UNKNOWN_STATE)))
+                    .isEqualTo(
+                            LogListUpdateStatus.builder()
+                                    .setSignature(SIGNATURE)
+                                    .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+                                    // Ensure the state is correctly overridden to SUCCESS
+                                    .setState(SUCCESS)
+                                    .build());
+        }
+    }
+
+    @Test
     public void testCompatibilityVersion_installSuccessful() throws Exception {
         String version = "i_am_version";
         JSONObject logList = makeLogList(version, "i_am_content");
 
         try (InputStream inputStream = asStream(logList)) {
-            assertThat(mCompatVersion.install(inputStream)).isTrue();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
         }
 
         File logListFile = mCompatVersion.getLogsFile();
@@ -107,7 +142,10 @@
     @Test
     public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
         try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
-            assertThat(mCompatVersion.install(inputStream)).isTrue();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
         }
 
         mCompatVersion.delete();
@@ -118,7 +156,10 @@
     @Test
     public void testCompatibilityVersion_invalidLogList() throws Exception {
         try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
-            assertThat(mCompatVersion.install(inputStream)).isFalse();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(LogListUpdateStatus.builder().setState(LOG_LIST_INVALID).build());
         }
 
         assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
@@ -138,7 +179,10 @@
 
         JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
         try (InputStream inputStream = asStream(newLogList)) {
-            assertThat(mCompatVersion.install(inputStream)).isTrue();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
         }
 
         assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
@@ -149,11 +193,21 @@
         String existingVersion = "666";
         JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
         try (InputStream inputStream = asStream(existingLogList)) {
-            assertThat(mCompatVersion.install(inputStream)).isTrue();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
         }
 
         try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
-            assertThat(mCompatVersion.install(inputStream)).isFalse();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(
+                            LogListUpdateStatus.builder()
+                                    .setState(VERSION_ALREADY_EXISTS)
+                                    .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+                                    .build());
         }
 
         assertThat(readAsString(mCompatVersion.getLogsFile()))
@@ -165,13 +219,22 @@
     }
 
     private static JSONObject makeLogList(String version) throws JSONException {
-        return new JSONObject().put("version", version);
+        return new JSONObject()
+                .put("version", version)
+                .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
     }
 
     private static JSONObject makeLogList(String version, String content) throws JSONException {
         return makeLogList(version).put("content", content);
     }
 
+    private static LogListUpdateStatus getSuccessfulUpdateStatus() {
+        return LogListUpdateStatus.builder()
+                .setState(SUCCESS)
+                .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+                .build();
+    }
+
     private static String readAsString(File file) throws IOException {
         try (InputStream in = new FileInputStream(file)) {
             return new String(in.readAllBytes());