1 /*
2  * Copyright (C) 2021 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.biometrics
18 
19 import android.animation.ValueAnimator
20 import android.content.res.Configuration
21 import android.util.MathUtils
22 import android.view.MotionEvent
23 import android.view.View
24 import androidx.annotation.VisibleForTesting
25 import androidx.lifecycle.Lifecycle
26 import androidx.lifecycle.repeatOnLifecycle
27 import com.android.app.animation.Interpolators
28 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
29 import com.android.keyguard.KeyguardUpdateMonitor
30 import com.android.systemui.R
31 import com.android.systemui.animation.ActivityLaunchAnimator
32 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
33 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
34 import com.android.systemui.dump.DumpManager
35 import com.android.systemui.flags.FeatureFlags
36 import com.android.systemui.flags.Flags
37 import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
38 import com.android.systemui.lifecycle.repeatWhenAttached
39 import com.android.systemui.plugins.statusbar.StatusBarStateController
40 import com.android.systemui.shade.ShadeExpansionListener
41 import com.android.systemui.shade.ShadeExpansionStateManager
42 import com.android.systemui.statusbar.LockscreenShadeTransitionController
43 import com.android.systemui.statusbar.StatusBarState
44 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
45 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
46 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback
47 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.OccludingAppBiometricUI
48 import com.android.systemui.statusbar.phone.SystemUIDialogManager
49 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
50 import com.android.systemui.statusbar.policy.ConfigurationController
51 import com.android.systemui.statusbar.policy.KeyguardStateController
52 import java.io.PrintWriter
53 import kotlinx.coroutines.CoroutineScope
54 import kotlinx.coroutines.Job
55 import kotlinx.coroutines.launch
56 
57 /** Class that coordinates non-HBM animations during keyguard authentication. */
58 open class UdfpsKeyguardViewControllerLegacy
59 constructor(
60     private val view: UdfpsKeyguardViewLegacy,
61     statusBarStateController: StatusBarStateController,
62     shadeExpansionStateManager: ShadeExpansionStateManager,
63     private val keyguardViewManager: StatusBarKeyguardViewManager,
64     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
65     dumpManager: DumpManager,
66     private val lockScreenShadeTransitionController: LockscreenShadeTransitionController,
67     private val configurationController: ConfigurationController,
68     private val keyguardStateController: KeyguardStateController,
69     private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
70     systemUIDialogManager: SystemUIDialogManager,
71     private val udfpsController: UdfpsController,
72     private val activityLaunchAnimator: ActivityLaunchAnimator,
73     featureFlags: FeatureFlags,
74     private val primaryBouncerInteractor: PrimaryBouncerInteractor,
75     private val alternateBouncerInteractor: AlternateBouncerInteractor,
76     private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
77 ) :
78     UdfpsAnimationViewController<UdfpsKeyguardViewLegacy>(
79         view,
80         statusBarStateController,
81         shadeExpansionStateManager,
82         systemUIDialogManager,
83         dumpManager,
84     ),
85     UdfpsKeyguardViewControllerAdapter {
86     private val uniqueIdentifier = this.toString()
87     private val useExpandedOverlay: Boolean =
88         featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
89     private var showingUdfpsBouncer = false
90     private var udfpsRequested = false
91     private var qsExpansion = 0f
92     private var faceDetectRunning = false
93     private var statusBarState = 0
94     private var transitionToFullShadeProgress = 0f
95     private var lastDozeAmount = 0f
96     private var panelExpansionFraction = 0f
97     private var launchTransitionFadingAway = false
98     private var isLaunchingActivity = false
99     private var activityLaunchProgress = 0f
100     private val unlockedScreenOffDozeAnimator =
101         ValueAnimator.ofFloat(0f, 1f).apply {
102             duration = StackStateAnimator.ANIMATION_DURATION_STANDARD.toLong()
103             interpolator = Interpolators.ALPHA_IN
104             addUpdateListener { animation ->
105                 view.onDozeAmountChanged(
106                     animation.animatedFraction,
107                     animation.animatedValue as Float,
108                     UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF
109                 )
110             }
111         }
112     private var inputBouncerExpansion = 0f
113 
114     private val stateListener: StatusBarStateController.StateListener =
115         object : StatusBarStateController.StateListener {
116             override fun onDozeAmountChanged(linear: Float, eased: Float) {
117                 if (lastDozeAmount < linear) {
118                     showUdfpsBouncer(false)
119                 }
120                 unlockedScreenOffDozeAnimator.cancel()
121                 val animatingFromUnlockedScreenOff =
122                     unlockedScreenOffAnimationController.isAnimationPlaying()
123                 if (animatingFromUnlockedScreenOff && linear != 0f) {
124                     // we manually animate the fade in of the UDFPS icon since the unlocked
125                     // screen off animation prevents the doze amounts to be incrementally eased in
126                     unlockedScreenOffDozeAnimator.start()
127                 } else {
128                     view.onDozeAmountChanged(
129                         linear,
130                         eased,
131                         UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN
132                     )
133                 }
134                 lastDozeAmount = linear
135                 updatePauseAuth()
136             }
137 
138             override fun onStateChanged(statusBarState: Int) {
139                 this@UdfpsKeyguardViewControllerLegacy.statusBarState = statusBarState
140                 updateAlpha()
141                 updatePauseAuth()
142             }
143         }
144 
145     private val configurationListener: ConfigurationController.ConfigurationListener =
146         object : ConfigurationController.ConfigurationListener {
147             override fun onUiModeChanged() {
148                 view.updateColor()
149             }
150 
151             override fun onThemeChanged() {
152                 view.updateColor()
153             }
154 
155             override fun onConfigChanged(newConfig: Configuration) {
156                 updateScaleFactor()
157                 view.updatePadding()
158                 view.updateColor()
159             }
160         }
161 
162     private val shadeExpansionListener = ShadeExpansionListener { (fraction) ->
163         panelExpansionFraction =
164             if (keyguardViewManager.isPrimaryBouncerInTransit) {
165                 aboutToShowBouncerProgress(fraction)
166             } else {
167                 fraction
168             }
169         updateAlpha()
170         updatePauseAuth()
171     }
172 
173     private val keyguardStateControllerCallback: KeyguardStateController.Callback =
174         object : KeyguardStateController.Callback {
175             override fun onUnlockedChanged() {
176                 updatePauseAuth()
177             }
178             override fun onLaunchTransitionFadingAwayChanged() {
179                 launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway
180                 updatePauseAuth()
181             }
182         }
183 
184     private val activityLaunchAnimatorListener: ActivityLaunchAnimator.Listener =
185         object : ActivityLaunchAnimator.Listener {
186             override fun onLaunchAnimationStart() {
187                 isLaunchingActivity = true
188                 activityLaunchProgress = 0f
189                 updateAlpha()
190             }
191 
192             override fun onLaunchAnimationEnd() {
193                 isLaunchingActivity = false
194                 updateAlpha()
195             }
196 
197             override fun onLaunchAnimationProgress(linearProgress: Float) {
198                 activityLaunchProgress = linearProgress
199                 updateAlpha()
200             }
201         }
202 
203     private val statusBarKeyguardViewManagerCallback: KeyguardViewManagerCallback =
204         object : KeyguardViewManagerCallback {
205             override fun onQSExpansionChanged(qsExpansion: Float) {
206                 this@UdfpsKeyguardViewControllerLegacy.qsExpansion = qsExpansion
207                 updateAlpha()
208                 updatePauseAuth()
209             }
210 
211             /**
212              * Forward touches to the UdfpsController. This allows the touch to start from outside
213              * the sensor area and then slide their finger into the sensor area.
214              */
215             override fun onTouch(event: MotionEvent) {
216                 // Don't forward touches if the shade has already started expanding.
217                 if (transitionToFullShadeProgress != 0f) {
218                     return
219                 }
220 
221                 // Forwarding touches not needed with expanded overlay
222                 if (useExpandedOverlay) {
223                     return
224                 } else {
225                     udfpsController.onTouch(event)
226                 }
227             }
228         }
229 
230     private val occludingAppBiometricUI: OccludingAppBiometricUI =
231         object : OccludingAppBiometricUI {
232             override fun requestUdfps(request: Boolean, color: Int) {
233                 udfpsRequested = request
234                 view.requestUdfps(request, color)
235                 updateAlpha()
236                 updatePauseAuth()
237             }
238 
239             override fun dump(pw: PrintWriter) {
240                 pw.println(tag)
241             }
242         }
243 
244     override val tag: String
245         get() = TAG
246 
247     override fun onInit() {
248         super.onInit()
249         keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI)
250     }
251 
252     init {
253         view.repeatWhenAttached {
254             // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion
255             // can make the view not visible; and we still want to listen for events
256             // that may make the view visible again.
257             repeatOnLifecycle(Lifecycle.State.CREATED) {
258                 listenForBouncerExpansion(this)
259                 listenForAlternateBouncerVisibility(this)
260             }
261         }
262     }
263 
264     @VisibleForTesting
265     suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job {
266         return scope.launch {
267             primaryBouncerInteractor.bouncerExpansion.collect { bouncerExpansion: Float ->
268                 inputBouncerExpansion = bouncerExpansion
269                 updateAlpha()
270                 updatePauseAuth()
271             }
272         }
273     }
274 
275     @VisibleForTesting
276     suspend fun listenForAlternateBouncerVisibility(scope: CoroutineScope): Job {
277         return scope.launch {
278             alternateBouncerInteractor.isVisible.collect { isVisible: Boolean ->
279                 showUdfpsBouncer(isVisible)
280             }
281         }
282     }
283 
284     public override fun onViewAttached() {
285         super.onViewAttached()
286         alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, uniqueIdentifier)
287         val dozeAmount = statusBarStateController.dozeAmount
288         lastDozeAmount = dozeAmount
289         stateListener.onDozeAmountChanged(dozeAmount, dozeAmount)
290         statusBarStateController.addCallback(stateListener)
291         udfpsRequested = false
292         launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway
293         keyguardStateController.addCallback(keyguardStateControllerCallback)
294         statusBarState = statusBarStateController.state
295         qsExpansion = keyguardViewManager.qsExpansion
296         keyguardViewManager.addCallback(statusBarKeyguardViewManagerCallback)
297         configurationController.addCallback(configurationListener)
298         val currentState = shadeExpansionStateManager.addExpansionListener(shadeExpansionListener)
299         shadeExpansionListener.onPanelExpansionChanged(currentState)
300         updateScaleFactor()
301         view.updatePadding()
302         updateAlpha()
303         updatePauseAuth()
304         keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI)
305         lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy = this
306         activityLaunchAnimator.addListener(activityLaunchAnimatorListener)
307         view.mUseExpandedOverlay = useExpandedOverlay
308         view.startIconAsyncInflate {
309             val animationViewInternal: View =
310                 view.requireViewById(R.id.udfps_animation_view_internal)
311             animationViewInternal.accessibilityDelegate = udfpsKeyguardAccessibilityDelegate
312         }
313     }
314 
315     override fun onViewDetached() {
316         super.onViewDetached()
317         alternateBouncerInteractor.setAlternateBouncerUIAvailable(false, uniqueIdentifier)
318         faceDetectRunning = false
319         keyguardStateController.removeCallback(keyguardStateControllerCallback)
320         statusBarStateController.removeCallback(stateListener)
321         keyguardViewManager.removeOccludingAppBiometricUI(occludingAppBiometricUI)
322         keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
323         configurationController.removeCallback(configurationListener)
324         shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener)
325         if (lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy === this) {
326             lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy = null
327         }
328         activityLaunchAnimator.removeListener(activityLaunchAnimatorListener)
329         keyguardViewManager.removeCallback(statusBarKeyguardViewManagerCallback)
330     }
331 
332     override fun dump(pw: PrintWriter, args: Array<String>) {
333         super.dump(pw, args)
334         pw.println("showingUdfpsAltBouncer=$showingUdfpsBouncer")
335         pw.println(
336             "altBouncerInteractor#isAlternateBouncerVisible=" +
337                 "${alternateBouncerInteractor.isVisibleState()}"
338         )
339         pw.println(
340             "altBouncerInteractor#canShowAlternateBouncerForFingerprint=" +
341                 "${alternateBouncerInteractor.canShowAlternateBouncerForFingerprint()}"
342         )
343         pw.println("faceDetectRunning=$faceDetectRunning")
344         pw.println("statusBarState=" + StatusBarState.toString(statusBarState))
345         pw.println("transitionToFullShadeProgress=$transitionToFullShadeProgress")
346         pw.println("qsExpansion=$qsExpansion")
347         pw.println("panelExpansionFraction=$panelExpansionFraction")
348         pw.println("unpausedAlpha=" + view.unpausedAlpha)
349         pw.println("udfpsRequestedByApp=$udfpsRequested")
350         pw.println("launchTransitionFadingAway=$launchTransitionFadingAway")
351         pw.println("lastDozeAmount=$lastDozeAmount")
352         pw.println("inputBouncerExpansion=$inputBouncerExpansion")
353         view.dump(pw)
354     }
355 
356     /**
357      * Overrides non-bouncer show logic in shouldPauseAuth to still show icon.
358      *
359      * @return whether the udfpsBouncer has been newly shown or hidden
360      */
361     private fun showUdfpsBouncer(show: Boolean): Boolean {
362         if (showingUdfpsBouncer == show) {
363             return false
364         }
365         val udfpsAffordanceWasNotShowing = shouldPauseAuth()
366         showingUdfpsBouncer = show
367         if (showingUdfpsBouncer) {
368             if (udfpsAffordanceWasNotShowing) {
369                 view.animateInUdfpsBouncer(null)
370             }
371             if (keyguardStateController.isOccluded) {
372                 keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true)
373             }
374             view.announceForAccessibility(
375                 view.context.getString(R.string.accessibility_fingerprint_bouncer)
376             )
377         } else {
378             keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
379         }
380         updateAlpha()
381         updatePauseAuth()
382         return true
383     }
384 
385     /**
386      * Returns true if the fingerprint manager is running but we want to temporarily pause
387      * authentication. On the keyguard, we may want to show udfps when the shade is expanded, so
388      * this can be overridden with the showBouncer method.
389      */
390     override fun shouldPauseAuth(): Boolean {
391         if (showingUdfpsBouncer) {
392             return false
393         }
394         if (
395             udfpsRequested &&
396                 !notificationShadeVisible &&
397                 !isInputBouncerFullyVisible() &&
398                 keyguardStateController.isShowing
399         ) {
400             return false
401         }
402         if (launchTransitionFadingAway) {
403             return true
404         }
405 
406         // Only pause auth if we're not on the keyguard AND we're not transitioning to doze.
407         // For the UnlockedScreenOffAnimation, the statusBarState is
408         // delayed. However, we still animate in the UDFPS affordance with the
409         // unlockedScreenOffDozeAnimator.
410         if (
411             statusBarState != StatusBarState.KEYGUARD &&
412                 !unlockedScreenOffAnimationController.isAnimationPlaying()
413         ) {
414             return true
415         }
416         if (isBouncerExpansionGreaterThan(.5f)) {
417             return true
418         }
419         if (
420             keyguardUpdateMonitor.getUserUnlockedWithBiometric(
421                 KeyguardUpdateMonitor.getCurrentUser()
422             )
423         ) {
424             // If the device was unlocked by a biometric, immediately hide the UDFPS icon to avoid
425             // overlap with the LockIconView. Shortly afterwards, UDFPS will stop running.
426             return true
427         }
428         return view.unpausedAlpha < 255 * .1
429     }
430 
431     fun isBouncerExpansionGreaterThan(bouncerExpansionThreshold: Float): Boolean {
432         return inputBouncerExpansion >= bouncerExpansionThreshold
433     }
434 
435     fun isInputBouncerFullyVisible(): Boolean {
436         return inputBouncerExpansion == 1f
437     }
438 
439     override fun listenForTouchesOutsideView(): Boolean {
440         return true
441     }
442 
443     /**
444      * Set the progress we're currently transitioning to the full shade. 0.0f means we're not
445      * transitioning yet, while 1.0f means we've fully dragged down. For example, start swiping down
446      * to expand the notification shade from the empty space in the middle of the lock screen.
447      */
448     fun setTransitionToFullShadeProgress(progress: Float) {
449         transitionToFullShadeProgress = progress
450         updateAlpha()
451     }
452 
453     /**
454      * Update alpha for the UDFPS lock screen affordance. The AoD UDFPS visual affordance's alpha is
455      * based on the doze amount.
456      */
457     override fun updateAlpha() {
458         // Fade icon on transitions to showing the status bar or bouncer, but if mUdfpsRequested,
459         // then the keyguard is occluded by some application - so instead use the input bouncer
460         // hidden amount to determine the fade.
461         val expansion = if (udfpsRequested) getInputBouncerHiddenAmt() else panelExpansionFraction
462         var alpha: Int =
463             if (showingUdfpsBouncer) 255
464             else MathUtils.constrain(MathUtils.map(.5f, .9f, 0f, 255f, expansion), 0f, 255f).toInt()
465         if (!showingUdfpsBouncer) {
466             // swipe from top of the lockscreen to expand full QS:
467             alpha =
468                 (alpha * (1.0f - Interpolators.EMPHASIZED_DECELERATE.getInterpolation(qsExpansion)))
469                     .toInt()
470 
471             // swipe from the middle (empty space) of lockscreen to expand the notification shade:
472             alpha = (alpha * (1.0f - transitionToFullShadeProgress)).toInt()
473 
474             // Fade out the icon if we are animating an activity launch over the lockscreen and the
475             // activity didn't request the UDFPS.
476             if (isLaunchingActivity && !udfpsRequested) {
477                 val udfpsActivityLaunchAlphaMultiplier =
478                     1f -
479                         (activityLaunchProgress *
480                                 (ActivityLaunchAnimator.TIMINGS.totalDuration / 83))
481                             .coerceIn(0f, 1f)
482                 alpha = (alpha * udfpsActivityLaunchAlphaMultiplier).toInt()
483             }
484 
485             // Fade out alpha when a dialog is shown
486             // Fade in alpha when a dialog is hidden
487             alpha = (alpha * view.dialogSuggestedAlpha).toInt()
488         }
489         view.unpausedAlpha = alpha
490     }
491 
492     private fun getInputBouncerHiddenAmt(): Float {
493         return 1f - inputBouncerExpansion
494     }
495 
496     /** Update the scale factor based on the device's resolution. */
497     private fun updateScaleFactor() {
498         udfpsController.mOverlayParams?.scaleFactor?.let { view.setScaleFactor(it) }
499     }
500     companion object {
501         const val TAG = "UdfpsKeyguardViewController"
502     }
503 }
504