<lambda>null1 package com.android.permissioncontroller.safetycenter.ui.view
2
3 import android.animation.ValueAnimator
4 import android.content.Context
5 import android.graphics.drawable.Animatable2
6 import android.graphics.drawable.AnimatedVectorDrawable
7 import android.graphics.drawable.Drawable
8 import android.graphics.drawable.GradientDrawable
9 import android.graphics.drawable.RippleDrawable
10 import android.os.Build
11 import android.safetycenter.SafetyCenterIssue
12 import android.text.TextUtils
13 import android.util.AttributeSet
14 import android.util.Log
15 import android.view.View
16 import android.widget.ImageView
17 import android.widget.TextView
18 import androidx.annotation.DrawableRes
19 import androidx.annotation.RequiresApi
20 import androidx.constraintlayout.widget.ConstraintLayout
21 import androidx.core.view.ViewCompat
22 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
23 import androidx.core.view.isVisible
24 import com.android.permissioncontroller.R
25 import com.android.permissioncontroller.permission.utils.StringUtils
26 import com.android.permissioncontroller.safetycenter.ui.MoreIssuesCardAnimator
27 import com.android.permissioncontroller.safetycenter.ui.MoreIssuesCardData
28 import java.text.NumberFormat
29 import java.time.Duration
30
31 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
32 internal class MoreIssuesHeaderView
33 @JvmOverloads
34 constructor(
35 context: Context,
36 attrs: AttributeSet? = null,
37 defStyleAttr: Int = 0,
38 defStyleRes: Int = 0
39 ) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
40
41 init {
42 inflate(context, R.layout.view_more_issues, this)
43 }
44
45 private val moreIssuesCardAnimator = MoreIssuesCardAnimator()
46 private val statusIconView: ImageView by lazy { findViewById(R.id.status_icon) }
47 private val titleView: TextView by lazy { findViewById(R.id.title) }
48 private val expandCollapseLayout: View by lazy { findViewById(android.R.id.widget_frame) }
49 private val counterView: TextView by lazy {
50 expandCollapseLayout.findViewById(R.id.widget_title)
51 }
52 private val expandCollapseIcon: ImageView by lazy {
53 expandCollapseLayout.findViewById(R.id.widget_icon)
54 }
55 private var cornerAnimator: ValueAnimator? = null
56
57 fun showExpandableHeader(
58 previousData: MoreIssuesCardData?,
59 nextData: MoreIssuesCardData,
60 title: String,
61 @DrawableRes overrideChevronIconResId: Int?,
62 onClick: () -> Unit
63 ) {
64 titleView.text = title
65 updateStatusIcon(previousData?.severityLevel, nextData.severityLevel)
66 updateExpandCollapseButton(
67 previousData?.isExpanded,
68 nextData.isExpanded,
69 overrideChevronIconResId
70 )
71 updateIssueCount(previousData?.hiddenIssueCount, nextData.hiddenIssueCount)
72 updateBackground(previousData?.isExpanded, nextData.isExpanded)
73 setOnClickListener { onClick() }
74
75 val expansionString =
76 StringUtils.getIcuPluralsString(
77 context,
78 R.string.safety_center_more_issues_card_expand_action,
79 nextData.hiddenIssueCount
80 )
81 // Replacing the on-click label to indicate the number of hidden issues. The on-click
82 // command is set to null so that it uses the existing expansion behaviour.
83 ViewCompat.replaceAccessibilityAction(
84 this,
85 AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
86 expansionString,
87 null
88 )
89 }
90
91 fun showStaticHeader(title: String, severityLevel: Int) {
92 titleView.text = title
93 updateStatusIcon(previousSeverityLevel = null, severityLevel)
94 expandCollapseLayout.isVisible = false
95 setOnClickListener(null)
96 isClickable = false
97 setBackgroundResource(android.R.color.transparent)
98 }
99
100 private fun updateExpandCollapseButton(
101 wasExpanded: Boolean?,
102 isExpanded: Boolean,
103 @DrawableRes overrideChevronIconResId: Int?
104 ) {
105 expandCollapseLayout.isVisible = true
106 if (overrideChevronIconResId != null) {
107 expandCollapseIcon.setImageResource(overrideChevronIconResId)
108 } else if (wasExpanded != null && wasExpanded != isExpanded) {
109 if (isExpanded) {
110 expandCollapseIcon.animate(
111 R.drawable.more_issues_expand_anim,
112 R.drawable.ic_collapse_issues
113 )
114 } else {
115 expandCollapseIcon.animate(
116 R.drawable.more_issues_collapse_anim,
117 R.drawable.ic_expand_issues
118 )
119 }
120 } else {
121 expandCollapseIcon.setImageResource(
122 if (isExpanded) {
123 R.drawable.ic_collapse_issues
124 } else {
125 R.drawable.ic_expand_issues
126 }
127 )
128 }
129 }
130
131 private fun updateStatusIcon(previousSeverityLevel: Int?, endSeverityLevel: Int) {
132 statusIconView.isVisible = true
133 moreIssuesCardAnimator.cancelStatusAnimation(statusIconView)
134 if (previousSeverityLevel != null && previousSeverityLevel != endSeverityLevel) {
135 moreIssuesCardAnimator.animateStatusIconsChange(
136 statusIconView,
137 previousSeverityLevel,
138 endSeverityLevel,
139 selectIconResId(endSeverityLevel)
140 )
141 } else {
142 statusIconView.setImageResource(selectIconResId(endSeverityLevel))
143 }
144 }
145
146 @DrawableRes
147 private fun selectIconResId(severityLevel: Int): Int {
148 return when (severityLevel) {
149 SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK -> R.drawable.ic_safety_info
150 SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION ->
151 R.drawable.ic_safety_recommendation
152 SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING -> R.drawable.ic_safety_warn
153 else -> {
154 Log.e(TAG, "Unexpected SafetyCenterIssue.IssueSeverityLevel: $severityLevel")
155 R.drawable.ic_safety_null_state
156 }
157 }
158 }
159
160 private fun updateIssueCount(previousCount: Int?, endCount: Int) {
161 moreIssuesCardAnimator.cancelTextChangeAnimation(counterView)
162
163 val numberFormat = NumberFormat.getInstance()
164 val previousText = previousCount?.let(numberFormat::format)
165 val newText = numberFormat.format(endCount)
166 val animateTextChange =
167 !previousText.isNullOrEmpty() && !TextUtils.equals(previousText, newText)
168
169 if (animateTextChange) {
170 counterView.text = previousText
171 moreIssuesCardAnimator.animateChangeText(counterView, newText)
172 } else {
173 counterView.text = newText
174 }
175 }
176
177 private fun updateBackground(wasExpanded: Boolean?, isExpanded: Boolean) {
178 if (background !is RippleDrawable) {
179 setBackgroundResource(R.drawable.safety_center_more_issues_card_background)
180 }
181 (background?.mutate() as? RippleDrawable)?.let { ripple ->
182 val topRadius = context.resources.getDimension(R.dimen.sc_card_corner_radius_large)
183 val bottomRadiusStart =
184 if (wasExpanded ?: isExpanded) {
185 context.resources.getDimension(R.dimen.sc_card_corner_radius_xsmall)
186 } else {
187 topRadius
188 }
189 val bottomRadiusEnd =
190 if (isExpanded) {
191 context.resources.getDimension(R.dimen.sc_card_corner_radius_xsmall)
192 } else {
193 topRadius
194 }
195 val cornerRadii =
196 floatArrayOf(
197 topRadius,
198 topRadius,
199 topRadius,
200 topRadius,
201 bottomRadiusStart,
202 bottomRadiusStart,
203 bottomRadiusStart,
204 bottomRadiusStart
205 )
206 setCornerRadii(ripple, cornerRadii)
207 if (bottomRadiusEnd != bottomRadiusStart) {
208 cornerAnimator?.removeAllUpdateListeners()
209 cornerAnimator?.removeAllListeners()
210 cornerAnimator?.cancel()
211 val animator =
212 ValueAnimator.ofFloat(bottomRadiusStart, bottomRadiusEnd)
213 .setDuration(CORNER_RADII_ANIMATION_DURATION.toMillis())
214 if (isExpanded) {
215 animator.startDelay = CORNER_RADII_ANIMATION_DELAY.toMillis()
216 }
217 animator.addUpdateListener {
218 cornerRadii.fill(it.animatedValue as Float, fromIndex = 4, toIndex = 8)
219 setCornerRadii(ripple, cornerRadii)
220 }
221 animator.start()
222 cornerAnimator = animator
223 }
224 }
225 }
226
227 private fun setCornerRadii(ripple: RippleDrawable, cornerRadii: FloatArray) {
228 for (index in 0 until ripple.numberOfLayers) {
229 (ripple.getDrawable(index).mutate() as? GradientDrawable)?.let {
230 it.cornerRadii = cornerRadii
231 }
232 }
233 }
234
235 private fun ImageView.animate(@DrawableRes animationRes: Int, @DrawableRes imageRes: Int) {
236 (drawable as? AnimatedVectorDrawable)?.clearAnimationCallbacks()
237 setImageResource(animationRes)
238 (drawable as? AnimatedVectorDrawable)?.apply {
239 registerAnimationCallback(
240 object : Animatable2.AnimationCallback() {
241 override fun onAnimationEnd(drawable: Drawable?) {
242 setImageResource(imageRes)
243 }
244 }
245 )
246 start()
247 }
248 }
249
250 companion object {
251 val TAG: String = MoreIssuesHeaderView::class.java.simpleName
252 private val CORNER_RADII_ANIMATION_DELAY = Duration.ofMillis(250)
253 private val CORNER_RADII_ANIMATION_DURATION = Duration.ofMillis(120)
254 }
255 }
256