diff options
4 files changed, 226 insertions, 22 deletions
diff --git a/flags/flags.aconfig b/flags/flags.aconfig index 98c64507d..c694e36be 100644 --- a/flags/flags.aconfig +++ b/flags/flags.aconfig @@ -81,3 +81,12 @@ flag { bug: "356910008" is_fixed_read_only: true } + +flag { + name: "safety_center_issue_only_affects_group_status" + is_exported: true + namespace: "permissions" + description: "This flag is used by Safety Center to affect the status light of an entry group when an issue only source pushes a warning" + bug: "356910111" + is_fixed_read_only: true +} diff --git a/service/java/com/android/safetycenter/SafetyCenterDataFactory.java b/service/java/com/android/safetycenter/SafetyCenterDataFactory.java index 7c7ade3f1..7f4e2e166 100644 --- a/service/java/com/android/safetycenter/SafetyCenterDataFactory.java +++ b/service/java/com/android/safetycenter/SafetyCenterDataFactory.java @@ -17,7 +17,6 @@ package com.android.safetycenter; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; -import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM; import static com.android.safetycenter.UserProfileGroup.PROFILE_TYPE_MANAGED; import static com.android.safetycenter.UserProfileGroup.PROFILE_TYPE_PRIMARY; @@ -388,6 +387,8 @@ public final class SafetyCenterDataFactory { SafetySourcesGroup safetySourcesGroup, String defaultPackageName, UserProfileGroup userProfileGroup) { + HighestSeverityIssueOnlyIssue highestSeverityIssueOnlyIssue = + new HighestSeverityIssueOnlyIssue(); int groupSafetyCenterEntryLevel = SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; List<SafetySource> safetySources = safetySourcesGroup.getSafetySources(); @@ -415,6 +416,7 @@ public final class SafetyCenterDataFactory { groupSafetyCenterEntryLevel, addSafetyCenterEntry( safetyCenterOverallState, + highestSeverityIssueOnlyIssue, entries, safetySource, defaultPackageName, @@ -436,7 +438,10 @@ public final class SafetyCenterDataFactory { CharSequence groupSummary = getSafetyCenterEntryGroupSummary( - safetySourcesGroup, groupSafetyCenterEntryLevel, entries); + safetySourcesGroup, + groupSafetyCenterEntryLevel, + entries, + highestSeverityIssueOnlyIssue); safetyCenterEntryOrGroups.add( new SafetyCenterEntryOrGroup( new SafetyCenterEntryGroup.Builder( @@ -471,7 +476,8 @@ public final class SafetyCenterDataFactory { private CharSequence getSafetyCenterEntryGroupSummary( SafetySourcesGroup safetySourcesGroup, @SafetyCenterEntry.EntrySeverityLevel int groupSafetyCenterEntryLevel, - List<SafetyCenterEntry> entries) { + List<SafetyCenterEntry> entries, + HighestSeverityIssueOnlyIssue highestSeverityIssueOnlyIssue) { switch (groupSafetyCenterEntryLevel) { case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING: case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION: @@ -500,6 +506,16 @@ public final class SafetyCenterDataFactory { } } + SafetySourceIssue highestSeverityIssueOnlySafetySourceIssue = + highestSeverityIssueOnlyIssue.mSafetySourceIssue; + if (highestSeverityIssueOnlySafetySourceIssue != null + && toSafetyCenterEntrySeverityLevel( + highestSeverityIssueOnlySafetySourceIssue + .getSeverityLevel()) + == groupSafetyCenterEntryLevel) { + return highestSeverityIssueOnlySafetySourceIssue.getTitle(); + } + return getDefaultGroupSummary(safetySourcesGroup, entries); case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED: return getDefaultGroupSummary(safetySourcesGroup, entries); @@ -554,6 +570,7 @@ public final class SafetyCenterDataFactory { @SafetyCenterEntry.EntrySeverityLevel private int addSafetyCenterEntry( SafetyCenterOverallState safetyCenterOverallState, + HighestSeverityIssueOnlyIssue highestSeverityIssueOnlyIssue, List<SafetyCenterEntry> entries, SafetySource safetySource, String defaultPackageName, @@ -562,13 +579,10 @@ public final class SafetyCenterDataFactory { boolean isUserRunning) { SafetyCenterEntry safetyCenterEntry = toSafetyCenterEntry( - safetySource, - defaultPackageName, - userId, - profileType, - isUserRunning); + safetySource, defaultPackageName, userId, profileType, isUserRunning); if (safetyCenterEntry == null) { - return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; + return getSafetyCenterEntrySeverityLevelFromIssues( + highestSeverityIssueOnlyIssue, safetySource, userId, isUserRunning); } safetyCenterOverallState.addEntryOverallSeverityLevel( @@ -579,6 +593,64 @@ public final class SafetyCenterDataFactory { return safetyCenterEntry.getSeverityLevel(); } + @SafetyCenterEntry.EntrySeverityLevel + private int getSafetyCenterEntrySeverityLevelFromIssues( + HighestSeverityIssueOnlyIssue highestSeverityIssueOnlyIssue, + SafetySource safetySource, + @UserIdInt int userId, + boolean isUserRunning) { + if (!Flags.safetyCenterIssueOnlyAffectsGroupStatus()) { + return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; + } + if (safetySource.getType() != SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY) { + return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; + } + if (!isUserRunning) { + // Issues don't show for non-running users. + return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; + } + SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId); + SafetySourceData safetySourceData = + mSafetyCenterDataManager.getSafetySourceDataInternal(key); + if (safetySourceData == null) { + return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; + } + List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues(); + int safetyCenterEntrySeverityLevel = SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; + for (int i = 0; i < safetySourceIssues.size(); i++) { + SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i); + + SafetyCenterIssueKey safetyCenterIssueKey = + SafetyCenterIssueKey.newBuilder() + .setSafetySourceId(safetySource.getId()) + .setSafetySourceIssueId(safetySourceIssue.getId()) + .setUserId(userId) + .build(); + + int safetySourceIssueSeverityLevel = safetySourceIssue.getSeverityLevel(); + + if (mSafetyCenterDataManager.isIssueDismissed( + safetyCenterIssueKey, safetySourceIssueSeverityLevel)) { + continue; + } + + SafetySourceIssue highestSeverityIssueOnlySafetySourceIssue = + highestSeverityIssueOnlyIssue.mSafetySourceIssue; + if (highestSeverityIssueOnlySafetySourceIssue == null + || safetySourceIssueSeverityLevel + > highestSeverityIssueOnlySafetySourceIssue.getSeverityLevel()) { + highestSeverityIssueOnlyIssue.mSafetySourceIssue = safetySourceIssue; + } + + safetyCenterEntrySeverityLevel = + mergeSafetyCenterEntrySeverityLevels( + safetyCenterEntrySeverityLevel, + toSafetyCenterEntrySeverityLevel(safetySourceIssueSeverityLevel)); + } + + return safetyCenterEntrySeverityLevel; + } + @Nullable private SafetyCenterEntry toSafetyCenterEntry( SafetySource safetySource, @@ -783,11 +855,7 @@ public final class SafetyCenterDataFactory { boolean isUserRunning) { SafetyCenterStaticEntry staticEntry = toSafetyCenterStaticEntry( - safetySource, - defaultPackageName, - userId, - profileType, - isUserRunning); + safetySource, defaultPackageName, userId, profileType, isUserRunning); if (staticEntry == null) { return; } @@ -987,7 +1055,7 @@ public final class SafetyCenterDataFactory { Log.w( TAG, - "Unexpected SafetySourceData.SeverityLevel in SafetySourceStatus: " + "Unexpected SafetySourceData.SeverityLevel in SafetySourceData: " + safetySourceSeverityLevel); return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN; } @@ -1155,7 +1223,7 @@ public final class SafetyCenterDataFactory { return getIcuPluralsString( "overall_severity_level_action_taken_summary", numAutomaticIssues); } - // Fall through. + // Fall through. case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION: case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING: return getIcuPluralsString("overall_severity_n_alerts_summary", numIssues); @@ -1201,7 +1269,7 @@ public final class SafetyCenterDataFactory { if (!overallSeverityUnknown) { return null; } - // Fall through. + // Fall through. case SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS: return mSafetyCenterResourcesApk.getStringByName("scanning_title"); } @@ -1232,9 +1300,9 @@ public final class SafetyCenterDataFactory { return mSafetyCenterResourcesApk.getString(safetySource.getTitleResId()); case PROFILE_TYPE_MANAGED: return DevicePolicyResources.getSafetySourceWorkString( - mSafetyCenterResourcesApk, - safetySource.getId(), - safetySource.getTitleForWorkResId()); + mSafetyCenterResourcesApk, + safetySource.getId(), + safetySource.getTitleForWorkResId()); case PROFILE_TYPE_PRIVATE: if (SdkLevel.isAtLeastV() && Flags.privateProfileTitleApi()) { return mSafetyCenterResourcesApk.getString( @@ -1337,4 +1405,8 @@ public final class SafetyCenterDataFactory { return Math.max(left, right); } } + + private static final class HighestSeverityIssueOnlyIssue { + @Nullable private SafetySourceIssue mSafetySourceIssue = null; + } } diff --git a/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt b/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt index 4f06c0f3f..cb3935ec5 100644 --- a/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt +++ b/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt @@ -22,6 +22,9 @@ import android.content.Intent import android.os.Build.VERSION_CODES.TIRAMISU import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE import android.os.UserHandle +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.safetycenter.SafetyCenterData import android.safetycenter.SafetyCenterEntry import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING @@ -64,6 +67,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress import com.android.compatibility.common.util.SystemUtil import com.android.modules.utils.build.SdkLevel +import com.android.permission.flags.Flags import com.android.safetycenter.internaldata.SafetyCenterBundles import com.android.safetycenter.internaldata.SafetyCenterBundles.ISSUES_TO_GROUPS_BUNDLE_KEY import com.android.safetycenter.internaldata.SafetyCenterEntryId @@ -783,8 +787,9 @@ class SafetyCenterManagerTest { ) ) - @get:Rule(order = 1) val supportsSafetyCenterRule = SupportsSafetyCenterRule(context) - @get:Rule(order = 2) val safetyCenterTestRule = SafetyCenterTestRule(safetyCenterTestHelper) + @get:Rule(order = 1) val flagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + @get:Rule(order = 2) val supportsSafetyCenterRule = SupportsSafetyCenterRule(context) + @get:Rule(order = 3) val safetyCenterTestRule = SafetyCenterTestRule(safetyCenterTestHelper) @Test fun getSafetySourceData_differentPackageWithManageSafetyCenterPermission_returnsData() { @@ -3153,6 +3158,105 @@ class SafetyCenterManagerTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_SAFETY_CENTER_ISSUE_ONLY_AFFECTS_GROUP_STATUS) + fun getSafetyCenterData_entryGroupWithIssueOnlySourceHighestOfGroup_affectsEntryGroupStatus() { + safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.entryGroupWithIssueOnlyConfig) + safetyCenterTestHelper.setData(SOURCE_ID_1, safetySourceTestData.information) + safetyCenterTestHelper.setData(SOURCE_ID_2, safetySourceTestData.information) + safetyCenterTestHelper.setData( + ISSUE_ONLY_ALL_OPTIONAL_ID, + SafetySourceTestData.issuesOnly(safetySourceTestData.recommendationGeneralIssue) + ) + + val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission() + + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.severityLevel) + .isEqualTo(ENTRY_SEVERITY_LEVEL_RECOMMENDATION) + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.summary!!.toString()) + .isEqualTo("Recommendation issue title") + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_SAFETY_CENTER_ISSUE_ONLY_AFFECTS_GROUP_STATUS) + fun getSafetyCenterData_entryGroupWithIssueOnlySourceSameAsGroup_affectsTitle() { + safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.entryGroupWithIssueOnlyConfig) + safetyCenterTestHelper.setData(SOURCE_ID_1, safetySourceTestData.information) + safetyCenterTestHelper.setData(SOURCE_ID_2, safetySourceTestData.information) + safetyCenterTestHelper.setData( + ISSUE_ONLY_ALL_OPTIONAL_ID, + SafetySourceTestData.issuesOnly(safetySourceTestData.informationIssue) + ) + + val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission() + + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.severityLevel) + .isEqualTo(ENTRY_SEVERITY_LEVEL_OK) + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.summary!!.toString()) + .isEqualTo("Information issue title") + } + + @Test + fun getSafetyCenterData_entryGroupWithIssueOnlySourceNotHighest_doesntAffectEntryGroupStatus() { + safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.entryGroupWithIssueOnlyConfig) + safetyCenterTestHelper.setData( + SOURCE_ID_1, + safetySourceTestData.recommendationWithGeneralIssue + ) + safetyCenterTestHelper.setData(SOURCE_ID_2, safetySourceTestData.information) + safetyCenterTestHelper.setData( + ISSUE_ONLY_ALL_OPTIONAL_ID, + SafetySourceTestData.issuesOnly(safetySourceTestData.informationIssue) + ) + + val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission() + + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.severityLevel) + .isEqualTo(ENTRY_SEVERITY_LEVEL_RECOMMENDATION) + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.summary!!.toString()) + .isEqualTo("Recommendation summary") + } + + @Test + fun getSafetyCenterData_entryGroupWithIssueOnlySourceDismissed_doesntAffectEntryGroupStatus() { + safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.entryGroupWithIssueOnlyConfig) + safetyCenterTestHelper.setData(SOURCE_ID_1, safetySourceTestData.information) + safetyCenterTestHelper.setData(SOURCE_ID_2, safetySourceTestData.information) + safetyCenterTestHelper.setData( + ISSUE_ONLY_ALL_OPTIONAL_ID, + SafetySourceTestData.issuesOnly(safetySourceTestData.recommendationGeneralIssue) + ) + safetyCenterManager.dismissSafetyCenterIssueWithPermission( + SafetyCenterTestData.issueId(ISSUE_ONLY_ALL_OPTIONAL_ID, RECOMMENDATION_ISSUE_ID) + ) + + val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission() + + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.severityLevel) + .isEqualTo(ENTRY_SEVERITY_LEVEL_OK) + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.summary!!.toString()) + .isEqualTo("OK") + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_SAFETY_CENTER_ISSUE_ONLY_AFFECTS_GROUP_STATUS) + fun getSafetyCenterData_entryGroupWithIssueOnlySourceFlagOff_doesntAffectEntryGroupStatus() { + safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.entryGroupWithIssueOnlyConfig) + safetyCenterTestHelper.setData(SOURCE_ID_1, safetySourceTestData.information) + safetyCenterTestHelper.setData(SOURCE_ID_2, safetySourceTestData.information) + safetyCenterTestHelper.setData( + ISSUE_ONLY_ALL_OPTIONAL_ID, + SafetySourceTestData.issuesOnly(safetySourceTestData.recommendationGeneralIssue) + ) + + val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission() + + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.severityLevel) + .isEqualTo(ENTRY_SEVERITY_LEVEL_OK) + assertThat(apiSafetyCenterData.entriesOrGroups.single().entryGroup!!.summary!!.toString()) + .isEqualTo("OK") + } + + @Test fun addOnSafetyCenterDataChangedListener_listenerCalledWithSafetyCenterDataFromConfig() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.singleSourceConfig) @@ -3836,9 +3940,11 @@ class SafetyCenterManagerTest { companion object { private val RESURFACE_DELAY = Duration.ofMillis(500) + // Wait 3 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) + // 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. diff --git a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterTestConfigs.kt b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterTestConfigs.kt index 0e31b2934..261e179dd 100644 --- a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterTestConfigs.kt +++ b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterTestConfigs.kt @@ -351,6 +351,23 @@ class SafetyCenterTestConfigs(private val context: Context) { .build() /** + * A simple [SafetyCenterConfig] for tests with a single stateful group containing 2 dynamic + * sources and an issue only source. + */ + val entryGroupWithIssueOnlyConfig = + SafetyCenterConfig.Builder() + .addSafetySourcesGroup( + safetySourcesGroupBuilder("EntryGroupWithIssueOnly") + .addSafetySource(dynamicSource1) + .addSafetySource(dynamicSource2) + .addSafetySource( + issueOnlySafetySourceBuilder(ISSUE_ONLY_ALL_OPTIONAL_ID).build() + ) + .build() + ) + .build() + + /** * A simple [SafetyCenterConfig] for tests with multiple sources with one source having an * invalid default intent. */ |