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;
+        }
+
     }
 }