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.bouncer.domain.interactor
18 
19 import android.content.Context
20 import android.content.res.ColorStateList
21 import android.hardware.biometrics.BiometricSourceType
22 import android.os.Handler
23 import android.os.Trace
24 import android.util.Log
25 import android.view.View
26 import com.android.keyguard.KeyguardConstants
27 import com.android.keyguard.KeyguardSecurityModel
28 import com.android.keyguard.KeyguardUpdateMonitor
29 import com.android.keyguard.KeyguardUpdateMonitorCallback
30 import com.android.systemui.DejankUtils
31 import com.android.systemui.R
32 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
33 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
34 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
35 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
36 import com.android.systemui.bouncer.ui.BouncerView
37 import com.android.systemui.classifier.FalsingCollector
38 import com.android.systemui.dagger.SysUISingleton
39 import com.android.systemui.dagger.qualifiers.Application
40 import com.android.systemui.dagger.qualifiers.Main
41 import com.android.systemui.flags.FeatureFlags
42 import com.android.systemui.flags.Flags
43 import com.android.systemui.keyguard.DismissCallbackRegistry
44 import com.android.systemui.keyguard.data.repository.TrustRepository
45 import com.android.systemui.plugins.ActivityStarter
46 import com.android.systemui.shared.system.SysUiStatsLog
47 import com.android.systemui.statusbar.policy.KeyguardStateController
48 import javax.inject.Inject
49 import kotlinx.coroutines.CoroutineScope
50 import kotlinx.coroutines.flow.Flow
51 import kotlinx.coroutines.flow.StateFlow
52 import kotlinx.coroutines.flow.combine
53 import kotlinx.coroutines.flow.filter
54 import kotlinx.coroutines.flow.filterNotNull
55 import kotlinx.coroutines.flow.map
56 import kotlinx.coroutines.launch
57 
58 /**
59  * Encapsulates business logic for interacting with the lock-screen primary (pin/pattern/password)
60  * bouncer.
61  */
62 @SysUISingleton
63 class PrimaryBouncerInteractor
64 @Inject
65 constructor(
66     private val repository: KeyguardBouncerRepository,
67     private val primaryBouncerView: BouncerView,
68     @Main private val mainHandler: Handler,
69     private val keyguardStateController: KeyguardStateController,
70     private val keyguardSecurityModel: KeyguardSecurityModel,
71     private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
72     private val falsingCollector: FalsingCollector,
73     private val dismissCallbackRegistry: DismissCallbackRegistry,
74     private val context: Context,
75     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
76     private val trustRepository: TrustRepository,
77     private val featureFlags: FeatureFlags,
78     @Application private val applicationScope: CoroutineScope,
79 ) {
80     private val passiveAuthBouncerDelay =
81         context.resources.getInteger(R.integer.primary_bouncer_passive_auth_delay).toLong()
82     /** Runnable to show the primary bouncer. */
83     val showRunnable = Runnable {
84         repository.setPrimaryShow(true)
85         repository.setPrimaryShowingSoon(false)
86         primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.VISIBLE)
87     }
88 
89     val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
90     val isShowing: StateFlow<Boolean> = repository.primaryBouncerShow
91     val startingToHide: Flow<Unit> = repository.primaryBouncerStartingToHide.filter { it }.map {}
92     val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull()
93     val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull()
94     val startingDisappearAnimation: Flow<Runnable> =
95         repository.primaryBouncerStartingDisappearAnimation.filterNotNull()
96     val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it }
97     val keyguardPosition: Flow<Float> = repository.keyguardPosition.filterNotNull()
98     val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount
99     /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */
100     val bouncerExpansion: Flow<Float> =
101         combine(repository.panelExpansionAmount, repository.primaryBouncerShow) {
102             panelExpansion,
103             primaryBouncerIsShowing ->
104             if (primaryBouncerIsShowing) {
105                 1f - panelExpansion
106             } else {
107                 0f
108             }
109         }
110     /** Allow for interaction when just about fully visible */
111     val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 }
112     val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing
113     private var currentUserActiveUnlockRunning = false
114 
115     /** This callback needs to be a class field so it does not get garbage collected. */
116     val keyguardUpdateMonitorCallback =
117         object : KeyguardUpdateMonitorCallback() {
118             override fun onBiometricRunningStateChanged(
119                 running: Boolean,
120                 biometricSourceType: BiometricSourceType?
121             ) {
122                 updateSideFpsVisibility()
123             }
124 
125             override fun onStrongAuthStateChanged(userId: Int) {
126                 updateSideFpsVisibility()
127             }
128         }
129 
130     init {
131         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
132         if (featureFlags.isEnabled(Flags.DELAY_BOUNCER)) {
133             applicationScope.launch {
134                 trustRepository.isCurrentUserActiveUnlockRunning.collect {
135                     currentUserActiveUnlockRunning = it
136                 }
137             }
138         }
139     }
140 
141     // TODO(b/243685699): Move isScrimmed logic to data layer.
142     // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
143     /** Show the bouncer if necessary and set the relevant states. */
144     @JvmOverloads
145     fun show(isScrimmed: Boolean) {
146         // Reset some states as we show the bouncer.
147         repository.setKeyguardAuthenticated(null)
148         repository.setPrimaryStartingToHide(false)
149 
150         val resumeBouncer =
151             (isBouncerShowing() || repository.primaryBouncerShowingSoon.value) &&
152                 needsFullscreenBouncer()
153 
154         Trace.beginSection("KeyguardBouncer#show")
155         repository.setPrimaryScrimmed(isScrimmed)
156         if (isScrimmed) {
157             setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
158         }
159 
160         // In this special case, we want to hide the bouncer and show it again. We want to emit
161         // show(true) again so that we can reinflate the new view.
162         if (resumeBouncer) {
163             repository.setPrimaryShow(false)
164         }
165 
166         if (primaryBouncerView.delegate?.showNextSecurityScreenOrFinish() == true) {
167             // Keyguard is done.
168             return
169         }
170 
171         repository.setPrimaryShowingSoon(true)
172         if (usePrimaryBouncerPassiveAuthDelay()) {
173             Log.d(TAG, "delay bouncer, passive auth may succeed")
174             mainHandler.postDelayed(showRunnable, passiveAuthBouncerDelay)
175         } else {
176             DejankUtils.postAfterTraversal(showRunnable)
177         }
178         keyguardStateController.notifyPrimaryBouncerShowing(true)
179         primaryBouncerCallbackInteractor.dispatchStartingToShow()
180         Trace.endSection()
181     }
182 
183     /** Sets the correct bouncer states to hide the bouncer. */
184     fun hide() {
185         Trace.beginSection("KeyguardBouncer#hide")
186         if (isFullyShowing()) {
187             SysUiStatsLog.write(
188                 SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
189                 SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN
190             )
191             dismissCallbackRegistry.notifyDismissCancelled()
192         }
193 
194         repository.setPrimaryStartDisappearAnimation(null)
195         falsingCollector.onBouncerHidden()
196         keyguardStateController.notifyPrimaryBouncerShowing(false /* showing */)
197         cancelShowRunnable()
198         repository.setPrimaryShowingSoon(false)
199         repository.setPrimaryShow(false)
200         repository.setPanelExpansion(EXPANSION_HIDDEN)
201         primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
202         Trace.endSection()
203     }
204 
205     /**
206      * Sets the panel expansion which is calculated further upstream. Panel expansion is from 0f
207      * (panel fully hidden) to 1f (panel fully showing). As the panel shows (from 0f => 1f), the
208      * bouncer hides and as the panel becomes hidden (1f => 0f), the bouncer starts to show.
209      * Therefore, a panel expansion of 1f represents the bouncer fully hidden and a panel expansion
210      * of 0f represents the bouncer fully showing.
211      */
212     fun setPanelExpansion(expansion: Float) {
213         val oldExpansion = repository.panelExpansionAmount.value
214         val expansionChanged = oldExpansion != expansion
215         if (repository.primaryBouncerStartingDisappearAnimation.value == null) {
216             repository.setPanelExpansion(expansion)
217         }
218 
219         if (
220             expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
221                 oldExpansion != KeyguardBouncerConstants.EXPANSION_VISIBLE
222         ) {
223             falsingCollector.onBouncerShown()
224             primaryBouncerCallbackInteractor.dispatchFullyShown()
225         } else if (
226             expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN &&
227                 oldExpansion != KeyguardBouncerConstants.EXPANSION_HIDDEN
228         ) {
229             /*
230              * There are cases where #hide() was not invoked, such as when
231              * NotificationPanelViewController controls the hide animation. Make sure the state gets
232              * updated by calling #hide() directly.
233              */
234             hide()
235             DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() }
236             primaryBouncerCallbackInteractor.dispatchFullyHidden()
237         } else if (
238             expansion != KeyguardBouncerConstants.EXPANSION_VISIBLE &&
239                 oldExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE
240         ) {
241             primaryBouncerCallbackInteractor.dispatchStartingToHide()
242             repository.setPrimaryStartingToHide(true)
243         }
244         if (expansionChanged) {
245             primaryBouncerCallbackInteractor.dispatchExpansionChanged(expansion)
246         }
247     }
248 
249     /** Set the initial keyguard message to show when bouncer is shown. */
250     fun showMessage(message: String?, colorStateList: ColorStateList?) {
251         repository.setShowMessage(BouncerShowMessageModel(message, colorStateList))
252     }
253 
254     /**
255      * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is
256      * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we
257      * call cancelAction.
258      */
259     fun setDismissAction(
260         onDismissAction: ActivityStarter.OnDismissAction?,
261         cancelAction: Runnable?
262     ) {
263         primaryBouncerView.delegate?.setDismissAction(onDismissAction, cancelAction)
264     }
265 
266     /** Update the resources of the views. */
267     fun updateResources() {
268         repository.setResourceUpdateRequests(true)
269     }
270 
271     /** Tell the bouncer that keyguard is authenticated. */
272     fun notifyKeyguardAuthenticated(strongAuth: Boolean) {
273         repository.setKeyguardAuthenticated(strongAuth)
274     }
275 
276     /** Update the position of the bouncer when showing. */
277     fun setKeyguardPosition(position: Float) {
278         repository.setKeyguardPosition(position)
279     }
280 
281     /** Notifies that the state change was handled. */
282     fun notifyKeyguardAuthenticatedHandled() {
283         repository.setKeyguardAuthenticated(null)
284     }
285 
286     /** Notifies that the message was shown. */
287     fun onMessageShown() {
288         repository.setShowMessage(null)
289     }
290 
291     /** Notify that the resources have been updated */
292     fun notifyUpdatedResources() {
293         repository.setResourceUpdateRequests(false)
294     }
295 
296     /** Set whether back button is enabled when on the bouncer screen. */
297     fun setBackButtonEnabled(enabled: Boolean) {
298         repository.setIsBackButtonEnabled(enabled)
299     }
300 
301     /** Tell the bouncer to start the pre hide animation. */
302     fun startDisappearAnimation(runnable: Runnable) {
303         if (willRunDismissFromKeyguard()) {
304             runnable.run()
305             return
306         }
307 
308         repository.setPrimaryStartDisappearAnimation(runnable)
309     }
310 
311     /** Determine whether to show the side fps animation. */
312     fun updateSideFpsVisibility() {
313         val sfpsEnabled: Boolean =
314             context.resources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)
315         val fpsDetectionRunning: Boolean = keyguardUpdateMonitor.isFingerprintDetectionRunning
316         val isUnlockingWithFpAllowed: Boolean =
317             keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed
318         val toShow =
319             (isBouncerShowing() &&
320                 sfpsEnabled &&
321                 fpsDetectionRunning &&
322                 isUnlockingWithFpAllowed &&
323                 !isAnimatingAway())
324 
325         if (KeyguardConstants.DEBUG) {
326             Log.d(
327                 TAG,
328                 ("sideFpsToShow=$toShow\n" +
329                     "isBouncerShowing=${isBouncerShowing()}\n" +
330                     "configEnabled=$sfpsEnabled\n" +
331                     "fpsDetectionRunning=$fpsDetectionRunning\n" +
332                     "isUnlockingWithFpAllowed=$isUnlockingWithFpAllowed\n" +
333                     "isAnimatingAway=${isAnimatingAway()}")
334             )
335         }
336         repository.setSideFpsShowing(toShow)
337     }
338 
339     /** Returns whether bouncer is fully showing. */
340     fun isFullyShowing(): Boolean {
341         return (repository.primaryBouncerShowingSoon.value || isBouncerShowing()) &&
342             repository.panelExpansionAmount.value == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
343             repository.primaryBouncerStartingDisappearAnimation.value == null
344     }
345 
346     /** Returns whether bouncer is scrimmed. */
347     fun isScrimmed(): Boolean {
348         return repository.primaryBouncerScrimmed.value
349     }
350 
351     /** If bouncer expansion is between 0f and 1f non-inclusive. */
352     fun isInTransit(): Boolean {
353         return repository.primaryBouncerShowingSoon.value ||
354             repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_HIDDEN &&
355                 repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_VISIBLE
356     }
357 
358     /** Return whether bouncer is animating away. */
359     fun isAnimatingAway(): Boolean {
360         return repository.primaryBouncerStartingDisappearAnimation.value != null
361     }
362 
363     /** Return whether bouncer will dismiss with actions */
364     fun willDismissWithAction(): Boolean {
365         return primaryBouncerView.delegate?.willDismissWithActions() == true
366     }
367 
368     /** Will the dismissal run from the keyguard layout (instead of from bouncer) */
369     fun willRunDismissFromKeyguard(): Boolean {
370         return primaryBouncerView.delegate?.willRunDismissFromKeyguard() == true
371     }
372 
373     /** Returns whether the bouncer should be full screen. */
374     private fun needsFullscreenBouncer(): Boolean {
375         val mode: KeyguardSecurityModel.SecurityMode =
376             keyguardSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser())
377         return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
378             mode == KeyguardSecurityModel.SecurityMode.SimPuk
379     }
380 
381     /** Remove the show runnable from the main handler queue to improve performance. */
382     private fun cancelShowRunnable() {
383         DejankUtils.removeCallbacks(showRunnable)
384         mainHandler.removeCallbacks(showRunnable)
385     }
386 
387     /** Returns whether the primary bouncer is currently showing. */
388     fun isBouncerShowing(): Boolean {
389         return isShowing.value
390     }
391 
392     /** Whether we want to wait to show the bouncer in case passive auth succeeds. */
393     private fun usePrimaryBouncerPassiveAuthDelay(): Boolean {
394         val canRunFaceAuth =
395             keyguardStateController.isFaceAuthEnabled &&
396                 keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE) &&
397                 keyguardUpdateMonitor.doesCurrentPostureAllowFaceAuth()
398         val canRunActiveUnlock =
399             currentUserActiveUnlockRunning &&
400                 keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState()
401 
402         return featureFlags.isEnabled(Flags.DELAY_BOUNCER) &&
403             !needsFullscreenBouncer() &&
404             (canRunFaceAuth || canRunActiveUnlock)
405     }
406 
407     companion object {
408         private const val TAG = "PrimaryBouncerInteractor"
409     }
410 }
411