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