<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