summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Giulio Fiscella <fiscella@google.com> 2022-08-04 11:44:24 +0000
committer Giulio Fiscella <fiscella@google.com> 2022-08-04 11:44:24 +0000
commitfa0f0865af0b1c4f32deb2154a55dcc9d46d5800 (patch)
tree3dfd60252e53544e29a4a43c992cab8f13f36f6e
parent1bf0298d2b9911cbfe5ca41b13aaeb997cc8c647 (diff)
Readd dismissed issue resurface logic
Bug: 236961176 Test: atest --no-bazel-mode CtsSafetyCenterTestCases Change-Id: I047a1041dd41ef1f59a20f300c47053c1eba6253
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterDataTracker.java71
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt141
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/testing/Coroutines.kt21
3 files changed, 220 insertions, 13 deletions
diff --git a/service/java/com/android/safetycenter/SafetyCenterDataTracker.java b/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
index c83734b3c..a0927a30b 100644
--- a/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
+++ b/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
@@ -69,6 +69,7 @@ import com.android.safetycenter.persistence.PersistedSafetyCenterIssue;
import com.android.safetycenter.resources.SafetyCenterResourcesContext;
import java.io.PrintWriter;
+import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
@@ -358,11 +359,18 @@ final class SafetyCenterDataTracker {
*/
void dismissSafetyCenterIssue(@NonNull SafetyCenterIssueKey safetyCenterIssueKey) {
IssueData issueData = mSafetyCenterIssueCache.get(safetyCenterIssueKey);
- if (issueData != null) {
- issueData.setDismissedAt(Instant.now());
- issueData.setDismissCount(issueData.getDismissCount() + 1);
- mSafetyCenterIssueCacheDirty = true;
+ if (issueData == null) {
+ Log.w(
+ TAG,
+ "Issue missing when writing to cache: "
+ + safetyCenterIssueKey.getSafetySourceIssueId()
+ + ", of source: "
+ + safetyCenterIssueKey.getSafetySourceId());
+ return;
}
+ issueData.setDismissedAt(Instant.now());
+ issueData.setDismissCount(issueData.getDismissCount() + 1);
+ mSafetyCenterIssueCacheDirty = true;
}
/**
@@ -373,10 +381,6 @@ final class SafetyCenterDataTracker {
*/
@Nullable
SafetySourceIssue getSafetySourceIssue(@NonNull SafetyCenterIssueKey safetyCenterIssueKey) {
- if (isDismissed(safetyCenterIssueKey)) {
- return null;
- }
-
SafetySourceKey key =
SafetySourceKey.of(
safetyCenterIssueKey.getSafetySourceId(), safetyCenterIssueKey.getUserId());
@@ -386,15 +390,24 @@ final class SafetyCenterDataTracker {
}
List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues();
+ SafetySourceIssue targetIssue = null;
for (int i = 0; i < safetySourceIssues.size(); i++) {
SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);
if (safetyCenterIssueKey.getSafetySourceIssueId().equals(safetySourceIssue.getId())) {
- return safetySourceIssue;
+ targetIssue = safetySourceIssue;
+ break;
}
}
+ if (targetIssue == null) {
+ return null;
+ }
- return null;
+ if (isDismissed(safetyCenterIssueKey, targetIssue.getSeverityLevel())) {
+ return null;
+ }
+
+ return targetIssue;
}
/**
@@ -560,12 +573,42 @@ final class SafetyCenterDataTracker {
}
}
- private boolean isDismissed(@NonNull SafetyCenterIssueKey safetyCenterIssueKey) {
+ private boolean isDismissed(
+ @NonNull SafetyCenterIssueKey safetyCenterIssueKey,
+ @SafetySourceData.SeverityLevel int safetySourceIssueSeverityLevel) {
IssueData issueData = mSafetyCenterIssueCache.get(safetyCenterIssueKey);
if (issueData == null) {
+ Log.w(
+ TAG,
+ "Issue missing when reading from cache: "
+ + safetyCenterIssueKey.getSafetySourceIssueId()
+ + ", of source: "
+ + safetyCenterIssueKey.getSafetySourceId());
+ return false;
+ }
+
+ Instant dismissedAt = issueData.getDismissedAt();
+ boolean hasNeverBeenDismissed = dismissedAt == null;
+ if (hasNeverBeenDismissed) {
+ return false;
+ }
+
+ long maxCount = SafetyCenterFlags.getResurfaceIssueMaxCount(safetySourceIssueSeverityLevel);
+ Duration delay = SafetyCenterFlags.getResurfaceIssueDelay(safetySourceIssueSeverityLevel);
+
+ boolean hasAlreadyResurfacedTheMaxAllowedNumberOfTimes =
+ issueData.getDismissCount() > maxCount;
+ if (hasAlreadyResurfacedTheMaxAllowedNumberOfTimes) {
+ return true;
+ }
+
+ Duration timeSinceLastDismissal = Duration.between(dismissedAt, Instant.now());
+ boolean isTimeToResurface = timeSinceLastDismissal.compareTo(delay) >= 0;
+ if (isTimeToResurface) {
return false;
}
- return issueData.getDismissedAt() != null;
+
+ return true;
}
private boolean isInFlight(@NonNull SafetyCenterIssueActionId safetyCenterIssueActionId) {
@@ -942,7 +985,9 @@ final class SafetyCenterDataTracker {
.setIssueTypeId(safetySourceIssue.getIssueTypeId())
.build();
- if (isDismissed(safetyCenterIssueId.getSafetyCenterIssueKey())) {
+ if (isDismissed(
+ safetyCenterIssueId.getSafetyCenterIssueKey(),
+ safetySourceIssue.getSeverityLevel())) {
return null;
}
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt
index ac92a0bed..971293e50 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt
@@ -60,6 +60,7 @@ import android.safetycenter.SafetySourceErrorDetails
import android.safetycenter.SafetySourceIssue
import android.safetycenter.cts.testing.Coroutines.TIMEOUT_LONG
import android.safetycenter.cts.testing.Coroutines.TIMEOUT_SHORT
+import android.safetycenter.cts.testing.Coroutines.waitForWithTimeout
import android.safetycenter.cts.testing.FakeExecutor
import android.safetycenter.cts.testing.SafetyCenterApisWithShellPermissions.addOnSafetyCenterDataChangedListenerWithPermission
import android.safetycenter.cts.testing.SafetyCenterApisWithShellPermissions.clearAllSafetySourceDataForTestsWithPermission
@@ -138,8 +139,10 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compatibility.common.preconditions.ScreenLockHelper
import com.android.safetycenter.resources.SafetyCenterResourcesContext
+import com.google.common.base.Preconditions.checkState
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import java.time.Duration
import java.util.Locale
import kotlin.test.assertFailsWith
import kotlinx.coroutines.TimeoutCancellationException
@@ -2496,6 +2499,133 @@ class SafetyCenterManagerTest {
}
@Test
+ fun dismissSafetyCenterIssue_withEmptyMaxCountMap_doesNotResurface() {
+ SafetyCenterFlags.resurfaceIssueMaxCounts = emptyMap()
+ SafetyCenterFlags.resurfaceIssueDelays = mapOf(SEVERITY_LEVEL_INFORMATION to Duration.ZERO)
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ safetyCenterCtsHelper.setData(SINGLE_SOURCE_ID, safetySourceCtsData.informationWithIssue)
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+ checkState(apiSafetyCenterData == safetyCenterDataOkOneAlert)
+
+ safetyCenterManager.dismissSafetyCenterIssueWithPermission(
+ SafetyCenterCtsData.issueId(SINGLE_SOURCE_ID, INFORMATION_ISSUE_ID))
+
+ assertFailsWith(TimeoutCancellationException::class) {
+ waitForWithTimeout(timeout = TIMEOUT_SHORT) {
+ val hasResurfaced =
+ safetyCenterManager.getSafetyCenterDataWithPermission() != safetyCenterDataOk
+ hasResurfaced
+ }
+ }
+ }
+
+ @Test
+ fun dismissSafetyCenterIssue_withZeroMaxCount_doesNotResurface() {
+ SafetyCenterFlags.resurfaceIssueMaxCounts =
+ mapOf(
+ SEVERITY_LEVEL_INFORMATION to 0L,
+ SEVERITY_LEVEL_RECOMMENDATION to 99L,
+ SEVERITY_LEVEL_CRITICAL_WARNING to 99L)
+ SafetyCenterFlags.resurfaceIssueDelays =
+ mapOf(
+ SEVERITY_LEVEL_INFORMATION to Duration.ZERO,
+ SEVERITY_LEVEL_RECOMMENDATION to Duration.ZERO,
+ SEVERITY_LEVEL_CRITICAL_WARNING to Duration.ZERO)
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ safetyCenterCtsHelper.setData(SINGLE_SOURCE_ID, safetySourceCtsData.informationWithIssue)
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+ checkState(apiSafetyCenterData == safetyCenterDataOkOneAlert)
+
+ safetyCenterManager.dismissSafetyCenterIssueWithPermission(
+ SafetyCenterCtsData.issueId(SINGLE_SOURCE_ID, INFORMATION_ISSUE_ID))
+
+ assertFailsWith(TimeoutCancellationException::class) {
+ waitForWithTimeout(timeout = TIMEOUT_SHORT) {
+ val hasResurfaced =
+ safetyCenterManager.getSafetyCenterDataWithPermission() != safetyCenterDataOk
+ hasResurfaced
+ }
+ }
+ }
+
+ @Test
+ fun dismissSafetyCenterIssue_withTwoMaxCount_resurfacesTwice() {
+ SafetyCenterFlags.resurfaceIssueMaxCounts =
+ mapOf(
+ SEVERITY_LEVEL_INFORMATION to 0L,
+ SEVERITY_LEVEL_RECOMMENDATION to 0L,
+ SEVERITY_LEVEL_CRITICAL_WARNING to 2L)
+ SafetyCenterFlags.resurfaceIssueDelays =
+ mapOf(
+ SEVERITY_LEVEL_INFORMATION to RESURFACE_DELAY,
+ SEVERITY_LEVEL_RECOMMENDATION to RESURFACE_DELAY,
+ SEVERITY_LEVEL_CRITICAL_WARNING to Duration.ZERO)
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ safetyCenterCtsHelper.setData(
+ SINGLE_SOURCE_ID, safetySourceCtsData.criticalWithResolvingDeviceIssue)
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+ checkState(apiSafetyCenterData == safetyCenterDataDeviceCriticalOneAlert)
+
+ safetyCenterManager.dismissSafetyCenterIssueWithPermission(
+ SafetyCenterCtsData.issueId(SINGLE_SOURCE_ID, CRITICAL_ISSUE_ID))
+ val apiSafetyCenterDataResurface = safetyCenterManager.getSafetyCenterDataWithPermission()
+ checkState(apiSafetyCenterDataResurface == safetyCenterDataDeviceCriticalOneAlert)
+ safetyCenterManager.dismissSafetyCenterIssueWithPermission(
+ SafetyCenterCtsData.issueId(SINGLE_SOURCE_ID, CRITICAL_ISSUE_ID))
+ val apiSafetyCenterDataResurfaceAgain =
+ safetyCenterManager.getSafetyCenterDataWithPermission()
+ checkState(apiSafetyCenterDataResurfaceAgain == safetyCenterDataDeviceCriticalOneAlert)
+ safetyCenterManager.dismissSafetyCenterIssueWithPermission(
+ SafetyCenterCtsData.issueId(SINGLE_SOURCE_ID, CRITICAL_ISSUE_ID))
+
+ assertFailsWith(TimeoutCancellationException::class) {
+ waitForWithTimeout(timeout = TIMEOUT_SHORT) {
+ val hasResurfaced =
+ safetyCenterManager.getSafetyCenterDataWithPermission() !=
+ safetyCenterDataOkReviewCriticalEntry
+ hasResurfaced
+ }
+ }
+ }
+
+ @Test
+ fun dismissSafetyCenterIssue_withNonZeroMaxCountAndNonZeroDelay_resurfacesAfterDelay() {
+ // We cannot rely on a listener in this test to assert on the API content at all times!
+ // The listener will not receive an update when a dismissed issue resurfaces, and it will
+ // not receive an update after subsequent dismissals because as far as the listener cache is
+ // concerned the dismissed issue never resurfaced. This is working as intended.
+ SafetyCenterFlags.resurfaceIssueMaxCounts =
+ mapOf(
+ SEVERITY_LEVEL_INFORMATION to 0L,
+ SEVERITY_LEVEL_RECOMMENDATION to 99L,
+ SEVERITY_LEVEL_CRITICAL_WARNING to 0L)
+ SafetyCenterFlags.resurfaceIssueDelays =
+ mapOf(
+ SEVERITY_LEVEL_INFORMATION to Duration.ZERO,
+ SEVERITY_LEVEL_RECOMMENDATION to RESURFACE_DELAY,
+ SEVERITY_LEVEL_CRITICAL_WARNING to Duration.ZERO)
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ safetyCenterCtsHelper.setData(
+ SINGLE_SOURCE_ID, safetySourceCtsData.recommendationWithDeviceIssue)
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+ checkState(apiSafetyCenterData == safetyCenterDataDeviceRecommendationOneAlert)
+ val listener = safetyCenterCtsHelper.addListener()
+
+ safetyCenterManager.dismissSafetyCenterIssueWithPermission(
+ SafetyCenterCtsData.issueId(SINGLE_SOURCE_ID, RECOMMENDATION_ISSUE_ID))
+
+ val safetyCenterDataAfterDismissal = listener.receiveSafetyCenterData()
+ assertThat(safetyCenterDataAfterDismissal)
+ .isEqualTo(safetyCenterDataOkReviewRecommendationEntry)
+ waitForWithTimeout(timeout = RESURFACE_TIMEOUT, checkPeriod = RESURFACE_CHECK) {
+ val hasResurfacedExactly =
+ safetyCenterManager.getSafetyCenterDataWithPermission() ==
+ safetyCenterDataDeviceRecommendationOneAlert
+ hasResurfacedExactly
+ }
+ }
+
+ @Test
fun dismissSafetyCenterIssue_withFlagDisabled_doesntCallListenerOrDismiss() {
safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
safetyCenterCtsHelper.setData(
@@ -3137,4 +3267,15 @@ class SafetyCenterManagerTest {
arguments["count"] = count
return messageFormat.format(arguments)
}
+
+ companion object {
+ private val RESURFACE_DELAY = Duration.ofMillis(500)
+ // Wait 1.5 times the RESURFACE_DELAY before asserting whether an issue has or has not
+ // resurfaced. Use a constant additive error buffer if we increase the delay considerably.
+ private val RESURFACE_TIMEOUT = RESURFACE_DELAY.multipliedBy(3).dividedBy(2)
+ // Check more than once during a RESURFACE_DELAY before asserting whether an issue has or
+ // has not resurfaced. Use a different check logic (focused at the expected resurface time)
+ // if we increase the delay considerably.
+ private val RESURFACE_CHECK = RESURFACE_DELAY.dividedBy(4)
+ }
}
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/Coroutines.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/Coroutines.kt
index 6325dbcbb..af916a950 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/Coroutines.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/Coroutines.kt
@@ -17,10 +17,12 @@
package android.safetycenter.cts.testing
import java.time.Duration
+import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
/** A class that facilitates interacting with coroutines. */
+// TODO(b/228823159) Consolidate with other Coroutines helper functions
object Coroutines {
/** Behaves in the same way as [runBlocking], but with a timeout. */
@@ -29,6 +31,25 @@ object Coroutines {
withTimeout(timeout.toMillis()) { block() }
}
+ /** Check a condition using coroutines with a timeout. */
+ fun waitForWithTimeout(
+ timeout: Duration = TIMEOUT_LONG,
+ checkPeriod: Duration = CHECK_PERIOD,
+ condition: () -> Boolean
+ ) {
+ runBlockingWithTimeout(timeout) { waitFor(checkPeriod, condition) }
+ }
+
+ /** Check a condition using coroutines. */
+ private suspend fun waitFor(checkPeriod: Duration = CHECK_PERIOD, condition: () -> Boolean) {
+ while (!condition()) {
+ delay(checkPeriod.toMillis())
+ }
+ }
+
+ /** A medium period, to be used for conditions that are expected to change. */
+ private val CHECK_PERIOD = Duration.ofMillis(250)
+
/** A long timeout, to be used for actions that are expected to complete. */
val TIMEOUT_LONG: Duration = Duration.ofSeconds(15)