1 /*
<lambda>null2  * Copyright (C) 2023 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.app.StatusBarManager.SESSION_KEYGUARD
20 import com.android.compose.animation.scene.SceneKey
21 import com.android.internal.logging.UiEventLogger
22 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
23 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
24 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
25 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
26 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
27 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim
28 import com.android.systemui.bouncer.data.repository.BouncerRepository
29 import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
30 import com.android.systemui.classifier.FalsingClassifier
31 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
32 import com.android.systemui.dagger.SysUISingleton
33 import com.android.systemui.dagger.qualifiers.Application
34 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
35 import com.android.systemui.log.SessionTracker
36 import com.android.systemui.power.domain.interactor.PowerInteractor
37 import com.android.systemui.scene.domain.interactor.SceneBackInteractor
38 import com.android.systemui.scene.shared.model.Scenes
39 import javax.inject.Inject
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.async
42 import kotlinx.coroutines.flow.Flow
43 import kotlinx.coroutines.flow.MutableSharedFlow
44 import kotlinx.coroutines.flow.SharedFlow
45 import kotlinx.coroutines.flow.StateFlow
46 import kotlinx.coroutines.flow.filter
47 import kotlinx.coroutines.flow.map
48 
49 /** Encapsulates business logic and application state accessing use-cases. */
50 @SysUISingleton
51 class BouncerInteractor
52 @Inject
53 constructor(
54     @Application private val applicationScope: CoroutineScope,
55     private val repository: BouncerRepository,
56     private val authenticationInteractor: AuthenticationInteractor,
57     private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
58     private val falsingInteractor: FalsingInteractor,
59     private val powerInteractor: PowerInteractor,
60     private val uiEventLogger: UiEventLogger,
61     private val sessionTracker: SessionTracker,
62     sceneBackInteractor: SceneBackInteractor,
63 ) {
64     private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>()
65     val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput
66 
67     /** Whether the auto confirm feature is enabled for the currently-selected user. */
68     val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled
69 
70     /** The length of the hinted PIN, or `null`, if pin length hint should not be shown. */
71     val hintedPinLength: StateFlow<Int?> = authenticationInteractor.hintedPinLength
72 
73     /** Whether the pattern should be visible for the currently-selected user. */
74     val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible
75 
76     /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
77     val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
78         authenticationInteractor.isPinEnhancedPrivacyEnabled
79 
80     /** Whether the user switcher should be displayed within the bouncer UI on large screens. */
81     val isUserSwitcherVisible: Boolean
82         get() = repository.isUserSwitcherVisible
83 
84     private val _onImeHiddenByUser = MutableSharedFlow<Unit>()
85     /** Emits a [Unit] each time the IME (keyboard) is hidden by the user. */
86     val onImeHiddenByUser: SharedFlow<Unit> = _onImeHiddenByUser
87 
88     /** Emits a [Unit] each time a lockout is started for the selected user. */
89     val onLockoutStarted: Flow<Unit> =
90         authenticationInteractor.onAuthenticationResult
91             .filter { successfullyAuthenticated ->
92                 !successfullyAuthenticated && authenticationInteractor.lockoutEndTimestamp != null
93             }
94             .map {}
95 
96     /** The scene to show when bouncer is dismissed. */
97     val dismissDestination: Flow<SceneKey> =
98         sceneBackInteractor.backScene
99             .filter { it != Scenes.Bouncer }
100             .map { it ?: Scenes.Lockscreen }
101 
102     /** Notifies that the user has places down a pointer, not necessarily dragging just yet. */
103     fun onDown() {
104         falsingInteractor.avoidGesture()
105     }
106 
107     /**
108      * Notifies of "intentional" (i.e. non-false) user interaction with the UI which is very likely
109      * to be real user interaction with the bouncer and not the result of a false touch in the
110      * user's pocket or by the user's face while holding their device up to their ear.
111      */
112     fun onIntentionalUserInput() {
113         deviceEntryFaceAuthInteractor.onPrimaryBouncerUserInput()
114         powerInteractor.onUserTouch()
115         falsingInteractor.updateFalseConfidence(FalsingClassifier.Result.passed(0.6))
116     }
117 
118     /**
119      * Notifies of false input which is very likely to be the result of a false touch in the user's
120      * pocket or by the user's face while holding their device up to their ear.
121      */
122     fun onFalseUserInput() {
123         falsingInteractor.updateFalseConfidence(
124             FalsingClassifier.Result.falsed(
125                 /* confidence= */ 0.7,
126                 /* context= */ javaClass.simpleName,
127                 /* reason= */ "empty pattern input",
128             )
129         )
130     }
131 
132     /**
133      * Attempts to authenticate based on the given user input.
134      *
135      * If the input is correct, the device will be unlocked and the lock screen and bouncer will be
136      * dismissed and hidden.
137      *
138      * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
139      * supports auto-confirming, and the input's length is at least the required length. Otherwise,
140      * `AuthenticationResult.SKIPPED` is returned.
141      *
142      * @param input The input from the user to try to authenticate with. This can be a list of
143      *   different things, based on the current authentication method.
144      * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
145      *   request to validate.
146      * @return The result of this authentication attempt.
147      */
148     suspend fun authenticate(
149         input: List<Any>,
150         tryAutoConfirm: Boolean = false,
151     ): AuthenticationResult {
152         if (input.isEmpty()) {
153             return AuthenticationResult.SKIPPED
154         }
155 
156         if (authenticationInteractor.getAuthenticationMethod() == Sim) {
157             // SIM is authenticated in SimBouncerInteractor.
158             return AuthenticationResult.SKIPPED
159         }
160 
161         // Switching to the application scope here since this method is often called from
162         // view-models, whose lifecycle (and thus scope) is shorter than this interactor.
163         // This allows the task to continue running properly even when the calling scope has been
164         // cancelled.
165         val authResult =
166             applicationScope
167                 .async { authenticationInteractor.authenticate(input, tryAutoConfirm) }
168                 .await()
169 
170         if (
171             authResult == AuthenticationResult.FAILED ||
172                 (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
173         ) {
174             _onIncorrectBouncerInput.emit(Unit)
175         }
176 
177         if (authenticationInteractor.getAuthenticationMethod() in setOf(Pin, Password, Pattern)) {
178             if (authResult == AuthenticationResult.SUCCEEDED) {
179                 uiEventLogger.log(BouncerUiEvent.BOUNCER_PASSWORD_SUCCESS)
180             } else if (authResult == AuthenticationResult.FAILED) {
181                 uiEventLogger.log(
182                     BouncerUiEvent.BOUNCER_PASSWORD_FAILURE,
183                     sessionTracker.getSessionId(SESSION_KEYGUARD)
184                 )
185             }
186         }
187 
188         return authResult
189     }
190 
191     /** Notifies that the input method editor (software keyboard) has been hidden by the user. */
192     suspend fun onImeHiddenByUser() {
193         _onImeHiddenByUser.emit(Unit)
194     }
195 }
196