blob: 71cb35f3041c2c574f3583380a258fe3e001cdb0 [file] [log] [blame]
/*
* 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 androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
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.animation.Interpolators
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
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.LegacyAlternateBouncer
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 UdfpsKeyguardViewController
constructor(
private val view: UdfpsKeyguardView,
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,
) :
UdfpsAnimationViewController<UdfpsKeyguardView>(
view,
statusBarStateController,
shadeExpansionStateManager,
systemUIDialogManager,
dumpManager,
) {
private val useExpandedOverlay: Boolean =
featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
private val isModernAlternateBouncerEnabled: Boolean =
featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER)
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,
UdfpsKeyguardView.ANIMATION_UNLOCKED_SCREEN_OFF
)
}
}
private var inputBouncerExpansion = 0f // only used for modernBouncer
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,
UdfpsKeyguardView.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN
)
}
lastDozeAmount = linear
updatePauseAuth()
}
override fun onStateChanged(statusBarState: Int) {
this@UdfpsKeyguardViewController.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 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@UdfpsKeyguardViewController.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)
if (isModernAlternateBouncerEnabled) listenForAlternateBouncerVisibility(this)
}
}
}
@VisibleForTesting
internal suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job {
return scope.launch {
primaryBouncerInteractor.bouncerExpansion.collect { bouncerExpansion: Float ->
inputBouncerExpansion = bouncerExpansion
updateAlpha()
updatePauseAuth()
}
}
}
@VisibleForTesting
internal 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)
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)
shadeExpansionStateManager.addExpansionListener(shadeExpansionListener)
updateScaleFactor()
view.updatePadding()
updateAlpha()
updatePauseAuth()
keyguardViewManager.setLegacyAlternateBouncer(legacyAlternateBouncer)
keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI)
lockScreenShadeTransitionController.udfpsKeyguardViewController = this
activityLaunchAnimator.addListener(activityLaunchAnimatorListener)
view.mUseExpandedOverlay = useExpandedOverlay
}
override fun onViewDetached() {
super.onViewDetached()
alternateBouncerInteractor.setAlternateBouncerUIAvailable(false)
faceDetectRunning = false
keyguardStateController.removeCallback(keyguardStateControllerCallback)
statusBarStateController.removeCallback(stateListener)
keyguardViewManager.removeLegacyAlternateBouncer(legacyAlternateBouncer)
keyguardViewManager.removeOccludingAppBiometricUI(occludingAppBiometricUI)
keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
configurationController.removeCallback(configurationListener)
shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener)
if (lockScreenShadeTransitionController.udfpsKeyguardViewController === this) {
lockScreenShadeTransitionController.udfpsKeyguardViewController = null
}
activityLaunchAnimator.removeListener(activityLaunchAnimatorListener)
keyguardViewManager.removeCallback(statusBarKeyguardViewManagerCallback)
}
override fun dump(pw: PrintWriter, args: Array<String>) {
super.dump(pw, args)
pw.println("isModernAlternateBouncerEnabled=$isModernAlternateBouncerEnabled")
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
// (ie: dozeAmount = 0f). For the UnlockedScreenOffAnimation, the statusBarState is
// delayed. However, we still animate in the UDFPS affordance with the
// mUnlockedScreenOffDozeAnimator.
if (statusBarState != StatusBarState.KEYGUARD && lastDozeAmount == 0f) {
return true
}
if (isBouncerExpansionGreaterThan(.5f)) {
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) {
alpha = (alpha * (1.0f - activityLaunchProgress)).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) }
}
private val legacyAlternateBouncer: LegacyAlternateBouncer =
object : LegacyAlternateBouncer {
override fun showAlternateBouncer(): Boolean {
return showUdfpsBouncer(true)
}
override fun hideAlternateBouncer(): Boolean {
return showUdfpsBouncer(false)
}
override fun isShowingAlternateBouncer(): Boolean {
return showingUdfpsBouncer
}
}
companion object {
const val TAG = "UdfpsKeyguardViewController"
}
}