<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 actionString =
72 if (nextData.isExpanded) {
73 context.getString(R.string.safety_center_more_issues_card_collapse_action)
74 } else {
75 StringUtils.getIcuPluralsString(
76 context,
77 R.string.safety_center_more_issues_card_expand_action,
78 nextData.hiddenIssueCount,
79 )
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 actionString,
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 Log.v(TAG, "Starting more issues card text animation")
172 moreIssuesCardAnimator.animateChangeText(counterView, newText)
173 } else {
174 counterView.text = newText
175 }
176 }
177
178 private fun updateBackground(wasExpanded: Boolean?, isExpanded: Boolean) {
179 if (background !is RippleDrawable) {
180 setBackgroundResource(R.drawable.safety_center_more_issues_card_background)
181 }
182 (background?.mutate() as? RippleDrawable)?.let { ripple ->
183 val topRadius = context.resources.getDimension(R.dimen.sc_card_corner_radius_large)
184 val bottomRadiusStart =
185 if (wasExpanded ?: isExpanded) {
186 context.resources.getDimension(R.dimen.sc_card_corner_radius_xsmall)
187 } else {
188 topRadius
189 }
190 val bottomRadiusEnd =
191 if (isExpanded) {
192 context.resources.getDimension(R.dimen.sc_card_corner_radius_xsmall)
193 } else {
194 topRadius
195 }
196 val cornerRadii =
197 floatArrayOf(
198 topRadius,
199 topRadius,
200 topRadius,
201 topRadius,
202 bottomRadiusStart,
203 bottomRadiusStart,
204 bottomRadiusStart,
205 bottomRadiusStart,
206 )
207 setCornerRadii(ripple, cornerRadii)
208 if (bottomRadiusEnd != bottomRadiusStart) {
209 cornerAnimator?.removeAllUpdateListeners()
210 cornerAnimator?.removeAllListeners()
211 cornerAnimator?.cancel()
212 val animator =
213 ValueAnimator.ofFloat(bottomRadiusStart, bottomRadiusEnd)
214 .setDuration(CORNER_RADII_ANIMATION_DURATION.toMillis())
215 if (isExpanded) {
216 animator.startDelay = CORNER_RADII_ANIMATION_DELAY.toMillis()
217 }
218 animator.addUpdateListener {
219 cornerRadii.fill(it.animatedValue as Float, fromIndex = 4, toIndex = 8)
220 setCornerRadii(ripple, cornerRadii)
221 }
222 animator.start()
223 cornerAnimator = animator
224 }
225 }
226 }
227
228 private fun setCornerRadii(ripple: RippleDrawable, cornerRadii: FloatArray) {
229 for (index in 0 until ripple.numberOfLayers) {
230 (ripple.getDrawable(index).mutate() as? GradientDrawable)?.let {
231 it.cornerRadii = cornerRadii
232 }
233 }
234 }
235
236 private fun ImageView.animate(@DrawableRes animationRes: Int, @DrawableRes imageRes: Int) {
237 (drawable as? AnimatedVectorDrawable)?.clearAnimationCallbacks()
238 setImageResource(animationRes)
239 (drawable as? AnimatedVectorDrawable)?.apply {
240 registerAnimationCallback(
241 object : Animatable2.AnimationCallback() {
242 override fun onAnimationEnd(drawable: Drawable?) {
243 setImageResource(imageRes)
244 }
245 }
246 )
247 start()
248 }
249 }
250
251 companion object {
252 val TAG: String = MoreIssuesHeaderView::class.java.simpleName
253 private val CORNER_RADII_ANIMATION_DELAY = Duration.ofMillis(250)
254 private val CORNER_RADII_ANIMATION_DURATION = Duration.ofMillis(120)
255 }
256 }
257