Add implementation for BackupRestoreEventLogger

Bug: 252762060
Test: atest BackupRestoreEventLoggerTest
Change-Id: If5516b873b5a0562c819d45270c27187280da3e6
diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java
index b789b38..760c6f0 100644
--- a/core/java/android/app/backup/BackupRestoreEventLogger.java
+++ b/core/java/android/app/backup/BackupRestoreEventLogger.java
@@ -19,10 +19,15 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.util.Slog;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Collections;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -38,6 +43,8 @@
  * @hide
  */
 public class BackupRestoreEventLogger {
+    private static final String TAG = "BackupRestoreEventLogger";
+
     /**
      * Max number of unique data types for which an instance of this logger can store info. Attempts
      * to use more distinct data type values will be rejected.
@@ -72,6 +79,8 @@
     public @interface BackupRestoreError {}
 
     private final int mOperationType;
+    private final Map<String, DataTypeResult> mResults = new HashMap<>();
+    private final MessageDigest mHashDigest;
 
     /**
      * @param operationType type of the operation for which logging will be performed. See
@@ -81,6 +90,14 @@
      */
     public BackupRestoreEventLogger(@OperationType int operationType) {
         mOperationType = operationType;
+
+        MessageDigest hashDigest = null;
+        try {
+            hashDigest = MessageDigest.getInstance("SHA-256");
+        } catch (NoSuchAlgorithmException e) {
+            Slog.w("Couldn't create MessageDigest for hash computation", e);
+        }
+        mHashDigest = hashDigest;
     }
 
     /**
@@ -98,7 +115,7 @@
      * @return boolean, indicating whether the log has been accepted.
      */
     public boolean logItemsBackedUp(@NonNull @BackupRestoreDataType String dataType, int count) {
-        return true;
+        return logSuccess(OperationType.BACKUP, dataType, count);
     }
 
     /**
@@ -118,7 +135,7 @@
      */
     public boolean logItemsBackupFailed(@NonNull @BackupRestoreDataType String dataType, int count,
             @Nullable @BackupRestoreError String error) {
-        return true;
+        return logFailure(OperationType.BACKUP, dataType, count, error);
     }
 
     /**
@@ -139,7 +156,7 @@
      */
     public boolean logBackupMetaData(@NonNull @BackupRestoreDataType String dataType,
             @NonNull String metaData) {
-        return true;
+        return logMetaData(OperationType.BACKUP, dataType, metaData);
     }
 
     /**
@@ -159,7 +176,7 @@
      * @return boolean, indicating whether the log has been accepted.
      */
     public boolean logItemsRestored(@NonNull @BackupRestoreDataType String dataType, int count) {
-        return true;
+        return logSuccess(OperationType.RESTORE, dataType, count);
     }
 
     /**
@@ -181,7 +198,7 @@
      */
     public boolean logItemsRestoreFailed(@NonNull @BackupRestoreDataType String dataType, int count,
             @Nullable @BackupRestoreError String error) {
-        return true;
+        return logFailure(OperationType.RESTORE, dataType, count, error);
     }
 
     /**
@@ -204,7 +221,7 @@
      */
     public boolean logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType,
             @NonNull  String metadata) {
-        return true;
+        return logMetaData(OperationType.RESTORE, dataType, metadata);
     }
 
     /**
@@ -214,7 +231,7 @@
      * @hide
      */
     public List<DataTypeResult> getLoggingResults() {
-        return Collections.emptyList();
+        return new ArrayList<>(mResults.values());
     }
 
     /**
@@ -227,22 +244,97 @@
         return mOperationType;
     }
 
+    private boolean logSuccess(@OperationType int operationType,
+            @BackupRestoreDataType String dataType, int count) {
+        DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType);
+        if (dataTypeResult == null) {
+            return false;
+        }
+
+        dataTypeResult.mSuccessCount += count;
+        mResults.put(dataType, dataTypeResult);
+
+        return true;
+    }
+
+    private boolean logFailure(@OperationType int operationType,
+            @NonNull @BackupRestoreDataType String dataType, int count,
+            @Nullable @BackupRestoreError String error) {
+        DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType);
+        if (dataTypeResult == null) {
+            return false;
+        }
+
+        dataTypeResult.mFailCount += count;
+        if (error != null) {
+            dataTypeResult.mErrors.merge(error, count, Integer::sum);
+        }
+
+        return true;
+    }
+
+    private boolean logMetaData(@OperationType int operationType,
+            @NonNull @BackupRestoreDataType String dataType, @NonNull String metaData) {
+        if (mHashDigest == null) {
+            return false;
+        }
+        DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType);
+        if (dataTypeResult == null) {
+            return false;
+        }
+
+        dataTypeResult.mMetadataHash = getMetaDataHash(metaData);
+
+        return true;
+    }
+
+    /**
+     * Get the result container for the given data type.
+     *
+     * @return {@code DataTypeResult} object corresponding to the given {@code dataType} or
+     *         {@code null} if the logger can't accept logs for the given data type.
+     */
+    @Nullable
+    private DataTypeResult getDataTypeResult(@OperationType int operationType,
+            @BackupRestoreDataType String dataType) {
+        if (operationType != mOperationType) {
+            // Operation type for which we're trying to record logs doesn't match the operation
+            // type for which this logger instance was created.
+            Slog.d(TAG, "Operation type mismatch: logger created for " + mOperationType
+                    + ", trying to log for " + operationType);
+            return null;
+        }
+
+        if (!mResults.containsKey(dataType)) {
+            if (mResults.keySet().size() == DATA_TYPES_ALLOWED) {
+                // This is a new data type and we're already at capacity.
+                Slog.d(TAG, "Logger is full, ignoring new data type");
+                return null;
+            }
+
+            mResults.put(dataType,  new DataTypeResult(dataType));
+        }
+
+        return mResults.get(dataType);
+    }
+
+    private byte[] getMetaDataHash(String metaData) {
+        return mHashDigest.digest(metaData.getBytes(StandardCharsets.UTF_8));
+    }
+
     /**
      * Encapsulate logging results for a single data type.
      */
     public static class DataTypeResult {
         @BackupRestoreDataType
         private final String mDataType;
-        private final int mSuccessCount;
-        private final Map<String, Integer> mErrors;
-        private final byte[] mMetadataHash;
+        private int mSuccessCount;
+        private int mFailCount;
+        private final Map<String, Integer> mErrors = new HashMap<>();
+        private byte[] mMetadataHash;
 
-        public DataTypeResult(String dataType, int successCount,
-                Map<String, Integer> errors, byte[] metadataHash) {
+        public DataTypeResult(String dataType) {
             mDataType = dataType;
-            mSuccessCount = successCount;
-            mErrors = errors;
-            mMetadataHash = metadataHash;
         }
 
         @NonNull
@@ -260,6 +352,13 @@
         }
 
         /**
+         * @return number of items of the given data type that have failed to back up or restore.
+         */
+        public int getFailCount() {
+            return mFailCount;
+        }
+
+        /**
          * @return mapping of {@link BackupRestoreError} to the count of items that are affected by
          *         the error.
          */
diff --git a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
new file mode 100644
index 0000000..67b24ec
--- /dev/null
+++ b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import static android.app.backup.BackupRestoreEventLogger.OperationType.BACKUP;
+import static android.app.backup.BackupRestoreEventLogger.OperationType.RESTORE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.fail;
+
+import android.app.backup.BackupRestoreEventLogger.BackupRestoreDataType;
+import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class BackupRestoreEventLoggerTest {
+    private static final int DATA_TYPES_ALLOWED = 15;
+
+    private static final String DATA_TYPE_1 = "data_type_1";
+    private static final String DATA_TYPE_2 = "data_type_2";
+    private static final String ERROR_1 = "error_1";
+    private static final String ERROR_2 = "error_2";
+    private static final String METADATA_1 = "metadata_1";
+    private static final String METADATA_2 = "metadata_2";
+
+    private BackupRestoreEventLogger mLogger;
+    private MessageDigest mHashDigest;
+
+    @Before
+    public void setUp() throws Exception {
+        mHashDigest = MessageDigest.getInstance("SHA-256");
+    }
+
+    @Test
+    public void testBackupLogger_rejectsRestoreLogs() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        assertThat(mLogger.logItemsRestored(DATA_TYPE_1, /* count */ 5)).isFalse();
+        assertThat(mLogger.logItemsRestoreFailed(DATA_TYPE_1, /* count */ 5, ERROR_1)).isFalse();
+        assertThat(mLogger.logRestoreMetadata(DATA_TYPE_1, /* metadata */ "metadata")).isFalse();
+    }
+
+    @Test
+    public void testRestoreLogger_rejectsBackupLogs() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        assertThat(mLogger.logItemsBackedUp(DATA_TYPE_1, /* count */ 5)).isFalse();
+        assertThat(mLogger.logItemsBackupFailed(DATA_TYPE_1, /* count */ 5, ERROR_1)).isFalse();
+        assertThat(mLogger.logBackupMetaData(DATA_TYPE_1, /* metadata */ "metadata")).isFalse();
+    }
+
+    @Test
+    public void testBackupLogger_onlyAcceptsAllowedNumberOfDataTypes() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        for (int i = 0; i < DATA_TYPES_ALLOWED; i++) {
+            String dataType = DATA_TYPE_1 + i;
+            assertThat(mLogger.logItemsBackedUp(dataType, /* count */ 5)).isTrue();
+            assertThat(mLogger.logItemsBackupFailed(dataType, /* count */ 5, /* error */ null))
+                    .isTrue();
+            assertThat(mLogger.logBackupMetaData(dataType, METADATA_1)).isTrue();
+        }
+
+        assertThat(mLogger.logItemsBackedUp(DATA_TYPE_2, /* count */ 5)).isFalse();
+        assertThat(mLogger.logItemsBackupFailed(DATA_TYPE_2, /* count */ 5, /* error */ null))
+                .isFalse();
+        assertThat(mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1)).isFalse();
+        assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty());
+    }
+
+    @Test
+    public void testRestoreLogger_onlyAcceptsAllowedNumberOfDataTypes() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        for (int i = 0; i < DATA_TYPES_ALLOWED; i++) {
+            String dataType = DATA_TYPE_1 + i;
+            assertThat(mLogger.logItemsRestored(dataType, /* count */ 5)).isTrue();
+            assertThat(mLogger.logItemsRestoreFailed(dataType, /* count */ 5, /* error */ null))
+                    .isTrue();
+            assertThat(mLogger.logRestoreMetadata(dataType, METADATA_1)).isTrue();
+        }
+
+        assertThat(mLogger.logItemsRestored(DATA_TYPE_2, /* count */ 5)).isFalse();
+        assertThat(mLogger.logItemsRestoreFailed(DATA_TYPE_2, /* count */ 5, /* error */ null))
+                .isFalse();
+        assertThat(mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1)).isFalse();
+        assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty());
+    }
+
+    @Test
+    public void testLogBackupMetadata_repeatedCalls_recordsLatestMetadataHash() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        mLogger.logBackupMetaData(DATA_TYPE_1, METADATA_1);
+        mLogger.logBackupMetaData(DATA_TYPE_1, METADATA_2);
+
+        byte[] recordedHash = getResultForDataType(mLogger, DATA_TYPE_1).getMetadataHash();
+        byte[] expectedHash = getMetaDataHash(METADATA_2);
+        assertThat(Arrays.equals(recordedHash, expectedHash)).isTrue();
+    }
+
+    @Test
+    public void testLogRestoreMetadata_repeatedCalls_recordsLatestMetadataHash() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        mLogger.logRestoreMetadata(DATA_TYPE_1, METADATA_1);
+        mLogger.logRestoreMetadata(DATA_TYPE_1, METADATA_2);
+
+        byte[] recordedHash = getResultForDataType(mLogger, DATA_TYPE_1).getMetadataHash();
+        byte[] expectedHash = getMetaDataHash(METADATA_2);
+        assertThat(Arrays.equals(recordedHash, expectedHash)).isTrue();
+    }
+
+    @Test
+    public void testLogItemsBackedUp_repeatedCalls_recordsTotalItems() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsBackedUp(DATA_TYPE_1, firstCount);
+        mLogger.logItemsBackedUp(DATA_TYPE_1, secondCount);
+
+        int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount();
+        assertThat(dataTypeCount).isEqualTo(firstCount + secondCount);
+    }
+
+    @Test
+    public void testLogItemsRestored_repeatedCalls_recordsTotalItems() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsRestored(DATA_TYPE_1, firstCount);
+        mLogger.logItemsRestored(DATA_TYPE_1, secondCount);
+
+        int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount();
+        assertThat(dataTypeCount).isEqualTo(firstCount + secondCount);
+    }
+
+    @Test
+    public void testLogItemsBackedUp_multipleDataTypes_recordsEachDataType() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsBackedUp(DATA_TYPE_1, firstCount);
+        mLogger.logItemsBackedUp(DATA_TYPE_2, secondCount);
+
+        int firstDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount();
+        int secondDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_2).getSuccessCount();
+        assertThat(firstDataTypeCount).isEqualTo(firstCount);
+        assertThat(secondDataTypeCount).isEqualTo(secondCount);
+    }
+
+    @Test
+    public void testLogItemsRestored_multipleDataTypes_recordsEachDataType() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsRestored(DATA_TYPE_1, firstCount);
+        mLogger.logItemsRestored(DATA_TYPE_2, secondCount);
+
+        int firstDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount();
+        int secondDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_2).getSuccessCount();
+        assertThat(firstDataTypeCount).isEqualTo(firstCount);
+        assertThat(secondDataTypeCount).isEqualTo(secondCount);
+    }
+
+    @Test
+    public void testLogItemsBackupFailed_repeatedCalls_recordsTotalItems() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsBackupFailed(DATA_TYPE_1, firstCount, /* error */ null);
+        mLogger.logItemsBackupFailed(DATA_TYPE_1, secondCount, "error");
+
+        int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getFailCount();
+        assertThat(dataTypeCount).isEqualTo(firstCount + secondCount);
+    }
+
+    @Test
+    public void testLogItemsRestoreFailed_repeatedCalls_recordsTotalItems() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstCount, /* error */ null);
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, secondCount, "error");
+
+        int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getFailCount();
+        assertThat(dataTypeCount).isEqualTo(firstCount + secondCount);
+    }
+
+    @Test
+    public void testLogItemsBackupFailed_multipleErrors_recordsEachError() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsBackupFailed(DATA_TYPE_1, firstCount, ERROR_1);
+        mLogger.logItemsBackupFailed(DATA_TYPE_1, secondCount, ERROR_2);
+
+        int firstErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1)
+                .getErrors().get(ERROR_1);
+        int secondErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1)
+                .getErrors().get(ERROR_2);
+        assertThat(firstErrorTypeCount).isEqualTo(firstCount);
+        assertThat(secondErrorTypeCount).isEqualTo(secondCount);
+    }
+
+    @Test
+    public void testLogItemsRestoreFailed_multipleErrors_recordsEachError() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstCount, ERROR_1);
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, secondCount, ERROR_2);
+
+        int firstErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1)
+                .getErrors().get(ERROR_1);
+        int secondErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1)
+                .getErrors().get(ERROR_2);
+        assertThat(firstErrorTypeCount).isEqualTo(firstCount);
+        assertThat(secondErrorTypeCount).isEqualTo(secondCount);
+    }
+
+    private static DataTypeResult getResultForDataType(BackupRestoreEventLogger logger,
+            @BackupRestoreDataType String dataType) {
+        Optional<DataTypeResult> result = getResultForDataTypeIfPresent(logger, dataType);
+        if (result.isEmpty()) {
+            fail("Failed to find result for data type: " + dataType);
+        }
+        return result.get();
+    }
+
+    private static Optional<DataTypeResult> getResultForDataTypeIfPresent(
+            BackupRestoreEventLogger logger, @BackupRestoreDataType String dataType) {
+        List<DataTypeResult> resultList = logger.getLoggingResults();
+        return resultList.stream().filter(
+                dataTypeResult -> dataTypeResult.getDataType().equals(dataType)).findAny();
+    }
+
+    private byte[] getMetaDataHash(String metaData) {
+        return mHashDigest.digest(metaData.getBytes(StandardCharsets.UTF_8));
+    }
+}