1 /* <lambda>null2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.permissioncontroller.safetycenter.ui 18 19 import android.content.Context 20 import android.content.Intent 21 import android.content.Intent.ACTION_SAFETY_CENTER 22 import android.os.Build 23 import android.os.Bundle 24 import android.safetycenter.SafetyCenterIssue 25 import android.safetycenter.SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK 26 import androidx.annotation.RequiresApi 27 import androidx.fragment.app.FragmentManager 28 import androidx.preference.PreferenceGroup 29 import com.android.permissioncontroller.R 30 import com.android.permissioncontroller.safetycenter.SafetyCenterConstants.EXPAND_ISSUE_GROUP_QS_FRAGMENT_KEY 31 import com.android.permissioncontroller.safetycenter.ui.model.ActionId 32 import com.android.permissioncontroller.safetycenter.ui.model.IssueId 33 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel 34 import com.android.safetycenter.internaldata.SafetyCenterIds 35 import com.android.safetycenter.internaldata.SafetyCenterIssueKey 36 import kotlin.math.max 37 38 /** 39 * Helper class to hide issue cards if over a predefined limit and handle revealing hidden issue 40 * cards when the more issues preference is clicked 41 */ 42 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 43 class CollapsableIssuesCardHelper( 44 val safetyCenterViewModel: SafetyCenterViewModel, 45 val sameTaskIssueIds: List<String> 46 ) { 47 private var isQuickSettingsFragment: Boolean = false 48 private var issueCardsExpanded: Boolean = false 49 private var focusedSafetyCenterIssueKey: SafetyCenterIssueKey? = null 50 private var previousMoreIssuesCardData: MoreIssuesCardData? = null 51 52 fun setFocusedIssueKey(safetyCenterIssueKey: SafetyCenterIssueKey?) { 53 focusedSafetyCenterIssueKey = safetyCenterIssueKey 54 } 55 56 /** 57 * Sets QuickSetting specific state for use to determine correct issue section expansion state 58 * as well ass more issues card icon values 59 * 60 * <p> Note the issueCardsExpanded value set here may be overridden here by calls to 61 * restoreState 62 * 63 * @param isQuickSettingsFragment {@code true} if CollapsableIssuesCardHelper is being used in 64 * quick settings fragment 65 * @param issueCardsExpanded Whether issue cards should be expanded or not when added to 66 * preference screen 67 */ 68 fun setQuickSettingsState(isQuickSettingsFragment: Boolean, issueCardsExpanded: Boolean) { 69 this.isQuickSettingsFragment = isQuickSettingsFragment 70 this.issueCardsExpanded = issueCardsExpanded 71 } 72 73 /** Restore previously saved state from [Bundle] */ 74 fun restoreState(state: Bundle?) { 75 if (state == null) { 76 return 77 } 78 // Apply the previously saved state 79 issueCardsExpanded = state.getBoolean(EXPAND_ISSUE_GROUP_SAVED_INSTANCE_STATE_KEY, false) 80 } 81 82 /** Save current state to provided [Bundle] */ 83 fun saveState(outState: Bundle) = 84 outState.putBoolean(EXPAND_ISSUE_GROUP_SAVED_INSTANCE_STATE_KEY, issueCardsExpanded) 85 86 /** 87 * Add the [IssueCardPreference] managed by this helper to the specified [PreferenceGroup] 88 * 89 * @param context Current context 90 * @param safetyCenterViewModel {@link SafetyCenterViewModel} used when executing issue actions 91 * @param dialogFragmentManager fragment manager use for issue dismissal 92 * @param issuesPreferenceGroup Preference group to add preference to 93 * @param issues {@link List} of {@link SafetyCenterIssue} to add to the preference fragment 94 * @param dismissedIssues {@link List} of dismissed {@link SafetyCenterIssue} to add 95 * @param resolvedIssues {@link Map} of issue id to action ids of resolved issues 96 */ 97 fun addIssues( 98 context: Context, 99 safetyCenterViewModel: SafetyCenterViewModel, 100 dialogFragmentManager: FragmentManager, 101 issuesPreferenceGroup: PreferenceGroup, 102 issues: List<SafetyCenterIssue>?, 103 dismissedIssues: List<SafetyCenterIssue>?, 104 resolvedIssues: Map<IssueId, ActionId>, 105 launchTaskId: Int 106 ) { 107 val (reorderedIssues, numberOfIssuesToShowWhenCollapsed) = 108 maybeReorderFocusedSafetyCenterIssueInList(issues) 109 110 val onlyDismissedIssuesAreCollapsed = 111 reorderedIssues.size <= numberOfIssuesToShowWhenCollapsed 112 113 val issueCardPreferences: List<IssueCardPreference> = 114 reorderedIssues.mapToIssueCardPreferences( 115 resolvedIssues, 116 launchTaskId, 117 context, 118 safetyCenterViewModel, 119 dialogFragmentManager, 120 areDismissed = false 121 ) { index -> 122 when (index) { 123 in 0 until numberOfIssuesToShowWhenCollapsed -> 124 PositionInCardList.LIST_START_END 125 this.size - 1 -> PositionInCardList.CARD_START_LIST_END 126 else -> PositionInCardList.CARD_START_END 127 } 128 } 129 130 val dismissedIssueCardPreferences: List<IssueCardPreference> = 131 dismissedIssues.mapToIssueCardPreferences( 132 resolvedIssues, 133 launchTaskId, 134 context, 135 safetyCenterViewModel, 136 dialogFragmentManager, 137 areDismissed = true 138 ) { index -> 139 when { 140 onlyDismissedIssuesAreCollapsed && index == size - 1 -> 141 PositionInCardList.CARD_START_LIST_END 142 onlyDismissedIssuesAreCollapsed -> PositionInCardList.CARD_START_END 143 size == 1 -> PositionInCardList.LIST_START_END 144 index == 0 -> PositionInCardList.LIST_START_CARD_END 145 index == size - 1 -> PositionInCardList.CARD_START_LIST_END 146 else -> PositionInCardList.CARD_START_END 147 } 148 } 149 150 val nextMoreIssuesCardData = 151 createMoreIssuesCardData( 152 issueCardPreferences, 153 dismissedIssueCardPreferences, 154 numberOfIssuesToShowWhenCollapsed 155 ) 156 157 val moreIssuesCardPreference = 158 createMoreIssuesCardPreference( 159 context, 160 dismissedOnly = onlyDismissedIssuesAreCollapsed, 161 staticHeader = false, 162 issuesPreferenceGroup, 163 previousMoreIssuesCardData, 164 nextMoreIssuesCardData, 165 numberOfIssuesToShowWhenCollapsed 166 ) 167 168 val dismissedIssuesHeaderCardPreference = 169 if (!onlyDismissedIssuesAreCollapsed && dismissedIssueCardPreferences.isNotEmpty()) { 170 createMoreIssuesCardPreference( 171 context, 172 dismissedOnly = false, 173 staticHeader = true, 174 issuesPreferenceGroup, 175 previousMoreIssuesCardData, 176 nextMoreIssuesCardData, 177 numberOfIssuesToShowWhenCollapsed 178 ) 179 } else { 180 null 181 } 182 183 // Keep track of previously presented more issues data to assist with transitions 184 previousMoreIssuesCardData = nextMoreIssuesCardData 185 186 addIssuesToPreferenceGroupAndSetVisibility( 187 issuesPreferenceGroup, 188 issueCardPreferences, 189 dismissedIssueCardPreferences, 190 moreIssuesCardPreference, 191 dismissedIssuesHeaderCardPreference, 192 numberOfIssuesToShowWhenCollapsed, 193 issueCardsExpanded 194 ) 195 } 196 197 private fun List<SafetyCenterIssue>?.mapToIssueCardPreferences( 198 resolvedIssues: Map<IssueId, ActionId>, 199 launchTaskId: Int, 200 context: Context, 201 safetyCenterViewModel: SafetyCenterViewModel, 202 dialogFragmentManager: FragmentManager, 203 areDismissed: Boolean, 204 position: List<SafetyCenterIssue>.(index: Int) -> PositionInCardList 205 ): List<IssueCardPreference> = 206 this?.mapIndexed { index: Int, issue: SafetyCenterIssue -> 207 val resolvedActionId: ActionId? = resolvedIssues[issue.id] 208 val resolvedTaskId = getLaunchTaskIdForIssue(issue, launchTaskId) 209 val positionInCardList = position(index) 210 IssueCardPreference( 211 context, 212 safetyCenterViewModel, 213 issue, 214 resolvedActionId, 215 dialogFragmentManager, 216 resolvedTaskId, 217 areDismissed, 218 positionInCardList 219 ) 220 } 221 ?: emptyList() 222 223 data class ReorderedSafetyCenterIssueList( 224 val issues: List<SafetyCenterIssue>, 225 val numberOfIssuesToShowWhenCollapsed: Int 226 ) 227 private fun maybeReorderFocusedSafetyCenterIssueInList( 228 issues: List<SafetyCenterIssue>? 229 ): ReorderedSafetyCenterIssueList { 230 if (issues == null) { 231 return ReorderedSafetyCenterIssueList( 232 emptyList(), 233 numberOfIssuesToShowWhenCollapsed = 0 234 ) 235 } 236 val mutableIssuesList = issues.toMutableList() 237 val focusedIssue: SafetyCenterIssue? = 238 focusedSafetyCenterIssueKey?.let { findAndRemoveIssueInList(it, mutableIssuesList) } 239 240 // If focused issue preference found, place at/near top of list and return new list and 241 // correct number of issue to show while collapsed 242 if (focusedIssue != null) { 243 val focusedIssuePlacement = getFocusedIssuePlacement(focusedIssue, mutableIssuesList) 244 mutableIssuesList.add(focusedIssuePlacement.index, focusedIssue) 245 return ReorderedSafetyCenterIssueList( 246 mutableIssuesList.toList(), 247 focusedIssuePlacement.numberForShownIssuesCollapsed 248 ) 249 } 250 251 return ReorderedSafetyCenterIssueList(issues, DEFAULT_NUMBER_SHOWN_ISSUES_COLLAPSED) 252 } 253 254 private fun findAndRemoveIssueInList( 255 focusedIssueKey: SafetyCenterIssueKey, 256 issues: MutableList<SafetyCenterIssue> 257 ): SafetyCenterIssue? { 258 issues.forEachIndexed { index, issue -> 259 val issueKey = SafetyCenterIds.issueIdFromString(issue.id).safetyCenterIssueKey 260 if (focusedIssueKey == issueKey) { 261 // Remove focused issue from current placement in list and exit loop 262 issues.removeAt(index) 263 return issue 264 } 265 } 266 267 return null 268 } 269 270 /** Defines indices and number of shown issues for use when prioritizing focused issues */ 271 private enum class FocusedIssuePlacement( 272 val index: Int, 273 val numberForShownIssuesCollapsed: Int 274 ) { 275 FOCUSED_ISSUE_INDEX_0(0, DEFAULT_NUMBER_SHOWN_ISSUES_COLLAPSED), 276 FOCUSED_ISSUE_INDEX_1(1, DEFAULT_NUMBER_SHOWN_ISSUES_COLLAPSED + 1) 277 } 278 279 private fun getFocusedIssuePlacement( 280 issue: SafetyCenterIssue, 281 issueList: List<SafetyCenterIssue> 282 ): FocusedIssuePlacement { 283 return if (issueList.isEmpty() || issueList[0].severityLevel <= issue.severityLevel) { 284 FocusedIssuePlacement.FOCUSED_ISSUE_INDEX_0 285 } else { 286 FocusedIssuePlacement.FOCUSED_ISSUE_INDEX_1 287 } 288 } 289 290 private fun createMoreIssuesCardData( 291 issueCardPreferences: List<IssueCardPreference>, 292 dismissedIssueCardPreferences: List<IssueCardPreference>, 293 numberOfIssuesToShowWhenCollapsed: Int 294 ): MoreIssuesCardData { 295 val numberOfHiddenIssue: Int = 296 getNumberOfHiddenIssues( 297 issueCardPreferences, 298 dismissedIssueCardPreferences, 299 numberOfIssuesToShowWhenCollapsed 300 ) 301 val firstHiddenIssueSeverityLevel: Int = 302 if (issueCardPreferences.size <= numberOfIssuesToShowWhenCollapsed) { 303 getFirstHiddenIssueSeverityLevel(dismissedIssueCardPreferences, 0) 304 } else { 305 getFirstHiddenIssueSeverityLevel( 306 issueCardPreferences, 307 numberOfIssuesToShowWhenCollapsed 308 ) 309 } 310 311 return MoreIssuesCardData( 312 firstHiddenIssueSeverityLevel, 313 numberOfHiddenIssue, 314 issueCardsExpanded 315 ) 316 } 317 318 private fun createMoreIssuesCardPreference( 319 context: Context, 320 dismissedOnly: Boolean, 321 staticHeader: Boolean, 322 issuesPreferenceGroup: PreferenceGroup, 323 previousMoreIssuesCardData: MoreIssuesCardData?, 324 nextMoreIssuesCardData: MoreIssuesCardData, 325 numberOfIssuesToShowWhenCollapsed: Int 326 ): MoreIssuesCardPreference { 327 val overrideChevronIconResId = 328 if (isQuickSettingsFragment) R.drawable.ic_chevron_right else null 329 330 return MoreIssuesCardPreference( 331 context, 332 overrideChevronIconResId, 333 previousMoreIssuesCardData, 334 nextMoreIssuesCardData, 335 dismissedOnly, 336 staticHeader 337 ) { 338 if (isQuickSettingsFragment) { 339 goToSafetyCenter(context) 340 } else { 341 setExpanded( 342 issuesPreferenceGroup, 343 !issueCardsExpanded, 344 numberOfIssuesToShowWhenCollapsed 345 ) 346 } 347 safetyCenterViewModel.interactionLogger.record(Action.MORE_ISSUES_CLICKED) 348 } 349 } 350 351 private fun setExpanded( 352 issuesPreferenceGroup: PreferenceGroup, 353 isExpanded: Boolean, 354 numberOfIssuesToShowWhenCollapsed: Int 355 ) { 356 if (issueCardsExpanded == isExpanded) { 357 return 358 } 359 360 val numberOfPreferences = issuesPreferenceGroup.preferenceCount 361 for (i in 0 until numberOfPreferences) { 362 when (val preference = issuesPreferenceGroup.getPreference(i)) { 363 // IssueCardPreference can all be visible now 364 is IssueCardPreference -> 365 preference.isVisible = isExpanded || i < numberOfIssuesToShowWhenCollapsed 366 // MoreIssuesCardPreference must be hidden after expansion of issues 367 is MoreIssuesCardPreference -> { 368 if (preference.isStaticHeader) { 369 preference.isVisible = isExpanded 370 } else { 371 previousMoreIssuesCardData?.let { 372 val newMoreIssuesCardData = it.copy(isExpanded = isExpanded) 373 preference.setNewMoreIssuesCardData(newMoreIssuesCardData) 374 previousMoreIssuesCardData = newMoreIssuesCardData 375 } 376 } 377 preference.isVisible = isExpanded || !preference.isStaticHeader 378 } 379 // Other types are undefined, no-op 380 else -> continue 381 } 382 } 383 issueCardsExpanded = isExpanded 384 } 385 386 private fun goToSafetyCenter(context: Context) { 387 // Navigate to Safety center with issues expanded 388 val safetyCenterIntent = Intent(ACTION_SAFETY_CENTER) 389 safetyCenterIntent.putExtra(EXPAND_ISSUE_GROUP_QS_FRAGMENT_KEY, true) 390 NavigationSource.QUICK_SETTINGS_TILE.addToIntent(safetyCenterIntent) 391 context.startActivity(safetyCenterIntent) 392 } 393 394 companion object { 395 private const val EXPAND_ISSUE_GROUP_SAVED_INSTANCE_STATE_KEY = 396 "expand_issue_group_saved_instance_state_key" 397 private const val DEFAULT_NUMBER_SHOWN_ISSUES_COLLAPSED = 1 398 399 private fun getNumberOfHiddenIssues( 400 issueCardPreferences: List<IssueCardPreference>, 401 dismissedIssueCardPreferences: List<IssueCardPreference>, 402 numberOfIssuesToShowWhenCollapsed: Int 403 ): Int = 404 max(0, issueCardPreferences.size - numberOfIssuesToShowWhenCollapsed) + 405 dismissedIssueCardPreferences.size 406 407 private fun getFirstHiddenIssueSeverityLevel( 408 issueCardPreferences: List<IssueCardPreference>, 409 numberOfIssuesToShowWhenCollapsed: Int 410 ): Int { 411 // Index of first hidden issue (zero based) is equal to number of shown issues when 412 // collapsed 413 val indexOfFirstHiddenIssue: Int = numberOfIssuesToShowWhenCollapsed 414 val firstHiddenIssue: IssueCardPreference? = 415 issueCardPreferences.getOrNull(indexOfFirstHiddenIssue) 416 // If no first hidden issue, default to ISSUE_SEVERITY_LEVEL_OK 417 return firstHiddenIssue?.severityLevel ?: ISSUE_SEVERITY_LEVEL_OK 418 } 419 420 private fun addIssuesToPreferenceGroupAndSetVisibility( 421 issuesPreferenceGroup: PreferenceGroup, 422 issueCardPreferences: List<IssueCardPreference>, 423 dismissedIssueCardPreferences: List<IssueCardPreference>, 424 moreIssuesCardPreference: MoreIssuesCardPreference, 425 dismissedIssuesHeaderPreference: MoreIssuesCardPreference?, 426 numberOfIssuesToShowWhenCollapsed: Int, 427 issueCardsExpanded: Boolean 428 ) { 429 // Index of first hidden issue (zero based) is equal to number of shown issues when 430 // collapsed 431 val indexOfFirstHiddenIssue: Int = numberOfIssuesToShowWhenCollapsed 432 issueCardPreferences.forEachIndexed { index, issueCardPreference -> 433 if (index == indexOfFirstHiddenIssue) { 434 issuesPreferenceGroup.addPreference(moreIssuesCardPreference) 435 } 436 issueCardPreference.isVisible = 437 index < indexOfFirstHiddenIssue || issueCardsExpanded 438 issuesPreferenceGroup.addPreference(issueCardPreference) 439 } 440 if (dismissedIssueCardPreferences.isNotEmpty()) { 441 if (issueCardPreferences.size <= numberOfIssuesToShowWhenCollapsed) { 442 issuesPreferenceGroup.addPreference(moreIssuesCardPreference) 443 } 444 dismissedIssuesHeaderPreference?.let { 445 it.isVisible = issueCardsExpanded 446 issuesPreferenceGroup.addPreference(it) 447 } 448 dismissedIssueCardPreferences.forEach { 449 it.isVisible = issueCardsExpanded 450 issuesPreferenceGroup.addPreference(it) 451 } 452 } 453 } 454 } 455 456 private fun getLaunchTaskIdForIssue(issue: SafetyCenterIssue, taskId: Int): Int? { 457 val issueId: String = 458 SafetyCenterIds.issueIdFromString(issue.id) 459 .getSafetyCenterIssueKey() 460 .getSafetySourceId() 461 return if (sameTaskIssueIds.contains(issueId)) taskId else null 462 } 463 } 464