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