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