• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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