• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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