1 /*
2  * Copyright (C) 2022 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 
17 package com.android.systemui
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.AnimatorSet
22 import android.animation.TimeInterpolator
23 import android.animation.ValueAnimator
24 import android.content.Context
25 import android.graphics.Canvas
26 import android.graphics.Color
27 import android.graphics.Matrix
28 import android.graphics.Paint
29 import android.graphics.Path
30 import android.graphics.RectF
31 import android.hardware.biometrics.BiometricSourceType
32 import android.view.View
33 import androidx.core.graphics.ColorUtils
34 import com.android.app.animation.Interpolators
35 import com.android.keyguard.KeyguardUpdateMonitor
36 import com.android.keyguard.KeyguardUpdateMonitorCallback
37 import com.android.settingslib.Utils
38 import com.android.systemui.biometrics.AuthController
39 import com.android.systemui.flags.FeatureFlags
40 import com.android.systemui.flags.Flags
41 import com.android.systemui.log.ScreenDecorationsLogger
42 import com.android.systemui.plugins.statusbar.StatusBarStateController
43 import com.android.systemui.util.asIndenting
44 import java.io.PrintWriter
45 import java.util.concurrent.Executor
46 
47 /**
48  * When the face is enrolled, we use this view to show the face scanning animation and the camera
49  * protection on the keyguard.
50  */
51 class FaceScanningOverlay(
52     context: Context,
53     pos: Int,
54     val statusBarStateController: StatusBarStateController,
55     val keyguardUpdateMonitor: KeyguardUpdateMonitor,
56     val mainExecutor: Executor,
57     val logger: ScreenDecorationsLogger,
58     val authController: AuthController,
59     val featureFlags: FeatureFlags,
60 ) : ScreenDecorations.DisplayCutoutView(context, pos) {
61     private var showScanningAnim = false
62     private val rimPaint = Paint()
63     private var rimProgress: Float = HIDDEN_CAMERA_PROTECTION_SCALE
64     private var rimAnimator: AnimatorSet? = null
65     private val rimRect = RectF()
66     private var cameraProtectionColor = Color.BLACK
67 
68     var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context,
69         com.android.internal.R.attr.materialColorPrimaryFixed)
70     private var cameraProtectionAnimator: ValueAnimator? = null
71     var hideOverlayRunnable: Runnable? = null
72     var faceAuthSucceeded = false
73 
74     init {
75         visibility = View.INVISIBLE // only show this view when face scanning is happening
76     }
77 
78     override fun onAttachedToWindow() {
79         super.onAttachedToWindow()
80         mainExecutor.execute {
81             keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
82         }
83     }
84 
85     override fun onDetachedFromWindow() {
86         super.onDetachedFromWindow()
87         mainExecutor.execute {
88             keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
89         }
90     }
91 
92     override fun setColor(color: Int) {
93         cameraProtectionColor = color
94         invalidate()
95     }
96 
97     override fun drawCutoutProtection(canvas: Canvas) {
98         if (protectionRect.isEmpty) {
99             return
100         }
101         if (rimProgress > HIDDEN_RIM_SCALE) {
102             drawFaceScanningRim(canvas)
103         }
104         if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE) {
105             drawCameraProtection(canvas)
106         }
107     }
108 
109     override fun enableShowProtection(show: Boolean) {
110         val animationRequired =
111                 keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing
112         val showScanningAnimNow = animationRequired && show
113         if (showScanningAnimNow == showScanningAnim) {
114             return
115         }
116         logger.cameraProtectionShownOrHidden(keyguardUpdateMonitor.isFaceDetectionRunning,
117                 authController.isShowing,
118                 show,
119                 showScanningAnim)
120         showScanningAnim = showScanningAnimNow
121         updateProtectionBoundingPath()
122         // Delay the relayout until the end of the animation when hiding,
123         // otherwise we'd clip it.
124         if (showScanningAnim) {
125             visibility = View.VISIBLE
126             requestLayout()
127         }
128 
129         cameraProtectionAnimator?.cancel()
130         cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress,
131                 if (showScanningAnimNow) SHOW_CAMERA_PROTECTION_SCALE
132                 else HIDDEN_CAMERA_PROTECTION_SCALE).apply {
133             startDelay =
134                     if (showScanningAnim) 0
135                     else if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
136                     else PULSE_ERROR_DISAPPEAR_DURATION
137             duration =
138                     if (showScanningAnim) CAMERA_PROTECTION_APPEAR_DURATION
139                     else if (faceAuthSucceeded) CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION
140                     else CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION
141             interpolator =
142                     if (showScanningAnim) Interpolators.STANDARD_ACCELERATE
143                     else if (faceAuthSucceeded) Interpolators.STANDARD
144                     else Interpolators.STANDARD_DECELERATE
145             addUpdateListener(this@FaceScanningOverlay::updateCameraProtectionProgress)
146             addListener(object : AnimatorListenerAdapter() {
147                 override fun onAnimationEnd(animation: Animator) {
148                     cameraProtectionAnimator = null
149                     if (!showScanningAnim) {
150                         hide()
151                     }
152                 }
153             })
154         }
155 
156         rimAnimator?.cancel()
157         rimAnimator = if (showScanningAnim) {
158             createFaceScanningRimAnimator()
159         } else if (faceAuthSucceeded) {
160             createFaceSuccessRimAnimator()
161         } else {
162             createFaceNotSuccessRimAnimator()
163         }
164         rimAnimator?.apply {
165             addListener(object : AnimatorListenerAdapter() {
166                 override fun onAnimationEnd(animation: Animator) {
167                     rimAnimator = null
168                     if (!showScanningAnim) {
169                         requestLayout()
170                     }
171                 }
172             })
173         }
174         rimAnimator?.start()
175     }
176 
177     override fun updateVisOnUpdateCutout(): Boolean {
178         return false // instead, we always update the visibility whenever face scanning starts/ends
179     }
180 
181     override fun updateProtectionBoundingPath() {
182         super.updateProtectionBoundingPath()
183         rimRect.set(protectionRect)
184         rimRect.scale(rimProgress)
185     }
186 
187     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
188         if (mBounds.isEmpty()) {
189             super.onMeasure(widthMeasureSpec, heightMeasureSpec)
190             return
191         }
192         if (showScanningAnim) {
193             // Make sure that our measured height encompasses the extra space for the animation
194             mTotalBounds.set(mBoundingRect)
195             mTotalBounds.union(
196                 rimRect.left.toInt(),
197                 rimRect.top.toInt(),
198                 rimRect.right.toInt(),
199                 rimRect.bottom.toInt())
200             val measuredWidth = resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0)
201             val measuredHeight = resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)
202             logger.boundingRect(rimRect, "onMeasure: Face scanning animation")
203             logger.boundingRect(mBoundingRect, "onMeasure: Display cutout view bounding rect")
204             logger.boundingRect(mTotalBounds, "onMeasure: TotalBounds")
205             logger.onMeasureDimensions(widthMeasureSpec,
206                     heightMeasureSpec,
207                     measuredWidth,
208                     measuredHeight)
209             setMeasuredDimension(measuredWidth, measuredHeight)
210         } else {
211             setMeasuredDimension(
212                 resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
213                 resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0))
214         }
215     }
216 
217     private fun drawFaceScanningRim(canvas: Canvas) {
218         val rimPath = Path(protectionPath)
219         scalePath(rimPath, rimProgress)
220         rimPaint.style = Paint.Style.FILL
221         val rimPaintAlpha = rimPaint.alpha
222         rimPaint.color = ColorUtils.blendARGB(
223             faceScanningAnimColor,
224             Color.WHITE,
225             statusBarStateController.dozeAmount
226         )
227         rimPaint.alpha = rimPaintAlpha
228         canvas.drawPath(rimPath, rimPaint)
229     }
230 
231     private fun drawCameraProtection(canvas: Canvas) {
232         val scaledProtectionPath = Path(protectionPath)
233         scalePath(scaledProtectionPath, cameraProtectionProgress)
234         paint.style = Paint.Style.FILL
235         paint.color = cameraProtectionColor
236         canvas.drawPath(scaledProtectionPath, paint)
237     }
238 
239     private fun createFaceSuccessRimAnimator(): AnimatorSet {
240         val rimSuccessAnimator = AnimatorSet()
241         rimSuccessAnimator.playTogether(
242             createRimDisappearAnimator(
243                 PULSE_RADIUS_SUCCESS,
244                 PULSE_SUCCESS_DISAPPEAR_DURATION,
245                 Interpolators.STANDARD_DECELERATE
246             ),
247             createSuccessOpacityAnimator(),
248         )
249         return AnimatorSet().apply {
250             playTogether(rimSuccessAnimator, cameraProtectionAnimator)
251         }
252     }
253 
254     private fun createFaceNotSuccessRimAnimator(): AnimatorSet {
255         return AnimatorSet().apply {
256             playTogether(
257                 createRimDisappearAnimator(
258                     SHOW_CAMERA_PROTECTION_SCALE,
259                     PULSE_ERROR_DISAPPEAR_DURATION,
260                     Interpolators.STANDARD
261                 ),
262                 cameraProtectionAnimator,
263             )
264         }
265     }
266 
267     private fun createRimDisappearAnimator(
268         endValue: Float,
269         animDuration: Long,
270         timeInterpolator: TimeInterpolator
271     ): ValueAnimator {
272         return ValueAnimator.ofFloat(rimProgress, endValue).apply {
273             duration = animDuration
274             interpolator = timeInterpolator
275             addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
276             addListener(object : AnimatorListenerAdapter() {
277                 override fun onAnimationEnd(animation: Animator) {
278                     rimProgress = HIDDEN_RIM_SCALE
279                     invalidate()
280                 }
281             })
282         }
283     }
284 
285     private fun createSuccessOpacityAnimator(): ValueAnimator {
286         return ValueAnimator.ofInt(255, 0).apply {
287             duration = PULSE_SUCCESS_DISAPPEAR_DURATION
288             interpolator = Interpolators.LINEAR
289             addUpdateListener(this@FaceScanningOverlay::updateRimAlpha)
290             addListener(object : AnimatorListenerAdapter() {
291                 override fun onAnimationEnd(animation: Animator) {
292                     rimPaint.alpha = 255
293                     invalidate()
294                 }
295             })
296         }
297     }
298 
299     private fun createFaceScanningRimAnimator(): AnimatorSet {
300         val dontPulse = featureFlags.isEnabled(Flags.STOP_PULSING_FACE_SCANNING_ANIMATION)
301         if (dontPulse) {
302             return AnimatorSet().apply {
303                 playSequentially(
304                         cameraProtectionAnimator,
305                         createRimAppearAnimator(),
306                 )
307             }
308         }
309         return AnimatorSet().apply {
310             playSequentially(
311                 cameraProtectionAnimator,
312                 createRimAppearAnimator(),
313                 createPulseAnimator()
314             )
315         }
316     }
317 
318     private fun createRimAppearAnimator(): ValueAnimator {
319         return ValueAnimator.ofFloat(
320             SHOW_CAMERA_PROTECTION_SCALE,
321             PULSE_RADIUS_OUT
322         ).apply {
323             duration = PULSE_APPEAR_DURATION
324             interpolator = Interpolators.STANDARD_DECELERATE
325             addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
326         }
327     }
328 
329     private fun hide() {
330         visibility = INVISIBLE
331         hideOverlayRunnable?.run()
332         hideOverlayRunnable = null
333         requestLayout()
334     }
335 
336     private fun updateRimProgress(animator: ValueAnimator) {
337         rimProgress = animator.animatedValue as Float
338         invalidate()
339     }
340 
341     private fun updateCameraProtectionProgress(animator: ValueAnimator) {
342         cameraProtectionProgress = animator.animatedValue as Float
343         invalidate()
344     }
345 
346     private fun updateRimAlpha(animator: ValueAnimator) {
347         rimPaint.alpha = animator.animatedValue as Int
348         invalidate()
349     }
350 
351     private fun createPulseAnimator(): ValueAnimator {
352         return ValueAnimator.ofFloat(
353                 PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply {
354             duration = HALF_PULSE_DURATION
355             interpolator = Interpolators.STANDARD
356             repeatCount = 11 // Pulse inwards and outwards, reversing direction, 6 times
357             repeatMode = ValueAnimator.REVERSE
358             addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
359         }
360     }
361 
362     private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
363         override fun onBiometricAuthenticated(
364             userId: Int,
365             biometricSourceType: BiometricSourceType?,
366             isStrongBiometric: Boolean
367         ) {
368             if (biometricSourceType == BiometricSourceType.FACE) {
369                 post {
370                     faceAuthSucceeded = true
371                     logger.biometricEvent("biometricAuthenticated")
372                     enableShowProtection(true)
373                 }
374             }
375         }
376 
377         override fun onBiometricAcquired(
378             biometricSourceType: BiometricSourceType?,
379             acquireInfo: Int
380         ) {
381             if (biometricSourceType == BiometricSourceType.FACE) {
382                 post {
383                     faceAuthSucceeded = false // reset
384                 }
385             }
386         }
387 
388         override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) {
389             if (biometricSourceType == BiometricSourceType.FACE) {
390                 post {
391                     faceAuthSucceeded = false
392                     logger.biometricEvent("biometricFailed")
393                     enableShowProtection(false)
394                 }
395             }
396         }
397 
398         override fun onBiometricError(
399             msgId: Int,
400             errString: String?,
401             biometricSourceType: BiometricSourceType?
402         ) {
403             if (biometricSourceType == BiometricSourceType.FACE) {
404                 post {
405                     faceAuthSucceeded = false
406                     logger.biometricEvent("biometricError")
407                     enableShowProtection(false)
408                 }
409             }
410         }
411     }
412 
413     companion object {
414         private const val HIDDEN_RIM_SCALE = HIDDEN_CAMERA_PROTECTION_SCALE
415         private const val SHOW_CAMERA_PROTECTION_SCALE = 1f
416 
417         private const val PULSE_RADIUS_IN = 1.1f
418         private const val PULSE_RADIUS_OUT = 1.125f
419         private const val PULSE_RADIUS_SUCCESS = 1.25f
420 
421         private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L
422         private const val PULSE_APPEAR_DURATION = 250L // without start delay
423 
424         private const val HALF_PULSE_DURATION = 500L
425 
426         private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L
427         private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay
428 
429         private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L
430         private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay
431 
432         private fun scalePath(path: Path, scalingFactor: Float) {
433             val scaleMatrix = Matrix().apply {
434                 val boundingRectangle = RectF()
435                 path.computeBounds(boundingRectangle, true)
436                 setScale(
437                     scalingFactor, scalingFactor,
438                     boundingRectangle.centerX(), boundingRectangle.centerY()
439                 )
440             }
441             path.transform(scaleMatrix)
442         }
443     }
444 
445     override fun dump(pw: PrintWriter) {
446         val ipw = pw.asIndenting()
447         ipw.increaseIndent()
448         ipw.println("FaceScanningOverlay:")
449         super.dump(ipw)
450         ipw.println("rimProgress=$rimProgress")
451         ipw.println("rimRect=$rimRect")
452         ipw.println("this=$this")
453         ipw.decreaseIndent()
454     }
455 }
456