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