• 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 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