diff options
author | 2025-02-21 10:55:53 +0000 | |
---|---|---|
committer | 2025-02-21 15:55:18 +0000 | |
commit | 5098fc7aff57f4819be964c02920a1f815befb55 (patch) | |
tree | 985514378fdf9e791897a018018686af877d8ca2 /PermissionController/src | |
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
Diffstat (limited to 'PermissionController/src')
4 files changed, 223 insertions, 20 deletions
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) |