Add a 60 days retention period to BMM Events
Test: manual testing(set a 5 minutes retention period, factory reset the test device, go trough SUW, confirm that the logs show in backup dumpsys, confirm that logs are deleted after 5 minutes)
atest CtsBackupHostTestCases, GtsBackupHostTestCases
atest BackupManagerMonitorDumpsysUtilsTest, BackupManagerMonitorEventSenderTest, UserBackupManagerServiceTest
Bug: 297159898
Change-Id: If8ce57c68cf9ffaa5472e5e14aee72c6bf2e5153
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 4c137bc..2d80af9 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -654,6 +654,13 @@
// the pending backup set
mBackupHandler.postDelayed(this::parseLeftoverJournals, INITIALIZATION_DELAY_MILLIS);
+ // check if we are past the retention period for BMM Events,
+ // if so delete expired events and do not print them to dumpsys
+ BackupManagerMonitorDumpsysUtils backupManagerMonitorDumpsysUtils =
+ new BackupManagerMonitorDumpsysUtils();
+ mBackupHandler.postDelayed(backupManagerMonitorDumpsysUtils::deleteExpiredBMMEvents,
+ INITIALIZATION_DELAY_MILLIS);
+
mBackupPreferences = new UserBackupPreferences(mContext, mBaseStateDir);
// Power management
@@ -4181,7 +4188,16 @@
private void dumpBMMEvents(PrintWriter pw) {
BackupManagerMonitorDumpsysUtils bm =
new BackupManagerMonitorDumpsysUtils();
+ if (bm.deleteExpiredBMMEvents()) {
+ pw.println("BACKUP MANAGER MONITOR EVENTS HAVE EXPIRED");
+ return;
+ }
File events = bm.getBMMEventsFile();
+ if (events.length() == 0){
+ // We have not recorded BMMEvents yet.
+ pw.println("NO BACKUP MANAGER MONITOR EVENTS");
+ return;
+ }
pw.println("START OF BACKUP MANAGER MONITOR EVENTS");
try (BufferedReader reader = new BufferedReader(new FileReader(events))) {
String line;
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
index 0b55ca2..bc2326d 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
@@ -23,15 +23,22 @@
import android.os.Environment;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FastPrintWriter;
+import java.io.BufferedReader;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
/*
@@ -46,12 +53,21 @@
// Name of the subdirectory where the text file containing the BMM events will be stored.
// Same as {@link UserBackupManagerFiles}
private static final String BACKUP_PERSISTENT_DIR = "backup";
+ private static final String INITIAL_SETUP_TIMESTAMP_KEY = "initialSetupTimestamp";
+ // Retention period of 60 days (in millisec) for the BMM Events.
+ // After tha time has passed the text file containing the BMM events will be emptied
+ private static final long LOGS_RETENTION_PERIOD_MILLISEC = 60 * TimeUnit.DAYS.toMillis(1);
+ // We cache the value of IsAfterRetentionPeriod() to avoid unnecessary disk I/O
+ // mIsAfterRetentionPeriodCached tracks if we have cached the value of IsAfterRetentionPeriod()
+ private boolean mIsAfterRetentionPeriodCached = false;
+ // The cahched value of IsAfterRetentionPeriod()
+ private boolean mIsAfterRetentionPeriod;
/**
* Parses the BackupManagerMonitor bundle for a RESTORE event in a series of strings that
* will be persisted in a text file and printed in the dumpsys.
*
- * If the evenntBundle passed is not a RESTORE event, return early
+ * If the eventBundle passed is not a RESTORE event, return early
*
* Key information related to the event:
* - Timestamp (HAS TO ALWAYS BE THE FIRST LINE OF EACH EVENT)
@@ -63,16 +79,21 @@
*
* Example of formatting:
* RESTORE Event: [2023-08-18 17:16:00.735] Agent - Agent logging results
- * Package name: com.android.wallpaperbackup
- * Agent Logs:
- * Data Type: wlp_img_system
- * Item restored: 0/1
- * Agent Error - Category: no_wallpaper, Count: 1
- * Data Type: wlp_img_lock
- * Item restored: 0/1
- * Agent Error - Category: no_wallpaper, Count: 1
+ * Package name: com.android.wallpaperbackup
+ * Agent Logs:
+ * Data Type: wlp_img_system
+ * Item restored: 0/1
+ * Agent Error - Category: no_wallpaper, Count: 1
+ * Data Type: wlp_img_lock
+ * Item restored: 0/1
+ * Agent Error - Category: no_wallpaper, Count: 1
*/
public void parseBackupManagerMonitorRestoreEventForDumpsys(Bundle eventBundle) {
+ if (isAfterRetentionPeriod()) {
+ // We only log data for the first 60 days since setup
+ return;
+ }
+
if (eventBundle == null) {
return;
}
@@ -89,8 +110,14 @@
}
File bmmEvents = getBMMEventsFile();
+ if (bmmEvents.length() == 0) {
+ // We are parsing the first restore event.
+ // Time to also record the setup timestamp of the device
+ recordSetUpTimestamp();
+ }
+
try (FileOutputStream out = new FileOutputStream(bmmEvents, /*append*/ true);
- PrintWriter pw = new FastPrintWriter(out);) {
+ PrintWriter pw = new FastPrintWriter(out);) {
int eventCategory = eventBundle.getInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY);
int eventId = eventBundle.getInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID);
@@ -257,4 +284,123 @@
default -> false;
};
}
+
+ /**
+ * Store the timestamp when the device was set up (date when the first BMM event is parsed)
+ * in a text file.
+ */
+ @VisibleForTesting
+ void recordSetUpTimestamp() {
+ File setupDateFile = getSetUpDateFile();
+ // record setup timestamp only once
+ if (setupDateFile.length() == 0) {
+ try (FileOutputStream out = new FileOutputStream(setupDateFile, /*append*/ true);
+ PrintWriter pw = new FastPrintWriter(out);) {
+ long currentDate = System.currentTimeMillis();
+ pw.println(currentDate);
+ } catch (IOException e) {
+ Slog.w(TAG, "An error occurred while recording the setup date: "
+ + e.getMessage());
+ }
+ }
+
+ }
+
+ @VisibleForTesting
+ String getSetUpDate() {
+ File fname = getSetUpDateFile();
+ try (FileInputStream inputStream = new FileInputStream(fname);
+ InputStreamReader reader = new InputStreamReader(inputStream);
+ BufferedReader bufferedReader = new BufferedReader(reader);) {
+ return bufferedReader.readLine();
+ } catch (Exception e) {
+ Slog.w(TAG, "An error occurred while reading the date: " + e.getMessage());
+ return "Could not retrieve setup date";
+ }
+ }
+
+ @VisibleForTesting
+ static boolean isDateAfterNMillisec(long startTimeStamp, long endTimeStamp, long millisec) {
+ if (startTimeStamp > endTimeStamp) {
+ // Something has gone wrong, timeStamp1 should always precede timeStamp2.
+ // Out of caution return true: we would delete the logs rather than
+ // risking them being kept for longer than the retention period
+ return true;
+ }
+ long timeDifferenceMillis = endTimeStamp - startTimeStamp;
+ return (timeDifferenceMillis >= millisec);
+ }
+
+ /**
+ * Check if current date is after retention period
+ */
+ @VisibleForTesting
+ boolean isAfterRetentionPeriod() {
+ if (mIsAfterRetentionPeriodCached) {
+ return mIsAfterRetentionPeriod;
+ } else {
+ File setUpDateFile = getSetUpDateFile();
+ if (setUpDateFile.length() == 0) {
+ // We are yet to record a setup date. This means we haven't parsed the first event.
+ mIsAfterRetentionPeriod = false;
+ mIsAfterRetentionPeriodCached = true;
+ return false;
+ }
+ try {
+ long setupTimestamp = Long.parseLong(getSetUpDate());
+ long currentTimestamp = System.currentTimeMillis();
+ mIsAfterRetentionPeriod = isDateAfterNMillisec(setupTimestamp, currentTimestamp,
+ getRetentionPeriodInMillisec());
+ mIsAfterRetentionPeriodCached = true;
+ return mIsAfterRetentionPeriod;
+ } catch (NumberFormatException e) {
+ // An error occurred when parsing the setup timestamp.
+ // Out of caution return true: we would delete the logs rather than
+ // risking them being kept for longer than the retention period
+ mIsAfterRetentionPeriod = true;
+ mIsAfterRetentionPeriodCached = true;
+ return true;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ File getSetUpDateFile() {
+ File dataDir = new File(Environment.getDataDirectory(), BACKUP_PERSISTENT_DIR);
+ File setupDateFile = new File(dataDir, INITIAL_SETUP_TIMESTAMP_KEY + ".txt");
+ return setupDateFile;
+ }
+
+ @VisibleForTesting
+ long getRetentionPeriodInMillisec() {
+ return LOGS_RETENTION_PERIOD_MILLISEC;
+ }
+
+ /**
+ * Delete the BMM Events file after the retention period has passed.
+ *
+ * @return true if the retention period has passed false otherwise.
+ * we want to return true even if we were unable to delete the file, as this will prevent
+ * expired BMM events from being printed to the dumpsys
+ */
+ public boolean deleteExpiredBMMEvents() {
+ try {
+ if (isAfterRetentionPeriod()) {
+ File bmmEvents = getBMMEventsFile();
+ if (bmmEvents.exists()) {
+ if (bmmEvents.delete()) {
+ Slog.i(TAG, "Deleted expired BMM Events");
+ } else {
+ Slog.e(TAG, "Unable to delete expired BMM Events");
+ }
+ }
+ return true;
+ }
+ return false;
+ } catch (Exception e) {
+ // Handle any unexpected exceptions
+ // To be safe we return true as we want to avoid exposing expired BMMEvents
+ return true;
+ }
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java
index 8e17b3a..a45b17e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java
@@ -17,26 +17,29 @@
package com.android.server.backup.utils;
import static org.junit.Assert.assertTrue;
-
+import static org.testng.AssertJUnit.assertFalse;
+import android.app.backup.BackupAnnotations;
import android.app.backup.BackupManagerMonitor;
import android.os.Bundle;
-
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
-
import java.io.File;
public class BackupManagerMonitorDumpsysUtilsTest {
- private File mTempFile;
+ private long mRetentionPeriod;
+ private File mTempBMMEventsFile;
+ private File mTempSetUpDateFile;
private TestBackupManagerMonitorDumpsysUtils mBackupManagerMonitorDumpsysUtils;
@Rule
public TemporaryFolder tmp = new TemporaryFolder();
@Before
public void setUp() throws Exception {
- mTempFile = tmp.newFile("testbmmevents.txt");
+ mRetentionPeriod = 30 * 60 * 1000;
+ mTempBMMEventsFile = tmp.newFile("testbmmevents.txt");
+ mTempSetUpDateFile = tmp.newFile("testSetUpDate.txt");
mBackupManagerMonitorDumpsysUtils = new TestBackupManagerMonitorDumpsysUtils();
}
@@ -46,7 +49,7 @@
throws Exception {
mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(null);
- assertTrue(mTempFile.length() == 0);
+ assertTrue(mTempBMMEventsFile.length() == 0);
}
@@ -57,7 +60,7 @@
event.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, 1);
mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event);
- assertTrue(mTempFile.length() == 0);
+ assertTrue(mTempBMMEventsFile.length() == 0);
}
@Test
@@ -67,18 +70,198 @@
event.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, 1);
mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event);
- assertTrue(mTempFile.length() == 0);
+ assertTrue(mTempBMMEventsFile.length() == 0);
+ }
+
+ @Test
+ public void parseBackupManagerMonitorEventForDumpsys_eventWithCategoryAndId_eventIsWrittenToFile()
+ throws Exception {
+ Bundle event = createRestoreBMMEvent();
+ mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event);
+
+ assertTrue(mTempBMMEventsFile.length() != 0);
+ }
+
+ @Test
+ public void parseBackupManagerMonitorEventForDumpsys_firstEvent_recordSetUpTimestamp()
+ throws Exception {
+ assertTrue(mTempBMMEventsFile.length()==0);
+ assertTrue(mTempSetUpDateFile.length()==0);
+
+ Bundle event = createRestoreBMMEvent();
+ mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event);
+
+ assertTrue(mTempBMMEventsFile.length() != 0);
+ assertTrue(mTempSetUpDateFile.length()!=0);
+ }
+
+ @Test
+ public void parseBackupManagerMonitorEventForDumpsys_notFirstEvent_doNotChangeSetUpTimestamp()
+ throws Exception {
+ Bundle event1 = createRestoreBMMEvent();
+ mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event1);
+ String setUpTimestampBefore = mBackupManagerMonitorDumpsysUtils.getSetUpDate();
+
+ Bundle event2 = createRestoreBMMEvent();
+ mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event2);
+ String setUpTimestampAfter = mBackupManagerMonitorDumpsysUtils.getSetUpDate();
+
+ assertTrue(setUpTimestampBefore.equals(setUpTimestampAfter));
+ }
+
+
+ @Test
+ public void deleteExpiredBackupManagerMonitorEvent_eventsAreExpired_deleteEventsAndReturnTrue()
+ throws Exception {
+ Bundle event = createRestoreBMMEvent();
+ mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event);
+ assertTrue(mTempBMMEventsFile.length() != 0);
+ // Re-initialise the test BackupManagerMonitorDumpsysUtils to
+ // clear the cached value of isAfterRetentionPeriod
+ mBackupManagerMonitorDumpsysUtils = new TestBackupManagerMonitorDumpsysUtils();
+
+ // set a retention period of 0 second
+ mBackupManagerMonitorDumpsysUtils.setTestRetentionPeriod(0);
+
+ assertTrue(mBackupManagerMonitorDumpsysUtils.deleteExpiredBMMEvents());
+ assertFalse(mTempBMMEventsFile.exists());
+ }
+
+ @Test
+ public void deleteExpiredBackupManagerMonitorEvent_eventsAreNotExpired_returnFalse() throws
+ Exception {
+ Bundle event = createRestoreBMMEvent();
+ mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event);
+ assertTrue(mTempBMMEventsFile.length() != 0);
+
+ // set a retention period of 30 minutes
+ mBackupManagerMonitorDumpsysUtils.setTestRetentionPeriod(30 * 60 * 1000);
+
+ assertFalse(mBackupManagerMonitorDumpsysUtils.deleteExpiredBMMEvents());
+ assertTrue(mTempBMMEventsFile.length() != 0);
+ }
+
+ @Test
+ public void isAfterRetentionPeriod_afterRetentionPeriod_returnTrue() throws
+ Exception {
+ mBackupManagerMonitorDumpsysUtils.recordSetUpTimestamp();
+
+ // set a retention period of 0 second
+ mBackupManagerMonitorDumpsysUtils.setTestRetentionPeriod(0);
+
+ assertTrue(mBackupManagerMonitorDumpsysUtils.isAfterRetentionPeriod());
+ }
+
+ @Test
+ public void isAfterRetentionPeriod_beforeRetentionPeriod_returnFalse() throws
+ Exception {
+ mBackupManagerMonitorDumpsysUtils.recordSetUpTimestamp();
+
+ // set a retention period of 30 minutes
+ mBackupManagerMonitorDumpsysUtils.setTestRetentionPeriod(30 * 60 * 1000);
+
+ assertFalse(mBackupManagerMonitorDumpsysUtils.isAfterRetentionPeriod());
+ }
+
+ @Test
+ public void isAfterRetentionPeriod_noSetupDate_returnFalse() throws
+ Exception {
+ assertTrue(mTempSetUpDateFile.length() == 0);
+
+ assertFalse(mBackupManagerMonitorDumpsysUtils.isAfterRetentionPeriod());
+ }
+
+ @Test
+ public void isDateAfterNMillisec_date1IsAfterThanDate2_returnTrue() throws
+ Exception {
+ long timestamp1 = System.currentTimeMillis();
+ long timestamp2 = timestamp1 - 1;
+
+ assertTrue(mBackupManagerMonitorDumpsysUtils.isDateAfterNMillisec(timestamp1, timestamp2,
+ 0));
+ }
+
+ @Test
+ public void isDateAfterNMillisec_date1IsAfterNMillisecFromDate2_returnTrue() throws
+ Exception {
+ long timestamp1 = System.currentTimeMillis();
+ long timestamp2 = timestamp1 + 10;
+
+ assertTrue(mBackupManagerMonitorDumpsysUtils.isDateAfterNMillisec(timestamp1, timestamp2,
+ 10));
+ }
+
+ @Test
+ public void isDateAfterNMillisec_date1IsLessThanNMillisecFromDate2_returnFalse() throws
+ Exception {
+ long timestamp1 = System.currentTimeMillis();
+ long timestamp2 = timestamp1 + 10;
+
+ assertFalse(mBackupManagerMonitorDumpsysUtils.isDateAfterNMillisec(timestamp1, timestamp2,
+ 11));
+ }
+
+ @Test
+ public void recordSetUpTimestamp_timestampNotSetBefore_setTimestamp() throws
+ Exception {
+ assertTrue(mTempSetUpDateFile.length() == 0);
+
+ mBackupManagerMonitorDumpsysUtils.recordSetUpTimestamp();
+
+ assertTrue(mTempSetUpDateFile.length() != 0);
+ }
+
+ @Test
+ public void recordSetUpTimestamp_timestampSetBefore_doNothing() throws
+ Exception {
+ mBackupManagerMonitorDumpsysUtils.recordSetUpTimestamp();
+ assertTrue(mTempSetUpDateFile.length() != 0);
+ String timestampBefore = mBackupManagerMonitorDumpsysUtils.getSetUpDate();
+
+ mBackupManagerMonitorDumpsysUtils.recordSetUpTimestamp();
+
+ assertTrue(mTempSetUpDateFile.length() != 0);
+ String timestampAfter = mBackupManagerMonitorDumpsysUtils.getSetUpDate();
+ assertTrue(timestampAfter.equals(timestampBefore));
+ }
+
+ private Bundle createRestoreBMMEvent() {
+ Bundle event = new Bundle();
+ event.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, 1);
+ event.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, 1);
+ event.putInt(BackupManagerMonitor.EXTRA_LOG_OPERATION_TYPE,
+ BackupAnnotations.OperationType.RESTORE);
+ return event;
}
private class TestBackupManagerMonitorDumpsysUtils
extends BackupManagerMonitorDumpsysUtils {
+
+ private long testRetentionPeriod;
+
TestBackupManagerMonitorDumpsysUtils() {
super();
+ this.testRetentionPeriod = mRetentionPeriod;
+ }
+
+ public void setTestRetentionPeriod(long testRetentionPeriod) {
+ this.testRetentionPeriod = testRetentionPeriod;
}
@Override
public File getBMMEventsFile() {
- return mTempFile;
+ return mTempBMMEventsFile;
}
+
+ @Override
+ File getSetUpDateFile() {
+ return mTempSetUpDateFile;
+ }
+
+ @Override
+ long getRetentionPeriodInMillisec() {
+ return testRetentionPeriod;
+ }
+
}
}