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.view 18 19 import android.content.Context 20 import android.graphics.drawable.Animatable2.AnimationCallback 21 import android.graphics.drawable.AnimatedVectorDrawable 22 import android.graphics.drawable.Drawable 23 import android.os.Build 24 import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION 25 import android.safetycenter.SafetyCenterEntryGroup 26 import android.util.AttributeSet 27 import android.view.Gravity 28 import android.view.View 29 import android.view.ViewGroup 30 import android.widget.ImageView 31 import android.widget.LinearLayout 32 import android.widget.TextView 33 import androidx.annotation.DrawableRes 34 import androidx.annotation.RequiresApi 35 import androidx.core.view.ViewCompat 36 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat 37 import androidx.transition.AutoTransition 38 import androidx.transition.TransitionManager 39 import com.android.permissioncontroller.R 40 import com.android.permissioncontroller.safetycenter.ui.PositionInCardList 41 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel 42 43 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 44 internal class SafetyEntryGroupView 45 @JvmOverloads 46 constructor( 47 context: Context?, 48 attrs: AttributeSet? = null, 49 defStyleAttr: Int = 0, 50 defStyleRes: Int = 0 51 ) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) { 52 53 private companion object { 54 val TAG = SafetyEntryGroupView::class.java.simpleName 55 const val EXPAND_COLLAPSE_ANIMATION_DURATION_MS = 183L 56 } 57 58 init { 59 inflate(context, R.layout.safety_center_group, this) 60 } 61 62 private val groupHeaderView: LinearLayout? by lazy { findViewById(R.id.group_header) } 63 64 private val expandedHeaderView: ViewGroup? by lazy { findViewById(R.id.expanded_header) } 65 private val expandedTitleView: TextView? by lazy { 66 expandedHeaderView?.findViewById(R.id.title) 67 } 68 69 private val collapsedHeaderView: ViewGroup? by lazy { findViewById(R.id.collapsed_header) } 70 private val commonEntryView: SafetyEntryCommonViewsManager? by lazy { 71 SafetyEntryCommonViewsManager(collapsedHeaderView) 72 } 73 74 private val chevronIconView: ImageView? by lazy { findViewById(R.id.chevron_icon) } 75 private val entriesContainerView: LinearLayout? by lazy { findViewById(R.id.entries_container) } 76 77 private var isExpanded: Boolean? = null 78 79 fun showGroup( 80 group: SafetyCenterEntryGroup, 81 initiallyExpanded: (String) -> Boolean, 82 isFirstCard: Boolean, 83 isLastCard: Boolean, 84 getTaskIdForEntry: (String) -> Int, 85 viewModel: SafetyCenterViewModel, 86 onGroupExpanded: (String) -> Unit, 87 onGroupCollapsed: (String) -> Unit 88 ) { 89 applyPosition(isFirstCard, isLastCard) 90 showGroupDetails(group) 91 showGroupEntries(group, getTaskIdForEntry, viewModel) 92 setupExpandedState(group, initiallyExpanded(group.id)) 93 setOnClickListener { toggleExpandedState(group, onGroupExpanded, onGroupCollapsed) } 94 } 95 96 private fun applyPosition(isFirstCard: Boolean, isLastCard: Boolean) { 97 val position = 98 when { 99 isFirstCard && isLastCard -> PositionInCardList.LIST_START_END 100 isFirstCard && !isLastCard -> PositionInCardList.LIST_START_CARD_END 101 !isFirstCard && isLastCard -> PositionInCardList.CARD_START_LIST_END 102 /* !isFirstCard && !isLastCard */ else -> PositionInCardList.CARD_START_END 103 } 104 setBackgroundResource(position.backgroundDrawableResId) 105 val topMargin: Int = position.getTopMargin(context) 106 107 val params = layoutParams as MarginLayoutParams 108 if (params.topMargin != topMargin) { 109 params.topMargin = topMargin 110 layoutParams = params 111 } 112 } 113 114 private fun showGroupDetails(group: SafetyCenterEntryGroup) { 115 expandedTitleView?.text = group.title 116 commonEntryView?.showDetails( 117 group.id, 118 group.title, 119 group.summary, 120 group.severityLevel, 121 group.severityUnspecifiedIconType 122 ) 123 } 124 125 private fun setupExpandedState(group: SafetyCenterEntryGroup, shouldBeExpanded: Boolean) { 126 if (isExpanded == shouldBeExpanded) { 127 return 128 } 129 130 collapsedHeaderView?.visibility = if (shouldBeExpanded) View.GONE else View.VISIBLE 131 expandedHeaderView?.visibility = if (shouldBeExpanded) View.VISIBLE else View.GONE 132 entriesContainerView?.visibility = if (shouldBeExpanded) View.VISIBLE else View.GONE 133 134 if (shouldBeExpanded) { 135 groupHeaderView?.gravity = Gravity.TOP 136 } else { 137 groupHeaderView?.gravity = Gravity.CENTER_VERTICAL 138 } 139 140 if (isExpanded == null) { 141 chevronIconView?.setImageResource( 142 if (shouldBeExpanded) { 143 R.drawable.ic_safety_group_collapse 144 } else { 145 R.drawable.ic_safety_group_expand 146 } 147 ) 148 } else if (shouldBeExpanded) { 149 chevronIconView?.animate( 150 R.drawable.safety_center_group_expand_anim, 151 R.drawable.ic_safety_group_collapse 152 ) 153 } else { 154 chevronIconView?.animate( 155 R.drawable.safety_center_group_collapse_anim, 156 R.drawable.ic_safety_group_expand 157 ) 158 } 159 160 isExpanded = shouldBeExpanded 161 162 val newPaddingTop = 163 context.resources.getDimensionPixelSize( 164 if (shouldBeExpanded) { 165 R.dimen.sc_entry_group_expanded_padding_top 166 } else { 167 R.dimen.sc_entry_group_collapsed_padding_top 168 } 169 ) 170 val newPaddingBottom = 171 context.resources.getDimensionPixelSize( 172 if (shouldBeExpanded) { 173 R.dimen.sc_entry_group_expanded_padding_bottom 174 } else { 175 R.dimen.sc_entry_group_collapsed_padding_bottom 176 } 177 ) 178 setPaddingRelative(paddingStart, newPaddingTop, paddingEnd, newPaddingBottom) 179 180 // accessibility attributes depend on the expanded state 181 // and should be updated every time this state changes 182 setAccessibilityAttributes(group) 183 } 184 185 private fun ImageView.animate(@DrawableRes animationRes: Int, @DrawableRes imageRes: Int) { 186 (drawable as? AnimatedVectorDrawable)?.clearAnimationCallbacks() 187 setImageResource(animationRes) 188 (drawable as? AnimatedVectorDrawable)?.apply { 189 registerAnimationCallback( 190 object : AnimationCallback() { 191 override fun onAnimationEnd(drawable: Drawable?) { 192 setImageResource(imageRes) 193 } 194 } 195 ) 196 start() 197 } 198 } 199 200 private fun showGroupEntries( 201 group: SafetyCenterEntryGroup, 202 getTaskIdForEntry: (String) -> Int, 203 viewModel: SafetyCenterViewModel 204 ) { 205 val entriesCount = group.entries.size 206 val existingViewsCount = entriesContainerView?.childCount ?: 0 207 if (entriesCount > existingViewsCount) { 208 for (i in 1..(entriesCount - existingViewsCount)) { 209 inflate(context, R.layout.safety_center_group_entry, entriesContainerView) 210 } 211 } else if (entriesCount < existingViewsCount) { 212 for (i in 1..(existingViewsCount - entriesCount)) { 213 entriesContainerView?.removeViewAt(0) 214 } 215 } 216 217 group.entries.forEachIndexed { index, entry -> 218 val childAt = entriesContainerView?.getChildAt(index) 219 val entryView = childAt as? SafetyEntryView 220 entryView?.showEntry( 221 entry, 222 PositionInCardList.INSIDE_GROUP, 223 getTaskIdForEntry(entry.id), 224 viewModel 225 ) 226 } 227 } 228 229 private fun setAccessibilityAttributes(group: SafetyCenterEntryGroup) { 230 // When status is yellow/red, adding an "Actions needed" before the summary is read. 231 contentDescription = 232 if (isExpanded == true) { 233 null 234 } else { 235 val isActionNeeded = group.severityLevel >= ENTRY_SEVERITY_LEVEL_RECOMMENDATION 236 val contentDescriptionResId = 237 if (isActionNeeded) { 238 R.string.safety_center_entry_group_with_actions_needed_content_description 239 } else { 240 R.string.safety_center_entry_group_content_description 241 } 242 context.getString(contentDescriptionResId, group.title, group.summary) 243 } 244 245 // Replacing the on-click label to indicate the expand/collapse action. The on-click command 246 // is set to null so that it uses the existing expand/collapse behaviour. 247 val accessibilityActionResId = 248 if (isExpanded == true) { 249 R.string.safety_center_entry_group_collapse_action 250 } else { 251 R.string.safety_center_entry_group_expand_action 252 } 253 ViewCompat.replaceAccessibilityAction( 254 this, 255 AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, 256 context.getString(accessibilityActionResId), 257 null 258 ) 259 } 260 261 private fun toggleExpandedState( 262 group: SafetyCenterEntryGroup, 263 onGroupExpanded: (String) -> Unit, 264 onGroupCollapsed: (String) -> Unit 265 ) { 266 val transition = AutoTransition() 267 transition.duration = EXPAND_COLLAPSE_ANIMATION_DURATION_MS 268 TransitionManager.beginDelayedTransition(rootView as ViewGroup, transition) 269 270 val shouldBeExpanded = isExpanded != true 271 setupExpandedState(group, shouldBeExpanded) 272 273 if (shouldBeExpanded) { 274 onGroupExpanded(group.id) 275 } else { 276 onGroupCollapsed(group.id) 277 } 278 } 279 } 280