[SafetyCenter] Populate SafetyCenterStatus.RefreshStatus using data from
the SafetyCenterRefreshTracker

Bug: 229189269
Test: atest CtsSafetyCenterTestCases

Change-Id: Ifbcaca4c6ee21e10c8209ed5759a78b9691ac23e
diff --git a/service/java/com/android/safetycenter/SafetyCenterDataTracker.java b/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
index d0f5f8f..e344bf5 100644
--- a/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
+++ b/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
@@ -527,8 +527,6 @@
         }
 
         // TODO(b/223349473): Reorder safetyCenterIssues based on some criteria.
-        // TODO(b/229189269): Populate refresh status in SafetyCenterStatus using data from the
-        // SafetyCenterRefreshTracker.
         int safetyCenterOverallSeverityLevel =
                 entryToSafetyCenterStatusOverallLevel(maxSafetyCenterEntryLevel);
         return new SafetyCenterData(
@@ -536,6 +534,7 @@
                                 getSafetyCenterStatusTitle(safetyCenterOverallSeverityLevel),
                                 getSafetyCenterStatusSummary(safetyCenterOverallSeverityLevel))
                         .setSeverityLevel(safetyCenterOverallSeverityLevel)
+                        .setRefreshStatus(mSafetyCenterRefreshTracker.getRefreshStatus())
                         .build(),
                 safetyCenterIssues,
                 safetyCenterEntryOrGroups,
diff --git a/service/java/com/android/safetycenter/SafetyCenterRefreshTracker.java b/service/java/com/android/safetycenter/SafetyCenterRefreshTracker.java
index 14cce55..50b1a10 100644
--- a/service/java/com/android/safetycenter/SafetyCenterRefreshTracker.java
+++ b/service/java/com/android/safetycenter/SafetyCenterRefreshTracker.java
@@ -17,11 +17,14 @@
 package com.android.safetycenter;
 
 import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_RESCAN_BUTTON_CLICK;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.safetycenter.SafetyCenterManager.RefreshReason;
+import android.safetycenter.SafetyCenterStatus;
+import android.safetycenter.SafetyCenterStatus.RefreshStatus;
 import android.util.ArraySet;
 import android.util.Log;
 
@@ -75,7 +78,14 @@
         List<Broadcast> broadcasts = mSafetyCenterConfigReader.getBroadcasts();
         String refreshBroadcastId =
                 Objects.hash(refreshReason, broadcasts, userProfileGroup) + "_" + mRefreshCounter++;
-        mRefreshInProgress = new RefreshInProgress(refreshBroadcastId);
+        Log.v(
+                TAG,
+                "Starting a new refresh with refreshReason:"
+                        + refreshReason
+                        + " refreshBroadcastId:"
+                        + refreshBroadcastId);
+
+        mRefreshInProgress = new RefreshInProgress(refreshBroadcastId, refreshReason);
 
         for (int i = 0; i < broadcasts.size(); i++) {
             Broadcast broadcast = broadcasts.get(i);
@@ -102,6 +112,19 @@
         return refreshBroadcastId;
     }
 
+    /** Returns the current refresh status. */
+    @RefreshStatus
+    int getRefreshStatus() {
+        if (mRefreshInProgress == null) {
+            return SafetyCenterStatus.REFRESH_STATUS_NONE;
+        }
+
+        if (mRefreshInProgress.getReason() == REFRESH_REASON_RESCAN_BUTTON_CLICK) {
+            return SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS;
+        }
+        return SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS;
+    }
+
     /**
      * Reports that a source has completed its refresh, and returns whether this caused the refresh
      * to complete.
@@ -116,6 +139,7 @@
         }
 
         mRefreshInProgress.markSourceRefreshAsComplete(SafetySourceKey.of(sourceId, userId));
+
         if (!mRefreshInProgress.isComplete()) {
             return false;
         }
@@ -134,7 +158,10 @@
     // TODO(b/229188900): Should we stop any scheduled broadcasts from going out?
     void clearRefresh() {
         if (mRefreshInProgress != null) {
+            Log.v(TAG, "Clearing refresh with refreshBroadcastId:" + mRefreshInProgress.getId());
             mRefreshInProgress = null;
+        } else {
+            Log.v(TAG, "Clear refresh called but no refresh in progress");
         }
     }
 
@@ -151,6 +178,7 @@
             return false;
         }
 
+        Log.v(TAG, "Clearing refresh with refreshBroadcastId:" + refreshBroadcastId);
         mRefreshInProgress = null;
         return true;
     }
@@ -173,12 +201,14 @@
     /** Class representing the state of a refresh in progress. */
     private static final class RefreshInProgress {
         @NonNull private final String mId;
+        @RefreshReason private final int mReason;
 
         @NonNull private final ArraySet<SafetySourceKey> mSourceRefreshInFlight = new ArraySet<>();
 
         /** Creates a {@link RefreshInProgress}. */
-        RefreshInProgress(@NonNull String id) {
+        RefreshInProgress(@NonNull String id, @RefreshReason int reason) {
             mId = id;
+            mReason = reason;
         }
 
         /**
@@ -191,12 +221,40 @@
             return mId;
         }
 
+        /** Returns the {@link RefreshReason} that was given for this {@link RefreshInProgress}. */
+        @RefreshReason
+        int getReason() {
+            return mReason;
+        }
+
         private void addSourceRefreshInFlight(@NonNull SafetySourceKey safetySourceKey) {
             mSourceRefreshInFlight.add(safetySourceKey);
+            Log.v(
+                    TAG,
+                    "Refresh started for sourceId:"
+                            + safetySourceKey.getSourceId()
+                            + " userId:"
+                            + safetySourceKey.getUserId()
+                            + " with refreshBroadcastId:"
+                            + mId
+                            + ", now "
+                            + mSourceRefreshInFlight.size()
+                            + " in flight.");
         }
 
         private void markSourceRefreshAsComplete(@NonNull SafetySourceKey safetySourceKey) {
             mSourceRefreshInFlight.remove(safetySourceKey);
+            Log.v(
+                    TAG,
+                    "Refresh completed for sourceId:"
+                            + safetySourceKey.getSourceId()
+                            + " userId:"
+                            + safetySourceKey.getUserId()
+                            + " with refreshBroadcastId:"
+                            + mId
+                            + ", "
+                            + mSourceRefreshInFlight.size()
+                            + " still in flight.");
         }
 
         private boolean isComplete() {
diff --git a/service/java/com/android/safetycenter/SafetyCenterService.java b/service/java/com/android/safetycenter/SafetyCenterService.java
index de76bb8..0c1d74a 100644
--- a/service/java/com/android/safetycenter/SafetyCenterService.java
+++ b/service/java/com/android/safetycenter/SafetyCenterService.java
@@ -276,10 +276,9 @@
 
             List<Broadcast> broadcasts;
             String refreshBroadcastId;
+
             synchronized (mApiLock) {
                 broadcasts = mSafetyCenterConfigReader.getBroadcasts();
-                // TODO(b/229060064): Check if a refresh is currently in progress, and only start a
-                //  new refresh if it should be replaced.
                 refreshBroadcastId =
                         mSafetyCenterRefreshTracker.reportRefreshInProgress(
                                 refreshReason, userProfileGroup);
@@ -287,6 +286,11 @@
                 RefreshTimeout refreshTimeout =
                         new RefreshTimeout(refreshBroadcastId, userProfileGroup);
                 mSafetyCenterTimeouts.add(refreshTimeout, REFRESH_TIMEOUT);
+
+                mSafetyCenterListeners.deliverUpdateForUserProfileGroup(
+                        userProfileGroup,
+                        mSafetyCenterDataTracker.getSafetyCenterData(userProfileGroup),
+                        null);
             }
 
             mSafetyCenterBroadcastDispatcher.sendRefreshSafetySources(
@@ -709,6 +713,10 @@
                         // TODO(b/229080761): Implement proper error message.
                         new SafetyCenterErrorDetails("Refresh timeout"));
             }
+
+            Log.v(
+                    TAG,
+                    "Cleared refresh with broadcastId:" + mRefreshBroadcastId + " after a timeout");
         }
     }
 
diff --git a/service/java/com/android/safetycenter/SafetySourceKey.java b/service/java/com/android/safetycenter/SafetySourceKey.java
index ac7de40..7250034 100644
--- a/service/java/com/android/safetycenter/SafetySourceKey.java
+++ b/service/java/com/android/safetycenter/SafetySourceKey.java
@@ -59,6 +59,16 @@
                 + '}';
     }
 
+    @NonNull
+    public String getSourceId() {
+        return mSourceId;
+    }
+
+    @UserIdInt
+    public int getUserId() {
+        return mUserId;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt
index 38a1bb0..b8ac511 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt
@@ -46,6 +46,9 @@
 import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING
 import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK
 import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN
+import android.safetycenter.SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS
+import android.safetycenter.SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS
+import android.safetycenter.SafetyCenterStatus.REFRESH_STATUS_NONE
 import android.safetycenter.SafetySourceData
 import android.safetycenter.SafetySourceErrorDetails
 import android.safetycenter.cts.testing.Coroutines.TIMEOUT_SHORT
@@ -1018,6 +1021,9 @@
     @Test
     fun refreshSafetySources_sendsDifferentBroadcastIdsOnEachMethodCall() {
         safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+        SafetySourceReceiver.safetySourceData[
+                SafetySourceDataKey(Reason.REFRESH_FETCH_FRESH_DATA, SINGLE_SOURCE_ID)] =
+            safetySourceCtsData.information
 
         val broadcastId1 =
             safetyCenterManager.refreshSafetySourcesWithReceiverPermissionAndWait(
@@ -1120,6 +1126,40 @@
     }
 
     @Test
+    fun refreshSafetySources_withRefreshReasonRescanButtonClick_notifiesUiDuringRescan() {
+        safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+        SafetySourceReceiver.safetySourceData[
+                SafetySourceDataKey(Reason.REFRESH_FETCH_FRESH_DATA, SINGLE_SOURCE_ID)] =
+            safetySourceCtsData.information
+        val listener = safetyCenterCtsHelper.addListener()
+
+        safetyCenterManager.refreshSafetySourcesWithReceiverPermissionAndWait(
+            REFRESH_REASON_RESCAN_BUTTON_CLICK)
+        val refreshStatus1 = listener.receiveSafetyCenterData().status.refreshStatus
+        val refreshStatus2 = listener.receiveSafetyCenterData().status.refreshStatus
+
+        assertThat(refreshStatus1).isEqualTo(REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS)
+        assertThat(refreshStatus2).isEqualTo(REFRESH_STATUS_NONE)
+    }
+
+    @Test
+    fun refreshSafetySources_withRefreshReasonPageOpen_notifiesUiWithFetch() {
+        safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+        SafetySourceReceiver.safetySourceData[
+                SafetySourceDataKey(Reason.REFRESH_GET_DATA, SINGLE_SOURCE_ID)] =
+            safetySourceCtsData.information
+        val listener = safetyCenterCtsHelper.addListener()
+
+        safetyCenterManager.refreshSafetySourcesWithReceiverPermissionAndWait(
+            REFRESH_REASON_PAGE_OPEN)
+        val refreshStatus1 = listener.receiveSafetyCenterData().status.refreshStatus
+        val refreshStatus2 = listener.receiveSafetyCenterData().status.refreshStatus
+
+        assertThat(refreshStatus1).isEqualTo(REFRESH_STATUS_DATA_FETCH_IN_PROGRESS)
+        assertThat(refreshStatus2).isEqualTo(REFRESH_STATUS_NONE)
+    }
+
+    @Test
     fun getSafetyCenterConfig_withFlagEnabled_isNotNull() {
         val config = safetyCenterManager.getSafetyCenterConfigWithPermission()