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