diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java index b789b38c966e6..760c6f0fc333f 100644 --- a/core/java/android/app/backup/BackupRestoreEventLogger.java +++ b/core/java/android/app/backup/BackupRestoreEventLogger.java @@ -19,10 +19,15 @@ package android.app.backup; 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 @@ import java.util.Map; * @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 class BackupRestoreEventLogger { public @interface BackupRestoreError {} private final int mOperationType; + private final Map 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 class BackupRestoreEventLogger { */ 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 @@ public class BackupRestoreEventLogger { * @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 class BackupRestoreEventLogger { */ 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 class BackupRestoreEventLogger { */ public boolean logBackupMetaData(@NonNull @BackupRestoreDataType String dataType, @NonNull String metaData) { - return true; + return logMetaData(OperationType.BACKUP, dataType, metaData); } /** @@ -159,7 +176,7 @@ public class BackupRestoreEventLogger { * @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 class BackupRestoreEventLogger { */ 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 class BackupRestoreEventLogger { */ public boolean logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType, @NonNull String metadata) { - return true; + return logMetaData(OperationType.RESTORE, dataType, metadata); } /** @@ -214,7 +231,7 @@ public class BackupRestoreEventLogger { * @hide */ public List getLoggingResults() { - return Collections.emptyList(); + return new ArrayList<>(mResults.values()); } /** @@ -227,22 +244,97 @@ public class BackupRestoreEventLogger { 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 mErrors; - private final byte[] mMetadataHash; + private int mSuccessCount; + private int mFailCount; + private final Map mErrors = new HashMap<>(); + private byte[] mMetadataHash; - public DataTypeResult(String dataType, int successCount, - Map errors, byte[] metadataHash) { + public DataTypeResult(String dataType) { mDataType = dataType; - mSuccessCount = successCount; - mErrors = errors; - mMetadataHash = metadataHash; } @NonNull @@ -259,6 +351,13 @@ public class BackupRestoreEventLogger { return mSuccessCount; } + /** + * @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 0000000000000..67b24ec17a270 --- /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 result = getResultForDataTypeIfPresent(logger, dataType); + if (result.isEmpty()) { + fail("Failed to find result for data type: " + dataType); + } + return result.get(); + } + + private static Optional getResultForDataTypeIfPresent( + BackupRestoreEventLogger logger, @BackupRestoreDataType String dataType) { + List 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)); + } +}