/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.biometrics import android.animation.ValueAnimator import android.content.res.Configuration import android.util.MathUtils import android.view.MotionEvent import android.view.View import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionListener import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.OccludingAppBiometricUI import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import java.io.PrintWriter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch /** Class that coordinates non-HBM animations during keyguard authentication. */ open class UdfpsKeyguardViewControllerLegacy constructor( private val view: UdfpsKeyguardViewLegacy, statusBarStateController: StatusBarStateController, shadeExpansionStateManager: ShadeExpansionStateManager, private val keyguardViewManager: StatusBarKeyguardViewManager, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, dumpManager: DumpManager, private val lockScreenShadeTransitionController: LockscreenShadeTransitionController, private val configurationController: ConfigurationController, private val keyguardStateController: KeyguardStateController, private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, systemUIDialogManager: SystemUIDialogManager, private val udfpsController: UdfpsController, private val activityLaunchAnimator: ActivityLaunchAnimator, featureFlags: FeatureFlags, private val primaryBouncerInteractor: PrimaryBouncerInteractor, private val alternateBouncerInteractor: AlternateBouncerInteractor, private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate, ) : UdfpsAnimationViewController( view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager, dumpManager, ), UdfpsKeyguardViewControllerAdapter { private val uniqueIdentifier = this.toString() private val useExpandedOverlay: Boolean = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) private var showingUdfpsBouncer = false private var udfpsRequested = false private var qsExpansion = 0f private var faceDetectRunning = false private var statusBarState = 0 private var transitionToFullShadeProgress = 0f private var lastDozeAmount = 0f private var panelExpansionFraction = 0f private var launchTransitionFadingAway = false private var isLaunchingActivity = false private var activityLaunchProgress = 0f private val unlockedScreenOffDozeAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = StackStateAnimator.ANIMATION_DURATION_STANDARD.toLong() interpolator = Interpolators.ALPHA_IN addUpdateListener { animation -> view.onDozeAmountChanged( animation.animatedFraction, animation.animatedValue as Float, UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF ) } } private var inputBouncerExpansion = 0f private val stateListener: StatusBarStateController.StateListener = object : StatusBarStateController.StateListener { override fun onDozeAmountChanged(linear: Float, eased: Float) { if (lastDozeAmount < linear) { showUdfpsBouncer(false) } unlockedScreenOffDozeAnimator.cancel() val animatingFromUnlockedScreenOff = unlockedScreenOffAnimationController.isAnimationPlaying() if (animatingFromUnlockedScreenOff && linear != 0f) { // we manually animate the fade in of the UDFPS icon since the unlocked // screen off animation prevents the doze amounts to be incrementally eased in unlockedScreenOffDozeAnimator.start() } else { view.onDozeAmountChanged( linear, eased, UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN ) } lastDozeAmount = linear updatePauseAuth() } override fun onStateChanged(statusBarState: Int) { this@UdfpsKeyguardViewControllerLegacy.statusBarState = statusBarState updateAlpha() updatePauseAuth() } } private val configurationListener: ConfigurationController.ConfigurationListener = object : ConfigurationController.ConfigurationListener { override fun onUiModeChanged() { view.updateColor() } override fun onThemeChanged() { view.updateColor() } override fun onConfigChanged(newConfig: Configuration) { updateScaleFactor() view.updatePadding() view.updateColor() } } private val shadeExpansionListener = ShadeExpansionListener { (fraction) -> panelExpansionFraction = if (keyguardViewManager.isPrimaryBouncerInTransit) { aboutToShowBouncerProgress(fraction) } else { fraction } updateAlpha() updatePauseAuth() } private val keyguardStateControllerCallback: KeyguardStateController.Callback = object : KeyguardStateController.Callback { override fun onUnlockedChanged() { updatePauseAuth() } override fun onLaunchTransitionFadingAwayChanged() { launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway updatePauseAuth() } } private val activityLaunchAnimatorListener: ActivityLaunchAnimator.Listener = object : ActivityLaunchAnimator.Listener { override fun onLaunchAnimationStart() { isLaunchingActivity = true activityLaunchProgress = 0f updateAlpha() } override fun onLaunchAnimationEnd() { isLaunchingActivity = false updateAlpha() } override fun onLaunchAnimationProgress(linearProgress: Float) { activityLaunchProgress = linearProgress updateAlpha() } } private val statusBarKeyguardViewManagerCallback: KeyguardViewManagerCallback = object : KeyguardViewManagerCallback { override fun onQSExpansionChanged(qsExpansion: Float) { this@UdfpsKeyguardViewControllerLegacy.qsExpansion = qsExpansion updateAlpha() updatePauseAuth() } /** * Forward touches to the UdfpsController. This allows the touch to start from outside * the sensor area and then slide their finger into the sensor area. */ override fun onTouch(event: MotionEvent) { // Don't forward touches if the shade has already started expanding. if (transitionToFullShadeProgress != 0f) { return } // Forwarding touches not needed with expanded overlay if (useExpandedOverlay) { return } else { udfpsController.onTouch(event) } } } private val occludingAppBiometricUI: OccludingAppBiometricUI = object : OccludingAppBiometricUI { override fun requestUdfps(request: Boolean, color: Int) { udfpsRequested = request view.requestUdfps(request, color) updateAlpha() updatePauseAuth() } override fun dump(pw: PrintWriter) { pw.println(tag) } } override val tag: String get() = TAG override fun onInit() { super.onInit() keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI) } init { view.repeatWhenAttached { // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion // can make the view not visible; and we still want to listen for events // that may make the view visible again. repeatOnLifecycle(Lifecycle.State.CREATED) { listenForBouncerExpansion(this) listenForAlternateBouncerVisibility(this) } } } @VisibleForTesting suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job { return scope.launch { primaryBouncerInteractor.bouncerExpansion.collect { bouncerExpansion: Float -> inputBouncerExpansion = bouncerExpansion updateAlpha() updatePauseAuth() } } } @VisibleForTesting suspend fun listenForAlternateBouncerVisibility(scope: CoroutineScope): Job { return scope.launch { alternateBouncerInteractor.isVisible.collect { isVisible: Boolean -> showUdfpsBouncer(isVisible) } } } public override fun onViewAttached() { super.onViewAttached() alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, uniqueIdentifier) val dozeAmount = statusBarStateController.dozeAmount lastDozeAmount = dozeAmount stateListener.onDozeAmountChanged(dozeAmount, dozeAmount) statusBarStateController.addCallback(stateListener) udfpsRequested = false launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway keyguardStateController.addCallback(keyguardStateControllerCallback) statusBarState = statusBarStateController.state qsExpansion = keyguardViewManager.qsExpansion keyguardViewManager.addCallback(statusBarKeyguardViewManagerCallback) configurationController.addCallback(configurationListener) val currentState = shadeExpansionStateManager.addExpansionListener(shadeExpansionListener) shadeExpansionListener.onPanelExpansionChanged(currentState) updateScaleFactor() view.updatePadding() updateAlpha() updatePauseAuth() keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI) lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy = this activityLaunchAnimator.addListener(activityLaunchAnimatorListener) view.mUseExpandedOverlay = useExpandedOverlay view.startIconAsyncInflate { val animationViewInternal: View = view.requireViewById(R.id.udfps_animation_view_internal) animationViewInternal.accessibilityDelegate = udfpsKeyguardAccessibilityDelegate } } override fun onViewDetached() { super.onViewDetached() alternateBouncerInteractor.setAlternateBouncerUIAvailable(false, uniqueIdentifier) faceDetectRunning = false keyguardStateController.removeCallback(keyguardStateControllerCallback) statusBarStateController.removeCallback(stateListener) keyguardViewManager.removeOccludingAppBiometricUI(occludingAppBiometricUI) keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false) configurationController.removeCallback(configurationListener) shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener) if (lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy === this) { lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy = null } activityLaunchAnimator.removeListener(activityLaunchAnimatorListener) keyguardViewManager.removeCallback(statusBarKeyguardViewManagerCallback) } override fun dump(pw: PrintWriter, args: Array) { super.dump(pw, args) pw.println("showingUdfpsAltBouncer=$showingUdfpsBouncer") pw.println( "altBouncerInteractor#isAlternateBouncerVisible=" + "${alternateBouncerInteractor.isVisibleState()}" ) pw.println( "altBouncerInteractor#canShowAlternateBouncerForFingerprint=" + "${alternateBouncerInteractor.canShowAlternateBouncerForFingerprint()}" ) pw.println("faceDetectRunning=$faceDetectRunning") pw.println("statusBarState=" + StatusBarState.toString(statusBarState)) pw.println("transitionToFullShadeProgress=$transitionToFullShadeProgress") pw.println("qsExpansion=$qsExpansion") pw.println("panelExpansionFraction=$panelExpansionFraction") pw.println("unpausedAlpha=" + view.unpausedAlpha) pw.println("udfpsRequestedByApp=$udfpsRequested") pw.println("launchTransitionFadingAway=$launchTransitionFadingAway") pw.println("lastDozeAmount=$lastDozeAmount") pw.println("inputBouncerExpansion=$inputBouncerExpansion") view.dump(pw) } /** * Overrides non-bouncer show logic in shouldPauseAuth to still show icon. * * @return whether the udfpsBouncer has been newly shown or hidden */ private fun showUdfpsBouncer(show: Boolean): Boolean { if (showingUdfpsBouncer == show) { return false } val udfpsAffordanceWasNotShowing = shouldPauseAuth() showingUdfpsBouncer = show if (showingUdfpsBouncer) { if (udfpsAffordanceWasNotShowing) { view.animateInUdfpsBouncer(null) } if (keyguardStateController.isOccluded) { keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true) } view.announceForAccessibility( view.context.getString(R.string.accessibility_fingerprint_bouncer) ) } else { keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false) } updateAlpha() updatePauseAuth() return true } /** * Returns true if the fingerprint manager is running but we want to temporarily pause * authentication. On the keyguard, we may want to show udfps when the shade is expanded, so * this can be overridden with the showBouncer method. */ override fun shouldPauseAuth(): Boolean { if (showingUdfpsBouncer) { return false } if ( udfpsRequested && !notificationShadeVisible && !isInputBouncerFullyVisible() && keyguardStateController.isShowing ) { return false } if (launchTransitionFadingAway) { return true } // Only pause auth if we're not on the keyguard AND we're not transitioning to doze. // For the UnlockedScreenOffAnimation, the statusBarState is // delayed. However, we still animate in the UDFPS affordance with the // unlockedScreenOffDozeAnimator. if ( statusBarState != StatusBarState.KEYGUARD && !unlockedScreenOffAnimationController.isAnimationPlaying() ) { return true } if (isBouncerExpansionGreaterThan(.5f)) { return true } if ( keyguardUpdateMonitor.getUserUnlockedWithBiometric( KeyguardUpdateMonitor.getCurrentUser() ) ) { // If the device was unlocked by a biometric, immediately hide the UDFPS icon to avoid // overlap with the LockIconView. Shortly afterwards, UDFPS will stop running. return true } return view.unpausedAlpha < 255 * .1 } fun isBouncerExpansionGreaterThan(bouncerExpansionThreshold: Float): Boolean { return inputBouncerExpansion >= bouncerExpansionThreshold } fun isInputBouncerFullyVisible(): Boolean { return inputBouncerExpansion == 1f } override fun listenForTouchesOutsideView(): Boolean { return true } /** * Set the progress we're currently transitioning to the full shade. 0.0f means we're not * transitioning yet, while 1.0f means we've fully dragged down. For example, start swiping down * to expand the notification shade from the empty space in the middle of the lock screen. */ fun setTransitionToFullShadeProgress(progress: Float) { transitionToFullShadeProgress = progress updateAlpha() } /** * Update alpha for the UDFPS lock screen affordance. The AoD UDFPS visual affordance's alpha is * based on the doze amount. */ override fun updateAlpha() { // Fade icon on transitions to showing the status bar or bouncer, but if mUdfpsRequested, // then the keyguard is occluded by some application - so instead use the input bouncer // hidden amount to determine the fade. val expansion = if (udfpsRequested) getInputBouncerHiddenAmt() else panelExpansionFraction var alpha: Int = if (showingUdfpsBouncer) 255 else MathUtils.constrain(MathUtils.map(.5f, .9f, 0f, 255f, expansion), 0f, 255f).toInt() if (!showingUdfpsBouncer) { // swipe from top of the lockscreen to expand full QS: alpha = (alpha * (1.0f - Interpolators.EMPHASIZED_DECELERATE.getInterpolation(qsExpansion))) .toInt() // swipe from the middle (empty space) of lockscreen to expand the notification shade: alpha = (alpha * (1.0f - transitionToFullShadeProgress)).toInt() // Fade out the icon if we are animating an activity launch over the lockscreen and the // activity didn't request the UDFPS. if (isLaunchingActivity && !udfpsRequested) { val udfpsActivityLaunchAlphaMultiplier = 1f - (activityLaunchProgress * (ActivityLaunchAnimator.TIMINGS.totalDuration / 83)) .coerceIn(0f, 1f) alpha = (alpha * udfpsActivityLaunchAlphaMultiplier).toInt() } // Fade out alpha when a dialog is shown // Fade in alpha when a dialog is hidden alpha = (alpha * view.dialogSuggestedAlpha).toInt() } view.unpausedAlpha = alpha } private fun getInputBouncerHiddenAmt(): Float { return 1f - inputBouncerExpansion } /** Update the scale factor based on the device's resolution. */ private fun updateScaleFactor() { udfpsController.mOverlayParams?.scaleFactor?.let { view.setScaleFactor(it) } } companion object { const val TAG = "UdfpsKeyguardViewController" } }