• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.tracing.FlowTracing.traceAsCounter
21 import com.android.app.tracing.coroutines.asyncTraced as async
22 import com.android.compose.animation.scene.ObservableTransitionState
23 import com.android.compose.animation.scene.SceneKey
24 import com.android.internal.logging.UiEventLogger
25 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
26 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
27 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
28 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
29 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
30 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim
31 import com.android.systemui.authentication.shared.model.BouncerInputSide
32 import com.android.systemui.bouncer.data.repository.BouncerRepository
33 import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
34 import com.android.systemui.classifier.FalsingClassifier
35 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
36 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
37 import com.android.systemui.dagger.SysUISingleton
38 import com.android.systemui.dagger.qualifiers.Application
39 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
40 import com.android.systemui.log.SessionTracker
41 import com.android.systemui.power.domain.interactor.PowerInteractor
42 import com.android.systemui.scene.domain.interactor.SceneBackInteractor
43 import com.android.systemui.scene.domain.interactor.SceneInteractor
44 import com.android.systemui.scene.shared.flag.SceneContainerFlag
45 import com.android.systemui.scene.shared.model.Overlays
46 import com.android.systemui.scene.shared.model.Scenes
47 import com.android.systemui.shade.ShadeDisplayAware
48 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
49 import javax.inject.Inject
50 import kotlinx.coroutines.CoroutineScope
51 import kotlinx.coroutines.flow.Flow
52 import kotlinx.coroutines.flow.MutableSharedFlow
53 import kotlinx.coroutines.flow.SharedFlow
54 import kotlinx.coroutines.flow.StateFlow
55 import kotlinx.coroutines.flow.asStateFlow
56 import kotlinx.coroutines.flow.combine
57 import kotlinx.coroutines.flow.distinctUntilChanged
58 import kotlinx.coroutines.flow.filter
59 import kotlinx.coroutines.flow.flowOf
60 import kotlinx.coroutines.flow.map
61 
62 /** Encapsulates business logic and application state accessing use-cases. */
63 @SysUISingleton
64 class BouncerInteractor
65 @Inject
66 constructor(
67     @Application private val applicationScope: CoroutineScope,
68     private val repository: BouncerRepository,
69     private val authenticationInteractor: AuthenticationInteractor,
70     private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
71     private val falsingInteractor: FalsingInteractor,
72     private val powerInteractor: PowerInteractor,
73     private val uiEventLogger: UiEventLogger,
74     private val sessionTracker: SessionTracker,
75     sceneInteractor: SceneInteractor,
76     sceneBackInteractor: SceneBackInteractor,
77     @ShadeDisplayAware private val configurationInteractor: ConfigurationInteractor,
78 ) {
79     private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>()
80     val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput
81 
82     /** Whether the auto confirm feature is enabled for the currently-selected user. */
83     val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled
84 
85     /** The length of the hinted PIN, or `null`, if pin length hint should not be shown. */
86     val hintedPinLength: StateFlow<Int?> = authenticationInteractor.hintedPinLength
87 
88     /** Whether the pattern should be visible for the currently-selected user. */
89     val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible
90 
91     /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
92     val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
93         authenticationInteractor.isPinEnhancedPrivacyEnabled
94 
95     /** Whether the user switcher should be displayed within the bouncer UI on large screens. */
96     val isUserSwitcherVisible: Flow<Boolean> =
97         authenticationInteractor.authenticationMethod.map { authMethod ->
98             when (authMethod) {
99                 Sim -> false
100                 else -> repository.isUserSwitcherEnabledInConfig
101             }
102         }
103 
104     /**
105      * Whether one handed bouncer mode is supported on large screen devices. This allows user to
106      * double tap on the half of the screen to bring the bouncer input to that side of the screen.
107      */
108     val isOneHandedModeSupported: Flow<Boolean> =
109         combine(
110             isUserSwitcherVisible,
111             authenticationInteractor.authenticationMethod,
112             configurationInteractor.onAnyConfigurationChange,
113         ) { userSwitcherVisible, authMethod, _ ->
114             userSwitcherVisible ||
115                 (repository.isOneHandedBouncerSupportedInConfig && (authMethod !is Password))
116         }
117 
118     /**
119      * Preferred side of the screen where the input area on the bouncer should be. This is
120      * applicable for large screen devices (foldables and tablets).
121      */
122     val preferredBouncerInputSide: Flow<BouncerInputSide?> =
123         combine(
124             configurationInteractor.onAnyConfigurationChange,
125             repository.preferredBouncerInputSide,
126         ) { _, _ ->
127             // always read the setting as that can change outside of this
128             // repository (tests/manual testing)
129             val preferredInputSide = repository.getPreferredInputSideSetting()
130             when {
131                 preferredInputSide != null -> preferredInputSide
132                 repository.isUserSwitcherEnabledInConfig -> BouncerInputSide.RIGHT
133                 repository.isOneHandedBouncerSupportedInConfig -> BouncerInputSide.LEFT
134                 else -> null
135             }
136         }
137 
138     private val _onImeHiddenByUser = MutableSharedFlow<Unit>()
139     /** Emits a [Unit] each time the IME (keyboard) is hidden by the user. */
140     val onImeHiddenByUser: SharedFlow<Unit> = _onImeHiddenByUser
141 
142     /** Emits a [Unit] each time a lockout is started for the selected user. */
143     val onLockoutStarted: Flow<Unit> =
144         authenticationInteractor.onAuthenticationResult
145             .filter { successfullyAuthenticated ->
146                 !successfullyAuthenticated && authenticationInteractor.lockoutEndTimestamp != null
147             }
148             .map {}
149 
150     /** X coordinate of the last recorded touch position on the lockscreen. */
151     val lastRecordedLockscreenTouchPosition = repository.lastRecordedLockscreenTouchPosition
152 
153     /** Value between 0-1 that specifies by how much the bouncer UI should be scaled down. */
154     val scale: StateFlow<Float> = repository.scale.asStateFlow()
155 
156     /** The scene to show when bouncer is dismissed. */
157     val dismissDestination: Flow<SceneKey> =
158         sceneBackInteractor.backScene.map { it ?: Scenes.Lockscreen }
159 
160     /** The amount [0-1] that the Bouncer Overlay has been transitioned to. */
161     val bouncerExpansion: Flow<Float> =
162         if (SceneContainerFlag.isEnabled) {
163                 sceneInteractor.transitionState.flatMapLatestConflated { state ->
164                     when (state) {
165                         is ObservableTransitionState.Idle ->
166                             flowOf(if (Overlays.Bouncer in state.currentOverlays) 1f else 0f)
167                         is ObservableTransitionState.Transition ->
168                             if (state.toContent == Overlays.Bouncer) {
169                                 state.progress
170                             } else if (state.fromContent == Overlays.Bouncer) {
171                                 state.progress.map { progress -> 1 - progress }
172                             } else {
173                                 state.currentOverlays().map {
174                                     if (Overlays.Bouncer in it) 1f else 0f
175                                 }
176                             }
177                     }
178                 }
179             } else {
180                 flowOf()
181             }
182             .distinctUntilChanged()
183             .traceAsCounter("bouncer_expansion") { (it * 100f).toInt() }
184 
185     /** Notifies that the user has places down a pointer, not necessarily dragging just yet. */
186     fun onDown() {
187         falsingInteractor.avoidGesture()
188     }
189 
190     /**
191      * Notifies of "intentional" (i.e. non-false) user interaction with the UI which is very likely
192      * to be real user interaction with the bouncer and not the result of a false touch in the
193      * user's pocket or by the user's face while holding their device up to their ear.
194      */
195     fun onIntentionalUserInput() {
196         deviceEntryFaceAuthInteractor.onPrimaryBouncerUserInput()
197         powerInteractor.onUserTouch()
198         falsingInteractor.updateFalseConfidence(FalsingClassifier.Result.passed(0.6))
199     }
200 
201     /**
202      * Notifies of false input which is very likely to be the result of a false touch in the user's
203      * pocket or by the user's face while holding their device up to their ear.
204      */
205     fun onFalseUserInput() {
206         falsingInteractor.updateFalseConfidence(
207             FalsingClassifier.Result.falsed(
208                 /* confidence= */ 0.7,
209                 /* context= */ javaClass.simpleName,
210                 /* reason= */ "empty pattern input",
211             )
212         )
213     }
214 
215     /** Update the preferred input side for the bouncer. */
216     fun setPreferredBouncerInputSide(inputSide: BouncerInputSide) {
217         repository.setPreferredBouncerInputSide(inputSide)
218     }
219 
220     /**
221      * Record the x coordinate of the last touch position on the lockscreen. This will be used to
222      * determine which side of the bouncer the input area should be shown.
223      */
224     fun recordKeyguardTouchPosition(x: Float) {
225         // todo (b/375245685) investigate why this is not working as expected when it is
226         //  wired up with SBKVM
227         repository.recordLockscreenTouchPosition(x)
228     }
229 
230     fun onBackEventProgressed(progress: Float) {
231         // this is applicable only for compose bouncer without flexiglass
232         SceneContainerFlag.assertInLegacyMode()
233         repository.scale.value = (mapBackEventProgressToScale(progress))
234     }
235 
236     fun onBackEventCancelled() {
237         // this is applicable only for compose bouncer without flexiglass
238         SceneContainerFlag.assertInLegacyMode()
239         repository.scale.value = DEFAULT_SCALE
240     }
241 
242     fun resetScale() {
243         repository.scale.value = DEFAULT_SCALE
244     }
245 
246     /**
247      * Attempts to authenticate based on the given user input.
248      *
249      * If the input is correct, the device will be unlocked and the lock screen and bouncer will be
250      * dismissed and hidden.
251      *
252      * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
253      * supports auto-confirming, and the input's length is at least the required length. Otherwise,
254      * `AuthenticationResult.SKIPPED` is returned.
255      *
256      * @param input The input from the user to try to authenticate with. This can be a list of
257      *   different things, based on the current authentication method.
258      * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
259      *   request to validate.
260      * @return The result of this authentication attempt.
261      */
262     suspend fun authenticate(
263         input: List<Any>,
264         tryAutoConfirm: Boolean = false,
265     ): AuthenticationResult {
266         if (input.isEmpty()) {
267             return AuthenticationResult.SKIPPED
268         }
269 
270         if (authenticationInteractor.getAuthenticationMethod() == Sim) {
271             // SIM is authenticated in SimBouncerInteractor.
272             return AuthenticationResult.SKIPPED
273         }
274 
275         // Switching to the application scope here since this method is often called from
276         // view-models, whose lifecycle (and thus scope) is shorter than this interactor.
277         // This allows the task to continue running properly even when the calling scope has been
278         // cancelled.
279         val authResult =
280             applicationScope
281                 .async { authenticationInteractor.authenticate(input, tryAutoConfirm) }
282                 .await()
283 
284         if (
285             authResult == AuthenticationResult.FAILED ||
286                 (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
287         ) {
288             _onIncorrectBouncerInput.emit(Unit)
289         }
290 
291         if (authenticationInteractor.getAuthenticationMethod() in setOf(Pin, Password, Pattern)) {
292             if (authResult == AuthenticationResult.SUCCEEDED) {
293                 uiEventLogger.log(BouncerUiEvent.BOUNCER_PASSWORD_SUCCESS)
294             } else if (authResult == AuthenticationResult.FAILED) {
295                 uiEventLogger.log(
296                     BouncerUiEvent.BOUNCER_PASSWORD_FAILURE,
297                     sessionTracker.getSessionId(SESSION_KEYGUARD),
298                 )
299             }
300         }
301 
302         return authResult
303     }
304 
305     /** Notifies that the input method editor (software keyboard) has been hidden by the user. */
306     suspend fun onImeHiddenByUser() {
307         _onImeHiddenByUser.emit(Unit)
308     }
309 
310     private fun mapBackEventProgressToScale(progress: Float): Float {
311         // TODO(b/263819310): Update the interpolator to match spec.
312         return MIN_BACK_SCALE + (1 - MIN_BACK_SCALE) * (1 - progress)
313     }
314 
315     companion object {
316         // How much the view scales down to during back gestures.
317         private const val MIN_BACK_SCALE: Float = 0.9f
318         private const val DEFAULT_SCALE: Float = 1.0f
319     }
320 }
321