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.view.View
32 import androidx.core.graphics.ColorUtils
33 import com.android.app.animation.Interpolators
34 import com.android.keyguard.KeyguardUpdateMonitor
35 import com.android.settingslib.Utils
36 import com.android.systemui.biometrics.AuthController
37 import com.android.systemui.log.ScreenDecorationsLogger
38 import com.android.systemui.plugins.statusbar.StatusBarStateController
39 import com.android.systemui.util.asIndenting
40 import java.io.PrintWriter
41 import java.util.concurrent.Executor
42 
43 /**
44  * When the face is enrolled, we use this view to show the face scanning animation and the camera
45  * protection on the keyguard.
46  */
47 class FaceScanningOverlay(
48     context: Context,
49     pos: Int,
50     val statusBarStateController: StatusBarStateController,
51     val keyguardUpdateMonitor: KeyguardUpdateMonitor,
52     val mainExecutor: Executor,
53     val logger: ScreenDecorationsLogger,
54     val authController: AuthController,
55 ) : ScreenDecorations.DisplayCutoutView(context, pos) {
56     private var showScanningAnim = false
57     private val rimPaint = Paint()
58     private var rimProgress: Float = HIDDEN_CAMERA_PROTECTION_SCALE
59     private var rimAnimator: AnimatorSet? = null
60     private val rimRect = RectF()
61     private var cameraProtectionColor = Color.BLACK
62 
63     var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context,
64         com.android.internal.R.attr.materialColorPrimaryFixed)
65     private var cameraProtectionAnimator: ValueAnimator? = null
66     var hideOverlayRunnable: Runnable? = null
67 
68     init {
69         visibility = View.INVISIBLE // only show this view when face scanning is happening
70     }
71 
setColornull72     override fun setColor(color: Int) {
73         cameraProtectionColor = color
74         invalidate()
75     }
76 
drawCutoutProtectionnull77     override fun drawCutoutProtection(canvas: Canvas) {
78         if (protectionRect.isEmpty) {
79             return
80         }
81         if (rimProgress > HIDDEN_RIM_SCALE) {
82             drawFaceScanningRim(canvas)
83         }
84         if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE) {
85             drawCameraProtection(canvas)
86         }
87     }
88 
enableShowProtectionnull89     override fun enableShowProtection(isCameraActive: Boolean) {
90         val scanningAnimationRequiredWhenCameraActive =
91                 keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing
92         val faceAuthSucceeded = keyguardUpdateMonitor.isFaceAuthenticated
93         val showScanningAnimationNow = scanningAnimationRequiredWhenCameraActive && isCameraActive
94         if (showScanningAnimationNow == showScanningAnim) {
95             return
96         }
97         logger.cameraProtectionShownOrHidden(
98                 showScanningAnimationNow,
99                 keyguardUpdateMonitor.isFaceDetectionRunning,
100                 authController.isShowing,
101                 faceAuthSucceeded,
102                 isCameraActive,
103                 showScanningAnim)
104         showScanningAnim = showScanningAnimationNow
105         updateProtectionBoundingPath()
106         // Delay the relayout until the end of the animation when hiding,
107         // otherwise we'd clip it.
108         if (showScanningAnim) {
109             visibility = View.VISIBLE
110             requestLayout()
111         }
112 
113         cameraProtectionAnimator?.cancel()
114         cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress,
115                 if (showScanningAnimationNow) SHOW_CAMERA_PROTECTION_SCALE
116                 else HIDDEN_CAMERA_PROTECTION_SCALE).apply {
117             startDelay =
118                     if (showScanningAnim) 0
119                     else if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
120                     else PULSE_ERROR_DISAPPEAR_DURATION
121             duration =
122                     if (showScanningAnim) CAMERA_PROTECTION_APPEAR_DURATION
123                     else if (faceAuthSucceeded) CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION
124                     else CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION
125             interpolator =
126                     if (showScanningAnim) Interpolators.STANDARD_ACCELERATE
127                     else if (faceAuthSucceeded) Interpolators.STANDARD
128                     else Interpolators.STANDARD_DECELERATE
129             addUpdateListener(this@FaceScanningOverlay::updateCameraProtectionProgress)
130             addListener(object : AnimatorListenerAdapter() {
131                 override fun onAnimationEnd(animation: Animator) {
132                     cameraProtectionAnimator = null
133                     if (!showScanningAnim) {
134                         hide()
135                     }
136                 }
137             })
138         }
139 
140         rimAnimator?.cancel()
141         rimAnimator = if (showScanningAnim) {
142             createFaceScanningRimAnimator()
143         } else if (faceAuthSucceeded) {
144             createFaceSuccessRimAnimator()
145         } else {
146             createFaceNotSuccessRimAnimator()
147         }
148         rimAnimator?.apply {
149             addListener(object : AnimatorListenerAdapter() {
150                 override fun onAnimationEnd(animation: Animator) {
151                     rimAnimator = null
152                     if (!showScanningAnim) {
153                         requestLayout()
154                     }
155                 }
156             })
157         }
158         rimAnimator?.start()
159     }
160 
updateVisOnUpdateCutoutnull161     override fun updateVisOnUpdateCutout(): Boolean {
162         return false // instead, we always update the visibility whenever face scanning starts/ends
163     }
164 
updateProtectionBoundingPathnull165     override fun updateProtectionBoundingPath() {
166         super.updateProtectionBoundingPath()
167         rimRect.set(protectionRect)
168         rimRect.scale(rimProgress)
169     }
170 
onMeasurenull171     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
172         if (mBounds.isEmpty()) {
173             super.onMeasure(widthMeasureSpec, heightMeasureSpec)
174             return
175         }
176         if (showScanningAnim) {
177             // Make sure that our measured height encompasses the extra space for the animation
178             mTotalBounds.set(mBoundingRect)
179             mTotalBounds.union(
180                 rimRect.left.toInt(),
181                 rimRect.top.toInt(),
182                 rimRect.right.toInt(),
183                 rimRect.bottom.toInt())
184             val measuredWidth = resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0)
185             val measuredHeight = resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)
186             logger.boundingRect(rimRect, "onMeasure: Face scanning animation")
187             logger.boundingRect(mBoundingRect, "onMeasure: Display cutout view bounding rect")
188             logger.boundingRect(mTotalBounds, "onMeasure: TotalBounds")
189             logger.onMeasureDimensions(widthMeasureSpec,
190                     heightMeasureSpec,
191                     measuredWidth,
192                     measuredHeight)
193             setMeasuredDimension(measuredWidth, measuredHeight)
194         } else {
195             setMeasuredDimension(
196                 resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
197                 resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0))
198         }
199     }
200 
drawFaceScanningRimnull201     private fun drawFaceScanningRim(canvas: Canvas) {
202         val rimPath = Path(protectionPath)
203         scalePath(rimPath, rimProgress)
204         rimPaint.style = Paint.Style.FILL
205         val rimPaintAlpha = rimPaint.alpha
206         rimPaint.color = ColorUtils.blendARGB(
207             faceScanningAnimColor,
208             Color.WHITE,
209             statusBarStateController.dozeAmount
210         )
211         rimPaint.alpha = rimPaintAlpha
212         canvas.drawPath(rimPath, rimPaint)
213     }
214 
drawCameraProtectionnull215     private fun drawCameraProtection(canvas: Canvas) {
216         val scaledProtectionPath = Path(protectionPath)
217         scalePath(scaledProtectionPath, cameraProtectionProgress)
218         paint.style = Paint.Style.FILL
219         paint.color = cameraProtectionColor
220         canvas.drawPath(scaledProtectionPath, paint)
221     }
222 
createFaceSuccessRimAnimatornull223     private fun createFaceSuccessRimAnimator(): AnimatorSet {
224         val rimSuccessAnimator = AnimatorSet()
225         rimSuccessAnimator.playTogether(
226             createRimDisappearAnimator(
227                 PULSE_RADIUS_SUCCESS,
228                 PULSE_SUCCESS_DISAPPEAR_DURATION,
229                 Interpolators.STANDARD_DECELERATE
230             ),
231             createSuccessOpacityAnimator(),
232         )
233         return AnimatorSet().apply {
234             playTogether(rimSuccessAnimator, cameraProtectionAnimator)
235         }
236     }
237 
createFaceNotSuccessRimAnimatornull238     private fun createFaceNotSuccessRimAnimator(): AnimatorSet {
239         return AnimatorSet().apply {
240             playTogether(
241                 createRimDisappearAnimator(
242                     SHOW_CAMERA_PROTECTION_SCALE,
243                     PULSE_ERROR_DISAPPEAR_DURATION,
244                     Interpolators.STANDARD
245                 ),
246                 cameraProtectionAnimator,
247             )
248         }
249     }
250 
createRimDisappearAnimatornull251     private fun createRimDisappearAnimator(
252         endValue: Float,
253         animDuration: Long,
254         timeInterpolator: TimeInterpolator
255     ): ValueAnimator {
256         return ValueAnimator.ofFloat(rimProgress, endValue).apply {
257             duration = animDuration
258             interpolator = timeInterpolator
259             addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
260             addListener(object : AnimatorListenerAdapter() {
261                 override fun onAnimationEnd(animation: Animator) {
262                     rimProgress = HIDDEN_RIM_SCALE
263                     invalidate()
264                 }
265             })
266         }
267     }
268 
createSuccessOpacityAnimatornull269     private fun createSuccessOpacityAnimator(): ValueAnimator {
270         return ValueAnimator.ofInt(255, 0).apply {
271             duration = PULSE_SUCCESS_DISAPPEAR_DURATION
272             interpolator = Interpolators.LINEAR
273             addUpdateListener(this@FaceScanningOverlay::updateRimAlpha)
274             addListener(object : AnimatorListenerAdapter() {
275                 override fun onAnimationEnd(animation: Animator) {
276                     rimPaint.alpha = 255
277                     invalidate()
278                 }
279             })
280         }
281     }
282 
createFaceScanningRimAnimatornull283     private fun createFaceScanningRimAnimator(): AnimatorSet {
284         return AnimatorSet().apply {
285             playSequentially(
286                     cameraProtectionAnimator,
287                     createRimAppearAnimator(),
288             )
289         }
290     }
291 
createRimAppearAnimatornull292     private fun createRimAppearAnimator(): ValueAnimator {
293         return ValueAnimator.ofFloat(
294             SHOW_CAMERA_PROTECTION_SCALE,
295             PULSE_RADIUS_OUT
296         ).apply {
297             duration = PULSE_APPEAR_DURATION
298             interpolator = Interpolators.STANDARD_DECELERATE
299             addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
300         }
301     }
302 
hidenull303     private fun hide() {
304         visibility = INVISIBLE
305         hideOverlayRunnable?.run()
306         hideOverlayRunnable = null
307         requestLayout()
308     }
309 
updateRimProgressnull310     private fun updateRimProgress(animator: ValueAnimator) {
311         rimProgress = animator.animatedValue as Float
312         invalidate()
313     }
314 
updateCameraProtectionProgressnull315     private fun updateCameraProtectionProgress(animator: ValueAnimator) {
316         cameraProtectionProgress = animator.animatedValue as Float
317         invalidate()
318     }
319 
updateRimAlphanull320     private fun updateRimAlpha(animator: ValueAnimator) {
321         rimPaint.alpha = animator.animatedValue as Int
322         invalidate()
323     }
324 
325     companion object {
326         private const val HIDDEN_RIM_SCALE = HIDDEN_CAMERA_PROTECTION_SCALE
327         private const val SHOW_CAMERA_PROTECTION_SCALE = 1f
328 
329         private const val PULSE_RADIUS_OUT = 1.125f
330         private const val PULSE_RADIUS_SUCCESS = 1.25f
331 
332         private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L
333         private const val PULSE_APPEAR_DURATION = 250L // without start delay
334 
335         private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L
336         private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay
337 
338         private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L
339         private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay
340 
scalePathnull341         private fun scalePath(path: Path, scalingFactor: Float) {
342             val scaleMatrix = Matrix().apply {
343                 val boundingRectangle = RectF()
344                 path.computeBounds(boundingRectangle, true)
345                 setScale(
346                     scalingFactor, scalingFactor,
347                     boundingRectangle.centerX(), boundingRectangle.centerY()
348                 )
349             }
350             path.transform(scaleMatrix)
351         }
352     }
353 
dumpnull354     override fun dump(pw: PrintWriter) {
355         val ipw = pw.asIndenting()
356         ipw.increaseIndent()
357         ipw.println("FaceScanningOverlay:")
358         super.dump(ipw)
359         ipw.println("rimProgress=$rimProgress")
360         ipw.println("rimRect=$rimRect")
361         ipw.println("this=$this")
362         ipw.decreaseIndent()
363     }
364 }
365