Add face scanning error animation

On face error or authentication fail, immedilately
retract the face scanning animation. On success,
the rim will expand outwards and decrease in opacity.

Update size of face scanning animation so that it's more
subtle.

Test: manually authenticate with face with bypass
enabled and disabled, check error animation animate away
inwards and success animation is an opacity fade outwards
Bug: 185128224

Change-Id: Id595e60cf13adb35e22eb7249ffd20b22eda827e
This commit is contained in:
Beverly
2022-06-15 19:07:55 +00:00
parent 0592820f44
commit 73607f9077
3 changed files with 161 additions and 44 deletions

View File

@@ -27,12 +27,15 @@ import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.hardware.biometrics.BiometricSourceType
import android.view.View
import androidx.core.graphics.ColorUtils
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.settingslib.Utils
import com.android.systemui.animation.Interpolators
import com.android.systemui.plugins.statusbar.StatusBarStateController
import java.util.concurrent.Executor
/**
* When the face is enrolled, we use this view to show the face scanning animation and the camera
@@ -42,7 +45,8 @@ class FaceScanningOverlay(
context: Context,
pos: Int,
val statusBarStateController: StatusBarStateController,
val keyguardUpdateMonitor: KeyguardUpdateMonitor
val keyguardUpdateMonitor: KeyguardUpdateMonitor,
val mainExecutor: Executor
) : ScreenDecorations.DisplayCutoutView(context, pos) {
private var showScanningAnim = false
private val rimPaint = Paint()
@@ -54,11 +58,26 @@ class FaceScanningOverlay(
com.android.systemui.R.attr.wallpaperTextColorAccent)
private var cameraProtectionAnimator: ValueAnimator? = null
var hideOverlayRunnable: Runnable? = null
var faceAuthSucceeded = false
init {
visibility = View.INVISIBLE // only show this view when face scanning is happening
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
mainExecutor.execute {
keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mainExecutor.execute {
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
}
}
override fun setColor(color: Int) {
cameraProtectionColor = color
invalidate()
@@ -108,7 +127,6 @@ class FaceScanningOverlay(
if (showScanningAnimNow == showScanningAnim) {
return
}
showScanningAnim = showScanningAnimNow
updateProtectionBoundingPath()
// Delay the relayout until the end of the animation when hiding,
@@ -120,13 +138,20 @@ class FaceScanningOverlay(
cameraProtectionAnimator?.cancel()
cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress,
if (showScanningAnimNow) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE).apply {
startDelay = if (showScanningAnim) 0 else PULSE_DISAPPEAR_DURATION
duration = if (showScanningAnim) PULSE_APPEAR_DURATION else
CAMERA_PROTECTION_DISAPPEAR_DURATION
interpolator = if (showScanningAnim) Interpolators.STANDARD else
Interpolators.EMPHASIZED
if (showScanningAnimNow) SHOW_CAMERA_PROTECTION_SCALE
else HIDDEN_CAMERA_PROTECTION_SCALE).apply {
startDelay =
if (showScanningAnim) 0
else if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
else PULSE_ERROR_DISAPPEAR_DURATION
duration =
if (showScanningAnim) CAMERA_PROTECTION_APPEAR_DURATION
else if (faceAuthSucceeded) CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION
else CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION
interpolator =
if (showScanningAnim) Interpolators.STANDARD_ACCELERATE
else if (faceAuthSucceeded) Interpolators.STANDARD
else Interpolators.STANDARD_DECELERATE
addUpdateListener(ValueAnimator.AnimatorUpdateListener {
animation: ValueAnimator ->
cameraProtectionProgress = animation.animatedValue as Float
@@ -143,47 +168,73 @@ class FaceScanningOverlay(
}
}
})
start()
}
rimAnimator?.cancel()
rimAnimator = AnimatorSet().apply {
val rimAppearOrDisappearAnimator = ValueAnimator.ofFloat(rimProgress,
if (showScanningAnim) PULSE_RADIUS_OUT else (PULSE_RADIUS_IN * 1.15f)).apply {
duration = if (showScanningAnim) PULSE_APPEAR_DURATION else PULSE_DISAPPEAR_DURATION
interpolator = Interpolators.STANDARD
addUpdateListener(ValueAnimator.AnimatorUpdateListener {
animation: ValueAnimator ->
rimProgress = animation.animatedValue as Float
invalidate()
})
}
if (showScanningAnim) {
// appear and then pulse in/out
playSequentially(rimAppearOrDisappearAnimator,
val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE,
PULSE_RADIUS_OUT).apply {
duration = PULSE_APPEAR_DURATION
interpolator = Interpolators.STANDARD_DECELERATE
addUpdateListener(ValueAnimator.AnimatorUpdateListener {
animation: ValueAnimator ->
rimProgress = animation.animatedValue as Float
invalidate()
})
}
// animate in camera protection, rim, and then pulse in/out
playSequentially(cameraProtectionAnimator, rimAppearAnimator,
createPulseAnimator(), createPulseAnimator(),
createPulseAnimator(), createPulseAnimator(),
createPulseAnimator(), createPulseAnimator())
} else {
val opacityAnimator = ValueAnimator.ofInt(255, 0).apply {
duration = PULSE_DISAPPEAR_DURATION
interpolator = Interpolators.LINEAR
val rimDisappearAnimator = ValueAnimator.ofFloat(
rimProgress,
if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS
else SHOW_CAMERA_PROTECTION_SCALE
).apply {
duration =
if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
else PULSE_ERROR_DISAPPEAR_DURATION
interpolator =
if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE
else Interpolators.STANDARD
addUpdateListener(ValueAnimator.AnimatorUpdateListener {
animation: ValueAnimator ->
rimPaint.alpha = animation.animatedValue as Int
rimProgress = animation.animatedValue as Float
invalidate()
})
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
rimProgress = HIDDEN_RIM_SCALE
invalidate()
}
})
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
rimProgress = HIDDEN_RIM_SCALE
rimPaint.alpha = 255
invalidate()
if (faceAuthSucceeded) {
val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply {
duration = PULSE_SUCCESS_DISAPPEAR_DURATION
interpolator = Interpolators.LINEAR
addUpdateListener(ValueAnimator.AnimatorUpdateListener {
animation: ValueAnimator ->
rimPaint.alpha = animation.animatedValue as Int
invalidate()
})
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
rimPaint.alpha = 255
invalidate()
}
})
}
})
// disappear
playTogether(rimAppearOrDisappearAnimator, opacityAnimator)
val rimSuccessAnimator = AnimatorSet()
rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator)
playTogether(rimSuccessAnimator, cameraProtectionAnimator)
} else {
playTogether(rimDisappearAnimator, cameraProtectionAnimator)
}
}
addListener(object : AnimatorListenerAdapter() {
@@ -253,15 +304,72 @@ class FaceScanningOverlay(
}
}
private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
override fun onBiometricAuthenticated(
userId: Int,
biometricSourceType: BiometricSourceType?,
isStrongBiometric: Boolean
) {
if (biometricSourceType == BiometricSourceType.FACE) {
post {
faceAuthSucceeded = true
enableShowProtection(true)
}
}
}
override fun onBiometricAcquired(
biometricSourceType: BiometricSourceType?,
acquireInfo: Int
) {
if (biometricSourceType == BiometricSourceType.FACE) {
post {
faceAuthSucceeded = false // reset
}
}
}
override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) {
if (biometricSourceType == BiometricSourceType.FACE) {
post {
faceAuthSucceeded = false
enableShowProtection(false)
}
}
}
override fun onBiometricError(
msgId: Int,
errString: String?,
biometricSourceType: BiometricSourceType?
) {
if (biometricSourceType == BiometricSourceType.FACE) {
post {
faceAuthSucceeded = false
enableShowProtection(false)
}
}
}
}
companion object {
private const val HIDDEN_RIM_SCALE = HIDDEN_CAMERA_PROTECTION_SCALE
private const val SHOW_CAMERA_PROTECTION_SCALE = 1f
private const val PULSE_RADIUS_IN = 1.1f
private const val PULSE_RADIUS_OUT = 1.125f
private const val PULSE_RADIUS_SUCCESS = 1.25f
private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L
private const val PULSE_APPEAR_DURATION = 250L // without start delay
private const val PULSE_APPEAR_DURATION = 350L
private const val PULSE_DURATION_INWARDS = 500L
private const val PULSE_DURATION_OUTWARDS = 500L
private const val PULSE_DISAPPEAR_DURATION = 850L
private const val CAMERA_PROTECTION_DISAPPEAR_DURATION = 700L // excluding start delay
private const val PULSE_RADIUS_IN = 1.15f
private const val PULSE_RADIUS_OUT = 1.25f
private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L
private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay
private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L
private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay
}
}

View File

@@ -32,10 +32,12 @@ import android.widget.FrameLayout
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.FaceScanningOverlay
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import java.util.concurrent.Executor
import javax.inject.Inject
@SysUISingleton
@@ -44,6 +46,7 @@ class FaceScanningProviderFactory @Inject constructor(
private val context: Context,
private val statusBarStateController: StatusBarStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@Main private val mainExecutor: Executor,
private val featureFlags: FeatureFlags
) : DecorProviderFactory() {
private val display = context.display
@@ -82,7 +85,9 @@ class FaceScanningProviderFactory @Inject constructor(
bound.baseOnRotation0(displayInfo.rotation),
authController,
statusBarStateController,
keyguardUpdateMonitor)
keyguardUpdateMonitor,
mainExecutor
)
)
}
}
@@ -102,7 +107,8 @@ class FaceScanningOverlayProviderImpl(
override val alignedBound: Int,
private val authController: AuthController,
private val statusBarStateController: StatusBarStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val mainExecutor: Executor
) : BoundDecorProvider() {
override val viewId: Int = com.android.systemui.R.id.face_scanning_anim
@@ -127,7 +133,9 @@ class FaceScanningOverlayProviderImpl(
context,
alignedBound,
statusBarStateController,
keyguardUpdateMonitor)
keyguardUpdateMonitor,
mainExecutor
)
view.id = viewId
FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT).let {

View File

@@ -210,7 +210,8 @@ public class ScreenDecorationsTest extends SysuiTestCase {
BOUNDS_POSITION_TOP,
mAuthController,
mStatusBarStateController,
mKeyguardUpdateMonitor));
mKeyguardUpdateMonitor,
mExecutor));
mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController,