diff options
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" } + } } } } |