diff options
author | 2025-02-21 10:55:53 +0000 | |
---|---|---|
committer | 2025-02-21 15:55:18 +0000 | |
commit | 5098fc7aff57f4819be964c02920a1f815befb55 (patch) | |
tree | 985514378fdf9e791897a018018686af877d8ca2 | |
parent | f4c9bdb9118400a98c95f119a5bc8f896e489c60 (diff) |
Use BannerMessagePrefs on SC homepage when expressive design enabled.
This affects SC homepage and quick settings. Will migrate subpages
separately when the expressive collapsible groups are ready. Preferences
are added directly to the page and are not collapsed for now.
Test: atest SafetyCenterActivityFunctionalTestCases CtsSafetyCenterTestCases
SafetyCenterFunctionalTestCases
Fixes: 379849463
Flag: com.android.settingslib.widget.theme.flags.is_expressive_design_enabled
Relnote: Flag protected Safety Center UI updates
Change-Id: I2c931ccebfe1db869ff63d5e81bb10668623f04e
9 files changed, 332 insertions, 51 deletions
diff --git a/PermissionController/Android.bp b/PermissionController/Android.bp index 63fb1a264..c1a54619b 100644 --- a/PermissionController/Android.bp +++ b/PermissionController/Android.bp @@ -131,6 +131,7 @@ android_library { "SettingsLibSearchWidget", "SettingsLibLayoutPreference", "SettingsLibBarChartPreference", + "SettingsLibBannerMessagePreference", "SettingsLibActionBarShadow", "SettingsLibProgressBar", "SettingsLibCollapsingToolbarBaseActivity", diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java index 88759797e..e47565e3b 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java @@ -295,7 +295,8 @@ public class IssueCardPreference extends Preference public static class ConfirmDismissalDialogFragment extends DialogFragment { private static final String ISSUE_KEY = "confirm_dialog_sc_issue"; - private static ConfirmDismissalDialogFragment newInstance(SafetyCenterIssue issue) { + /** Create new fragment with the data it will need. */ + public static ConfirmDismissalDialogFragment newInstance(SafetyCenterIssue issue) { ConfirmDismissalDialogFragment fragment = new ConfirmDismissalDialogFragment(); Bundle args = new Bundle(); diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java index ed6bc382c..1297bc4c2 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java @@ -35,7 +35,6 @@ import android.safetycenter.SafetyCenterData; import android.safetycenter.SafetyCenterEntry; import android.safetycenter.SafetyCenterEntryGroup; import android.safetycenter.SafetyCenterEntryOrGroup; -import android.safetycenter.SafetyCenterIssue; import android.safetycenter.SafetyCenterStaticEntry; import android.safetycenter.SafetyCenterStaticEntryGroup; import android.util.Log; @@ -52,6 +51,8 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.modules.utils.build.SdkLevel; import com.android.permissioncontroller.R; +import com.android.permissioncontroller.safetycenter.ui.expressive.SafetyBannerMessagePreference; +import com.android.permissioncontroller.safetycenter.ui.model.IssueUiData; import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData; import com.android.permissioncontroller.safetycenter.ui.model.StatusUiData; import com.android.safetycenter.internaldata.SafetyCenterBundles; @@ -61,7 +62,6 @@ import com.android.settingslib.widget.SettingsThemeHelper; import kotlin.Unit; import java.util.List; -import java.util.Map; import java.util.Objects; /** Dashboard fragment for the Safety Center. */ @@ -214,7 +214,7 @@ public final class SafetyCenterDashboardFragment extends SafetyCenterFragment { // TODO(b/208212820): Only update entries that have changed since last // update, rather than deleting and re-adding all. - updateIssues(context, data.getIssues(), uiData.getResolvedIssues()); + updateIssues(context, uiData); if (!mIsQuickSettingsFragment) { updateSafetyEntries(context, data.getEntriesOrGroups()); @@ -222,19 +222,29 @@ public final class SafetyCenterDashboardFragment extends SafetyCenterFragment { } } - private void updateIssues( - Context context, List<SafetyCenterIssue> issues, Map<String, String> resolvedIssues) { + private void updateIssues(Context context, SafetyCenterUiData uiData) { mIssuesGroup.removeAll(); - getCollapsableIssuesCardHelper() - .addIssues( - context, - getSafetyCenterViewModel(), - getChildFragmentManager(), - mIssuesGroup, - issues, - emptyList(), - resolvedIssues, - getActivity().getTaskId()); + if (SettingsThemeHelper.isExpressiveTheme(context)) { + for (IssueUiData issueUiData : uiData.getIssueUiDatas()) { + mIssuesGroup.addPreference( + new SafetyBannerMessagePreference( + context, + issueUiData, + getSafetyCenterViewModel(), + getChildFragmentManager())); + } + } else { + getCollapsableIssuesCardHelper() + .addIssues( + context, + getSafetyCenterViewModel(), + getChildFragmentManager(), + mIssuesGroup, + uiData.getSafetyCenterData().getIssues(), + emptyList(), + uiData.getResolvedIssues(), + requireActivity().getTaskId()); + } } // TODO(b/208212820): Add groups and move to separate controller diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/expressive/SafetyBannerMessagePreference.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/expressive/SafetyBannerMessagePreference.kt new file mode 100644 index 000000000..0f2239c60 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/expressive/SafetyBannerMessagePreference.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permissioncontroller.safetycenter.ui.expressive + +import android.content.Context +import android.os.Build +import android.safetycenter.SafetyCenterIssue +import android.util.Log +import android.view.View +import android.widget.LinearLayout +import androidx.annotation.RequiresApi +import androidx.fragment.app.FragmentManager +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.android.modules.utils.build.SdkLevel +import com.android.permissioncontroller.R +import com.android.permissioncontroller.safetycenter.ui.Action +import com.android.permissioncontroller.safetycenter.ui.ComparablePreference +import com.android.permissioncontroller.safetycenter.ui.IssueCardPreference.ConfirmActionDialogFragment +import com.android.permissioncontroller.safetycenter.ui.IssueCardPreference.ConfirmDismissalDialogFragment +import com.android.permissioncontroller.safetycenter.ui.model.IssueUiData +import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel +import com.android.settingslib.widget.BannerMessagePreference + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +class SafetyBannerMessagePreference( + context: Context, + private val issueUiData: IssueUiData, + private val viewModel: SafetyCenterViewModel, + private val dialogFragmentManager: FragmentManager, +) : BannerMessagePreference(context), ComparablePreference { + + init { + setButtonOrientation(LinearLayout.VERTICAL) + displayIssue() + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + viewModel.interactionLogger.recordIssueViewed(issueUiData.issue, issueUiData.isDismissed) + } + + private fun displayIssue() { + setAttentionLevel(issueUiData.issue.severityLevel.toAttentionLevel()) + + title = issueUiData.issue.title + summary = issueUiData.issue.summary + setHeader(issueUiData.issue.attributionTitle) + setSubtitle(issueUiData.issue.subtitle) + // Note: BannerMessagePreference i think always shows an icon (even if it's set to null), + // which is not in the spec + + configureDismissButton() + configureActionButtons() + maybeStartResolution() + } + + private fun configureDismissButton() { + if (issueUiData.issue.isDismissible && !issueUiData.isDismissed) { + setDismissButtonVisible(true) + setDismissButtonOnClickListener { + if (issueUiData.issue.shouldConfirmDismissal()) { + ConfirmDismissalDialogFragment.newInstance(issueUiData.issue) + .showNow(dialogFragmentManager, /* tag= */ null) + } else { + viewModel.dismissIssue(issueUiData.issue) + viewModel.interactionLogger.recordForIssue( + Action.ISSUE_DISMISS_CLICKED, + issueUiData.issue, + isDismissed = false, + ) + } + } + } else { + setDismissButtonVisible(false) + setDismissButtonOnClickListener(null) + } + } + + private fun configureActionButtons() { + val primaryAction = issueUiData.issue.actions.getOrNull(0) + if (primaryAction != null) { + setPositiveButtonText(primaryAction.label) + setPositiveButtonEnabled(issueUiData.resolvedIssueActionId != primaryAction.id) + setPositiveButtonVisible(true) + setPositiveButtonOnClickListener( + ActionButtonOnClickListener(primaryAction, isPrimaryButton = true) + ) + } else { + setPositiveButtonVisible(false) + setPositiveButtonOnClickListener(null) + } + + val secondaryAction = issueUiData.issue.actions.getOrNull(1) + if (secondaryAction != null) { + setNegativeButtonText(secondaryAction.label) + setNegativeButtonEnabled(issueUiData.resolvedIssueActionId != secondaryAction.id) + setNegativeButtonVisible(true) + setNegativeButtonOnClickListener( + ActionButtonOnClickListener(secondaryAction, isPrimaryButton = false) + ) + } else { + setNegativeButtonVisible(false) + setNegativeButtonOnClickListener(null) + } + } + + private inner class ActionButtonOnClickListener( + private val action: SafetyCenterIssue.Action, + private val isPrimaryButton: Boolean, + ) : View.OnClickListener { + override fun onClick(v: View?) { + if (SdkLevel.isAtLeastU() && action.confirmationDialogDetails != null) { + ConfirmActionDialogFragment.newInstance( + issueUiData.issue, + action, + issueUiData.launchTaskId, + isPrimaryButton, + issueUiData.isDismissed, + ) + .showNow(dialogFragmentManager, /* tag= */ null) + } else { + if (action.willResolve()) { + setPositiveButtonEnabled(false) + } + viewModel.executeIssueAction(issueUiData.issue, action, issueUiData.launchTaskId) + viewModel.interactionLogger.recordForIssue( + if (isPrimaryButton) { + Action.ISSUE_PRIMARY_ACTION_CLICKED + } else { + Action.ISSUE_SECONDARY_ACTION_CLICKED + }, + issueUiData.issue, + issueUiData.isDismissed, + ) + } + } + } + + private fun maybeStartResolution() { + val resolvedActionId = issueUiData.resolvedIssueActionId ?: return + + val action = issueUiData.issue.actions.firstOrNull { it.id == resolvedActionId } ?: return + val successMessage = + action.successMessage?.ifEmpty { null } + ?: context.getString(R.string.safety_center_resolved_issue_fallback) + + showResolutionAnimation(successMessage) { + viewModel.markIssueResolvedUiCompleted(issueUiData.issue.id) + } + } + + private fun Int.toAttentionLevel(): AttentionLevel { + return when (this) { + SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK -> AttentionLevel.LOW + SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION -> AttentionLevel.MEDIUM + SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING -> AttentionLevel.HIGH + else -> { + Log.w(TAG, "Unexpected issue severity level $this") + AttentionLevel.LOW + } + } + } + + private companion object { + const val TAG = "SafetyBannerMessagePref" + } + + override fun isSameItem(preference: Preference): Boolean = + preference is SafetyBannerMessagePreference && + preference.issueUiData.issue.id == issueUiData.issue.id + + override fun hasSameContents(preference: Preference): Boolean = + preference is SafetyBannerMessagePreference && preference.issueUiData == issueUiData +} diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/SafetyCenterUiData.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/SafetyCenterUiData.kt index d8aadae2f..1595b5812 100644 --- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/SafetyCenterUiData.kt +++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/SafetyCenterUiData.kt @@ -39,9 +39,7 @@ data class SafetyCenterUiData( val issueUiDatas: List<IssueUiData> by lazy(LazyThreadSafetyMode.NONE) { - safetyCenterData.issues.map { - IssueUiData(it, false, resolvedIssues[it.id], getLaunchTaskIdForIssue(it)) - } + safetyCenterData.issues.map { toIssueUiData(it, isDismissed = false) } } fun getMatchingIssue(issueKey: SafetyCenterIssueKey): SafetyCenterIssue? { @@ -92,9 +90,13 @@ data class SafetyCenterUiData( /** Returns the [SafetyCenterData.getDismissedIssues] that are meant to be visible in the UI. */ @RequiresApi(UPSIDE_DOWN_CAKE) - fun SafetyCenterData.visibleDismissedIssues() = + private fun SafetyCenterData.visibleDismissedIssues() = dismissedIssues.filter { it.severityLevel > ISSUE_SEVERITY_LEVEL_OK } + /** Converts a [SafetyCenterIssue] into [IssueUiData]. */ + private fun toIssueUiData(issue: SafetyCenterIssue, isDismissed: Boolean) = + IssueUiData(issue, isDismissed, resolvedIssues[issue.id], getLaunchTaskIdForIssue(issue)) + private fun getLaunchTaskIdForIssue(issue: SafetyCenterIssue): Int? { val sourceId: String = SafetyCenterIds.issueIdFromString(issue.id) diff --git a/tests/functional/safetycenter/safetycenteractivity/Android.bp b/tests/functional/safetycenter/safetycenteractivity/Android.bp index ea5f9f286..2346a0d5f 100644 --- a/tests/functional/safetycenter/safetycenteractivity/Android.bp +++ b/tests/functional/safetycenter/safetycenteractivity/Android.bp @@ -29,6 +29,7 @@ android_test { "src/**/*.kt", ], static_libs: [ + "aconfig_settingstheme_exported_flags_java_lib", "androidx.test.rules", "androidx.test.ext.junit", "compatibility-device-preconditions", diff --git a/tests/functional/safetycenter/safetycenteractivity/src/android/safetycenter/functional/ui/SafetyCenterActivityTest.kt b/tests/functional/safetycenter/safetycenteractivity/src/android/safetycenter/functional/ui/SafetyCenterActivityTest.kt index 09a32f058..fb577e8f6 100644 --- a/tests/functional/safetycenter/safetycenteractivity/src/android/safetycenter/functional/ui/SafetyCenterActivityTest.kt +++ b/tests/functional/safetycenter/safetycenteractivity/src/android/safetycenter/functional/ui/SafetyCenterActivityTest.kt @@ -21,6 +21,9 @@ import android.os.Build import android.os.Build.VERSION_CODES.TIRAMISU import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE import android.os.Bundle +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCE_ID import android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCE_ISSUE_ID import android.safetycenter.SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING @@ -74,6 +77,7 @@ import com.android.safetycenter.testing.UiTestHelper.waitPageTitleDisplayed import com.android.safetycenter.testing.UiTestHelper.waitSourceDataDisplayed import com.android.safetycenter.testing.UiTestHelper.waitSourceIssueDisplayed import com.android.safetycenter.testing.UiTestHelper.waitSourceIssueNotDisplayed +import com.android.settingslib.widget.theme.flags.Flags as SettingsThemeFlags import java.util.regex.Pattern import org.junit.After import org.junit.Assume.assumeFalse @@ -95,6 +99,8 @@ class SafetyCenterActivityTest { @get:Rule(order = 2) val safetyCenterTestRule = SafetyCenterTestRule(safetyCenterTestHelper) @get:Rule(order = 3) val disableAnimationRule = DisableAnimationRule() @get:Rule(order = 4) val freezeRotationRule = FreezeRotationRule() + @get:Rule(order = 5) + val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() @After fun clearDataAfterTest() { @@ -567,6 +573,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/398188361 - Update this for expressive theme + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun issueCard_noAttribution_hasProperContentDescriptions() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.issueOnlySourceNoGroupTitleConfig) @@ -581,6 +589,8 @@ class SafetyCenterActivityTest { @Test @SdkSuppress(minSdkVersion = UPSIDE_DOWN_CAKE) + // TODO: b/398188361 - Update this for expressive theme + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun issueCard_withAttribution_hasProperContentDescriptions() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.singleSourceConfig) @@ -693,7 +703,7 @@ class SafetyCenterActivityTest { @Test fun issueCard_resolveIssue_successConfirmationShown() { - SafetyCenterFlags.hideResolvedIssueUiTransitionDelay = TIMEOUT_LONG + SafetyCenterFlags.setHideResolvedIssueUiTransitionDelay(context, TIMEOUT_LONG) safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.singleSourceConfig) // Set the initial data for the source @@ -829,7 +839,7 @@ class SafetyCenterActivityTest { @Test fun issueCard_resolveIssue_noSuccessMessage_noResolutionUiShown_issueDismisses() { - SafetyCenterFlags.hideResolvedIssueUiTransitionDelay = TIMEOUT_LONG + SafetyCenterFlags.setHideResolvedIssueUiTransitionDelay(context, TIMEOUT_LONG) safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.singleSourceConfig) // Set the initial data for the source @@ -954,6 +964,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun launchActivity_fromQuickSettings_issuesExpanded() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( @@ -978,6 +990,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun launchActivity_fromNotification_targetIssueAlreadyFirstIssue() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( @@ -1003,6 +1017,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun launchActivity_fromNotification_targetIssueSamePriorityAsFirstIssue_reorderedFirstIssue() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( @@ -1028,6 +1044,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun launchActivity_fromNotification_targetLowerPriorityAsFirstIssue_reorderedSecondIssue() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( @@ -1052,6 +1070,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun launchActivity_fromNotification_targetIssueNotFound() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( @@ -1091,6 +1111,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun moreIssuesCard_moreIssuesCardShown_additionalIssueCardsCollapsed() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( @@ -1113,6 +1135,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun moreIssuesCard_expandAdditionalIssueCards() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( @@ -1139,6 +1163,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun moreIssuesCard_rotation_cardsStillExpanded() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( @@ -1173,6 +1199,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun moreIssuesCard_withThreeIssues_showsTopIssuesAndMoreIssuesCard() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( @@ -1197,6 +1225,8 @@ class SafetyCenterActivityTest { } @Test + // TODO: b/379849464 - Fix this for expressive design and stop disabling this flag + @RequiresFlagsDisabled(SettingsThemeFlags.FLAG_IS_EXPRESSIVE_DESIGN_ENABLED) fun moreIssuesCard_twoIssuesAlreadyShown_expandAdditionalIssueCards() { safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig) safetyCenterTestHelper.setData( diff --git a/tests/utils/safetycenter/Android.bp b/tests/utils/safetycenter/Android.bp index fab8c8dde..11fd3951d 100644 --- a/tests/utils/safetycenter/Android.bp +++ b/tests/utils/safetycenter/Android.bp @@ -36,6 +36,7 @@ android_library { "kotlinx-coroutines-android", "safety-center-internal-data", "safety-center-resources-lib", + "SettingsLibSettingsTheme", // TODO(b/326414126): aconfig: support multi-container library "com.android.permission.flags-aconfig-java", ], diff --git a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt index 7efbba7a0..66c5f46c4 100644 --- a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt +++ b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt @@ -21,6 +21,7 @@ import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG import android.Manifest.permission.WRITE_DEVICE_CONFIG import android.annotation.TargetApi import android.app.job.JobInfo +import android.content.Context import android.content.pm.PackageManager import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE import android.provider.DeviceConfig @@ -38,12 +39,16 @@ import com.android.modules.utils.build.SdkLevel import com.android.safetycenter.testing.Coroutines.TEST_TIMEOUT import com.android.safetycenter.testing.Coroutines.TIMEOUT_LONG import com.android.safetycenter.testing.ShellPermissions.callWithShellPermissionIdentity +import com.android.settingslib.widget.SettingsThemeHelper import java.time.Duration import kotlin.reflect.KProperty /** A class that facilitates working with Safety Center flags. */ object SafetyCenterFlags { + /** This is a hidden API constant within [DeviceConfig]. */ + private const val NAMESPACE_SETTINGS_UI = "settings_ui" + /** Flag that determines whether Safety Center is enabled. */ private val isEnabledFlag = Flag("safety_center_is_enabled", defaultValue = SdkLevel.isAtLeastU(), BooleanParser()) @@ -143,8 +148,7 @@ object SafetyCenterFlags { ) /** - * Flag that determines the time for which Safety Center will wait before starting dismissal of - * resolved issue UI + * Flag that determines how long Safety Center will wait before hiding the resolved issue UI. */ private val hideResolveUiTransitionDelayFlag = Flag( @@ -154,6 +158,18 @@ object SafetyCenterFlags { ) /** + * Flag that determines how long an expressive BannerMessagePreference will wait before hiding + * the resolved UI. + */ + private val bannerMessagePrefHideResolvedContentTransitionDelayFlag = + Flag( + "banner_message_pref_hide_resolved_content_delay_millis", + defaultValue = Duration.ofMillis(400), + DurationParser(), + namespace = NAMESPACE_SETTINGS_UI, + ) + + /** * Flag containing a comma delimited lists of source IDs that we won't track when deciding if a * broadcast is completed. We still send broadcasts to (and handle API calls from) these sources * as normal. @@ -312,6 +328,7 @@ object SafetyCenterFlags { resolveActionTimeoutFlag, tempHiddenIssueResurfaceDelayFlag, hideResolveUiTransitionDelayFlag, + bannerMessagePrefHideResolvedContentTransitionDelayFlag, untrackedSourcesFlag, resurfaceIssueMaxCountsFlag, resurfaceIssueDelaysFlag, @@ -357,9 +374,28 @@ object SafetyCenterFlags { /** A property that allows getting and setting the [tempHiddenIssueResurfaceDelayFlag]. */ var tempHiddenIssueResurfaceDelay: Duration by tempHiddenIssueResurfaceDelayFlag + // TODO: b/379849464 - replace remaining usages and make this private /** A property that allows getting and setting the [hideResolveUiTransitionDelayFlag]. */ var hideResolvedIssueUiTransitionDelay: Duration by hideResolveUiTransitionDelayFlag + /** + * A property that allows getting and setting the + * [bannerMessagePrefHideResolvedContentTransitionDelayFlag] + */ + private var bannerMessagePrefHideResolvedContentTransitionDelay: Duration by + bannerMessagePrefHideResolvedContentTransitionDelayFlag + + /** + * Sets the proper hide_resolved_issue_ui_transition_delay flag based on expressive design + * state. + */ + fun setHideResolvedIssueUiTransitionDelay(context: Context, value: Duration) = + if (SettingsThemeHelper.isExpressiveTheme(context)) { + bannerMessagePrefHideResolvedContentTransitionDelay = value + } else { + hideResolvedIssueUiTransitionDelay = value + } + /** A property that allows getting and setting the [untrackedSourcesFlag]. */ var untrackedSources: Set<String> by untrackedSourcesFlag @@ -396,14 +432,23 @@ object SafetyCenterFlags { * This snapshot is only taken once and cached afterwards. [setup] must be called at least once * prior to modifying any flag for the snapshot to be taken with the right values. */ - @Volatile lateinit var snapshot: Properties + @Volatile lateinit var snapshot: Map<String, Properties> - private val lazySnapshot: Properties by lazy { + private val lazySnapshot: Map<String, Properties> by lazy { callWithShellPermissionIdentity(READ_DEVICE_CONFIG) { - DeviceConfig.getProperties(NAMESPACE_PRIVACY, *FLAGS.map { it.name }.toTypedArray()) + mapOf( + NAMESPACE_PRIVACY to fetchPropertiesForNamespace(NAMESPACE_PRIVACY), + NAMESPACE_SETTINGS_UI to fetchPropertiesForNamespace(NAMESPACE_SETTINGS_UI), + ) } } + private fun fetchPropertiesForNamespace(namespace: String) = + DeviceConfig.getProperties( + namespace, + *FLAGS.filter { it.namespace == namespace }.map { it.name }.toTypedArray(), + ) + /** * Takes a snapshot of all Safety Center flags and sets them up to their default values. * @@ -414,7 +459,7 @@ object SafetyCenterFlags { fun setup() { snapshot = lazySnapshot FLAGS.filter { it.name != isEnabledFlag.name } - .forEach { writeDeviceConfigProperty(it.name, it.defaultStringValue) } + .forEach { it.writeToDeviceConfig(it.defaultStringValue) } } /** @@ -431,8 +476,8 @@ object SafetyCenterFlags { FLAGS.filter { it.name != isEnabledFlag.name } .forEach { val key = it.name - val value = snapshot.getString(key, /* defaultValue */ null) - writeDeviceConfigProperty(key, value) + val value = snapshot[it.namespace]?.getString(key, /* defaultValue */ null) + it.writeToDeviceConfig(value) } } @@ -442,8 +487,8 @@ object SafetyCenterFlags { } /** Returns the [isEnabledFlag] value of the Safety Center flags snapshot. */ - fun Properties.isSafetyCenterEnabled() = - getBoolean(isEnabledFlag.name, isEnabledFlag.defaultValue) + fun Map<String, Properties>.isSafetyCenterEnabled(): Boolean = + this[NAMESPACE_PRIVACY]!!.getBoolean(isEnabledFlag.name, isEnabledFlag.defaultValue) @TargetApi(UPSIDE_DOWN_CAKE) private fun getAllRefreshTimeoutsMap(refreshTimeout: Duration): Map<Int, Duration> = @@ -516,32 +561,32 @@ object SafetyCenterFlags { .joinToString(entriesDelimiter) } - private class Flag<T>(val name: String, val defaultValue: T, private val parser: Parser<T>) { + private class Flag<T>( + val name: String, + val defaultValue: T, + private val parser: Parser<T>, + val namespace: String = NAMESPACE_PRIVACY, + ) { val defaultStringValue = parser.toString(defaultValue) operator fun getValue(thisRef: Any?, property: KProperty<*>): T = - readDeviceConfigProperty(name)?.let(parser::parseFromString) ?: defaultValue + readFromDeviceConfig(name)?.let(parser::parseFromString) ?: defaultValue - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - writeDeviceConfigProperty(name, parser.toString(value)) - } - } + private fun readFromDeviceConfig(name: String): String? = + callWithShellPermissionIdentity(READ_DEVICE_CONFIG) { + DeviceConfig.getProperty(namespace, name) + } - private fun readDeviceConfigProperty(name: String): String? = - callWithShellPermissionIdentity(READ_DEVICE_CONFIG) { - DeviceConfig.getProperty(NAMESPACE_PRIVACY, name) + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + writeToDeviceConfig(parser.toString(value)) } - private fun writeDeviceConfigProperty(name: String, stringValue: String?) { - callWithShellPermissionIdentity(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) { - val valueWasSet = - DeviceConfig.setProperty( - NAMESPACE_PRIVACY, - name, - stringValue, /* makeDefault */ - false, - ) - require(valueWasSet) { "Could not set $name to: $stringValue" } + fun writeToDeviceConfig(stringValue: String?) { + callWithShellPermissionIdentity(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) { + val valueWasSet = + DeviceConfig.setProperty(namespace, name, stringValue, /* makeDefault */ false) + require(valueWasSet) { "Could not set $name to: $stringValue" } + } } } } |