1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.systemui.biometrics
17 
18 import android.animation.Animator
19 import android.animation.AnimatorListenerAdapter
20 import android.animation.AnimatorSet
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.graphics.Canvas
24 import android.graphics.Color
25 import android.graphics.Paint
26 import android.graphics.PointF
27 import android.util.AttributeSet
28 import android.view.View
29 import android.view.animation.PathInterpolator
30 import com.android.internal.graphics.ColorUtils
31 import com.android.systemui.animation.Interpolators
32 import com.android.systemui.statusbar.charging.DwellRippleShader
33 import com.android.systemui.statusbar.charging.RippleShader
34 
35 private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f
36 
37 /**
38  * Expanding ripple effect
39  * - startUnlockedRipple for the transition from biometric authentication success to showing
40  * launcher.
41  * - startDwellRipple for the ripple expansion out when the user has their finger down on the UDFPS
42  * sensor area
43  * - retractRipple for the ripple animation inwards to signal a failure
44  */
45 class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
46     private val retractInterpolator = PathInterpolator(.05f, .93f, .1f, 1f)
47 
48     private val dwellPulseDuration = 100L
49     private val dwellExpandDuration = 2000L - dwellPulseDuration
50 
51     private var drawDwell: Boolean = false
52     private var drawRipple: Boolean = false
53 
54     private var lockScreenColorVal = Color.WHITE
55     private val retractDuration = 400L
56     private var alphaInDuration: Long = 0
57     private var unlockedRippleInProgress: Boolean = false
58     private val dwellShader = DwellRippleShader()
59     private val dwellPaint = Paint()
60     private val rippleShader = RippleShader()
61     private val ripplePaint = Paint()
62     private var retractAnimator: Animator? = null
63     private var dwellPulseOutAnimator: Animator? = null
64     private var dwellRadius: Float = 0f
65         set(value) {
66             dwellShader.maxRadius = value
67             field = value
68         }
69     private var dwellOrigin: PointF = PointF()
70         set(value) {
71             dwellShader.origin = value
72             field = value
73         }
74     private var radius: Float = 0f
75         set(value) {
76             rippleShader.radius = value
77             field = value
78         }
79     private var origin: PointF = PointF()
80         set(value) {
81             rippleShader.origin = value
82             field = value
83         }
84 
85     init {
86         rippleShader.color = 0xffffffff.toInt() // default color
87         rippleShader.progress = 0f
88         rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
89         ripplePaint.shader = rippleShader
90 
91         dwellShader.color = 0xffffffff.toInt() // default color
92         dwellShader.progress = 0f
93         dwellShader.distortionStrength = .4f
94         dwellPaint.shader = dwellShader
95         visibility = GONE
96     }
97 
98     fun setSensorLocation(location: PointF) {
99         origin = location
100         radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat()
101     }
102 
103     fun setFingerprintSensorLocation(location: PointF, sensorRadius: Float) {
104         origin = location
105         radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat()
106         dwellOrigin = location
107         dwellRadius = sensorRadius * 1.5f
108     }
109 
110     fun setAlphaInDuration(duration: Long) {
111         alphaInDuration = duration
112     }
113 
114     /**
115      * Animate ripple inwards back to radius 0
116      */
117     fun retractRipple() {
118         if (retractAnimator?.isRunning == true) {
119             return // let the animation finish
120         }
121 
122         if (dwellPulseOutAnimator?.isRunning == true) {
123             val retractRippleAnimator = ValueAnimator.ofFloat(dwellShader.progress, 0f)
124                     .apply {
125                 interpolator = retractInterpolator
126                 duration = retractDuration
127                 addUpdateListener { animator ->
128                     val now = animator.currentPlayTime
129                     dwellShader.progress = animator.animatedValue as Float
130                     dwellShader.time = now.toFloat()
131 
132                     invalidate()
133                 }
134             }
135 
136             val retractAlphaAnimator = ValueAnimator.ofInt(255, 0).apply {
137                 interpolator = Interpolators.LINEAR
138                 duration = retractDuration
139                 addUpdateListener { animator ->
140                     dwellShader.color = ColorUtils.setAlphaComponent(
141                             dwellShader.color,
142                             animator.animatedValue as Int
143                     )
144                     invalidate()
145                 }
146             }
147 
148             retractAnimator = AnimatorSet().apply {
149                 playTogether(retractRippleAnimator, retractAlphaAnimator)
150                 addListener(object : AnimatorListenerAdapter() {
151                     override fun onAnimationStart(animation: Animator?) {
152                         dwellPulseOutAnimator?.cancel()
153                         drawDwell = true
154                     }
155 
156                     override fun onAnimationEnd(animation: Animator?) {
157                         drawDwell = false
158                         resetDwellAlpha()
159                     }
160                 })
161                 start()
162             }
163         }
164     }
165 
166     /**
167      * Plays a ripple animation that grows to the dwellRadius with distortion.
168      */
169     fun startDwellRipple(isDozing: Boolean) {
170         if (unlockedRippleInProgress || dwellPulseOutAnimator?.isRunning == true) {
171             return
172         }
173 
174         updateDwellRippleColor(isDozing)
175 
176         val dwellPulseOutRippleAnimator = ValueAnimator.ofFloat(0f, .8f).apply {
177             interpolator = Interpolators.LINEAR
178             duration = dwellPulseDuration
179             addUpdateListener { animator ->
180                 val now = animator.currentPlayTime
181                 dwellShader.progress = animator.animatedValue as Float
182                 dwellShader.time = now.toFloat()
183 
184                 invalidate()
185             }
186         }
187 
188         // slowly animate outwards until we receive a call to retractRipple or startUnlockedRipple
189         val expandDwellRippleAnimator = ValueAnimator.ofFloat(.8f, 1f).apply {
190             interpolator = Interpolators.LINEAR_OUT_SLOW_IN
191             duration = dwellExpandDuration
192             addUpdateListener { animator ->
193                 val now = animator.currentPlayTime
194                 dwellShader.progress = animator.animatedValue as Float
195                 dwellShader.time = now.toFloat()
196 
197                 invalidate()
198             }
199         }
200 
201         dwellPulseOutAnimator = AnimatorSet().apply {
202             playSequentially(
203                     dwellPulseOutRippleAnimator,
204                     expandDwellRippleAnimator
205             )
206             addListener(object : AnimatorListenerAdapter() {
207                 override fun onAnimationStart(animation: Animator?) {
208                     retractAnimator?.cancel()
209                     visibility = VISIBLE
210                     drawDwell = true
211                 }
212 
213                 override fun onAnimationEnd(animation: Animator?) {
214                     drawDwell = false
215                     resetRippleAlpha()
216                 }
217             })
218             start()
219         }
220     }
221 
222     /**
223      * Ripple that bursts outwards from the position of the sensor to the edges of the screen
224      */
225     fun startUnlockedRipple(onAnimationEnd: Runnable?) {
226         if (unlockedRippleInProgress) {
227             return // Ignore if ripple effect is already playing
228         }
229 
230         val rippleAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
231             interpolator = Interpolators.LINEAR_OUT_SLOW_IN
232             duration = AuthRippleController.RIPPLE_ANIMATION_DURATION
233             addUpdateListener { animator ->
234                 val now = animator.currentPlayTime
235                 rippleShader.progress = animator.animatedValue as Float
236                 rippleShader.time = now.toFloat()
237 
238                 invalidate()
239             }
240         }
241 
242         val alphaInAnimator = ValueAnimator.ofInt(0, 255).apply {
243             duration = alphaInDuration
244             addUpdateListener { animator ->
245                 rippleShader.color = ColorUtils.setAlphaComponent(
246                     rippleShader.color,
247                     animator.animatedValue as Int
248                 )
249                 invalidate()
250             }
251         }
252 
253         val animatorSet = AnimatorSet().apply {
254             playTogether(
255                 rippleAnimator,
256                 alphaInAnimator
257             )
258             addListener(object : AnimatorListenerAdapter() {
259                 override fun onAnimationStart(animation: Animator?) {
260                     unlockedRippleInProgress = true
261                     rippleShader.shouldFadeOutRipple = true
262                     drawRipple = true
263                     visibility = VISIBLE
264                 }
265 
266                 override fun onAnimationEnd(animation: Animator?) {
267                     onAnimationEnd?.run()
268                     unlockedRippleInProgress = false
269                     drawRipple = false
270                     visibility = GONE
271                 }
272             })
273         }
274         animatorSet.start()
275     }
276 
277     fun resetRippleAlpha() {
278         rippleShader.color = ColorUtils.setAlphaComponent(
279                 rippleShader.color,
280                 255
281         )
282     }
283 
284     fun setLockScreenColor(color: Int) {
285         lockScreenColorVal = color
286         rippleShader.color = lockScreenColorVal
287         resetRippleAlpha()
288     }
289 
290     fun updateDwellRippleColor(isDozing: Boolean) {
291         if (isDozing) {
292             dwellShader.color = Color.WHITE
293         } else {
294             dwellShader.color = lockScreenColorVal
295         }
296         resetDwellAlpha()
297     }
298 
299     fun resetDwellAlpha() {
300         dwellShader.color = ColorUtils.setAlphaComponent(
301                 dwellShader.color,
302                 255
303         )
304     }
305 
306     override fun onDraw(canvas: Canvas?) {
307         // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover
308         // the active effect area. Values here should be kept in sync with the
309         // animation implementation in the ripple shader.
310         if (drawDwell) {
311             val maskRadius = (1 - (1 - dwellShader.progress) * (1 - dwellShader.progress) *
312                     (1 - dwellShader.progress)) * dwellRadius * 2f
313             canvas?.drawCircle(dwellOrigin.x, dwellOrigin.y, maskRadius, dwellPaint)
314         }
315 
316         if (drawRipple) {
317             val mask = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
318                     (1 - rippleShader.progress)) * radius * 2f
319             canvas?.drawCircle(origin.x, origin.y, mask, ripplePaint)
320         }
321     }
322 }
323