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