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.deviceentry.domain.interactor 18 19 import com.android.internal.policy.IKeyguardDismissCallback 20 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor 21 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel 22 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor 23 import com.android.systemui.dagger.SysUISingleton 24 import com.android.systemui.dagger.qualifiers.Application 25 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository 26 import com.android.systemui.keyguard.DismissCallbackRegistry 27 import com.android.systemui.log.table.TableLogBuffer 28 import com.android.systemui.log.table.logDiffsForTable 29 import com.android.systemui.scene.data.model.asIterable 30 import com.android.systemui.scene.domain.SceneFrameworkTableLog 31 import com.android.systemui.scene.domain.interactor.SceneBackInteractor 32 import com.android.systemui.scene.domain.interactor.SceneInteractor 33 import com.android.systemui.scene.shared.model.Overlays 34 import com.android.systemui.scene.shared.model.Scenes 35 import com.android.systemui.util.kotlin.pairwise 36 import com.android.systemui.utils.coroutines.flow.mapLatestConflated 37 import javax.inject.Inject 38 import kotlinx.coroutines.CoroutineScope 39 import kotlinx.coroutines.flow.Flow 40 import kotlinx.coroutines.flow.SharingStarted 41 import kotlinx.coroutines.flow.StateFlow 42 import kotlinx.coroutines.flow.combine 43 import kotlinx.coroutines.flow.distinctUntilChanged 44 import kotlinx.coroutines.flow.filter 45 import kotlinx.coroutines.flow.first 46 import kotlinx.coroutines.flow.map 47 import kotlinx.coroutines.flow.onStart 48 import kotlinx.coroutines.flow.stateIn 49 import kotlinx.coroutines.launch 50 51 /** 52 * Hosts application business logic related to device entry. 53 * 54 * Device entry occurs when the user successfully dismisses (or bypasses) the lockscreen, regardless 55 * of the authentication method used. 56 */ 57 @SysUISingleton 58 class DeviceEntryInteractor 59 @Inject 60 constructor( 61 @Application private val applicationScope: CoroutineScope, 62 private val repository: DeviceEntryRepository, 63 private val authenticationInteractor: AuthenticationInteractor, 64 private val sceneInteractor: SceneInteractor, 65 private val deviceUnlockedInteractor: DeviceUnlockedInteractor, 66 private val alternateBouncerInteractor: AlternateBouncerInteractor, 67 private val dismissCallbackRegistry: DismissCallbackRegistry, 68 sceneBackInteractor: SceneBackInteractor, 69 @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer, 70 ) { 71 /** 72 * Whether the device is unlocked. 73 * 74 * A device that is not yet unlocked requires unlocking by completing an authentication 75 * challenge according to the current authentication method, unless in cases when the current 76 * authentication method is not "secure" (for example, None and Swipe); in such cases, the value 77 * of this flow will always be `true`, even if the lockscreen is showing and still needs to be 78 * dismissed by the user to proceed. 79 */ 80 val isUnlocked: StateFlow<Boolean> = 81 deviceUnlockedInteractor.deviceUnlockStatus 82 .map { it.isUnlocked } 83 .stateIn( 84 scope = applicationScope, 85 started = SharingStarted.WhileSubscribed(), 86 initialValue = deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked, 87 ) 88 89 /** 90 * Emits `true` when the current scene switches to [Scenes.Gone] for the first time after having 91 * been on [Scenes.Lockscreen]. 92 * 93 * Different from [isDeviceEntered] such that the current scene must actually go through 94 * [Scenes.Gone] to produce a `true`. [isDeviceEntered] also takes into account the navigation 95 * back stack and will produce a `true` value even when the current scene is still not 96 * [Scenes.Gone] but the bottommost entry of the navigation back stack switched from 97 * [Scenes.Lockscreen] to [Scenes.Gone] while the user is staring at another scene. 98 */ 99 val isDeviceEnteredDirectly: StateFlow<Boolean> = 100 sceneInteractor.currentScene 101 .filter { currentScene -> 102 currentScene == Scenes.Gone || currentScene == Scenes.Lockscreen 103 } 104 .mapLatestConflated { scene -> 105 if (scene == Scenes.Gone) { 106 // Make sure device unlock status is definitely unlocked before we 107 // consider the device "entered". 108 deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked } 109 true 110 } else { 111 false 112 } 113 } 114 .stateIn( 115 scope = applicationScope, 116 started = SharingStarted.Eagerly, 117 initialValue = false, 118 ) 119 120 /** 121 * Whether the device has been entered (i.e. the lockscreen has been dismissed, by any method). 122 * This can be `false` when the device is unlocked, e.g. when the user still needs to swipe away 123 * the non-secure lockscreen, even though they've already authenticated. 124 * 125 * Note: This does not imply that the lockscreen is visible or not. 126 * 127 * Different from [isDeviceEnteredDirectly] such that the current scene doesn't actually have to 128 * go through [Scenes.Gone] to produce a `true`. [isDeviceEnteredDirectly] doesn't take the 129 * navigation back stack into account and will only produce a `true` value even when the current 130 * scene is actually [Scenes.Gone]. 131 */ 132 val isDeviceEntered: StateFlow<Boolean> = 133 combine( 134 // This flow emits true when the currentScene switches to Gone for the first time 135 // after having been on Lockscreen. 136 isDeviceEnteredDirectly, 137 // This flow emits true only if the bottom of the navigation back stack has been 138 // switched from Lockscreen to Gone. In other words, only if the device was unlocked 139 // while visiting at least one scene "above" the Lockscreen scene. 140 sceneBackInteractor.backStack 141 // The bottom of the back stack, which is Lockscreen, Gone, or null if empty. 142 .map { it.asIterable().lastOrNull() } 143 // Filter out cases where the stack changes but the bottom remains unchanged. 144 .distinctUntilChanged() 145 // Detect changes of the bottom of the stack, start with null, so the first 146 // update emits a value and the logic doesn't need to wait for a second value 147 // before emitting something. 148 .pairwise(initialValue = null) 149 // Replacing a bottom of the stack that was Lockscreen with Gone constitutes a 150 // "device entered" event. 151 .map { (from, to) -> from == Scenes.Lockscreen && to == Scenes.Gone }, 152 ) { enteredDirectly, enteredOnBackStack -> 153 enteredOnBackStack || enteredDirectly 154 } 155 .logDiffsForTable( 156 tableLogBuffer = tableLogBuffer, 157 columnName = "isDeviceEntered", 158 initialValue = false, 159 ) 160 .stateIn( 161 scope = applicationScope, 162 started = SharingStarted.Eagerly, 163 initialValue = false, 164 ) 165 166 val isLockscreenEnabled: Flow<Boolean> by lazy { 167 repository.isLockscreenEnabled.onStart { refreshLockscreenEnabled() } 168 } 169 170 /** 171 * Whether it's currently possible to swipe up to enter the device without requiring 172 * authentication or when the device is already authenticated using a passive authentication 173 * mechanism like face or trust manager. This returns `false` whenever the lockscreen has been 174 * dismissed. 175 * 176 * A value of `null` is meaningless and is used as placeholder while the actual value is still 177 * being loaded in the background. 178 * 179 * Note: `true` doesn't mean the lockscreen is visible. It may be occluded or covered by other 180 * UI. 181 */ 182 val canSwipeToEnter: StateFlow<Boolean?> by lazy { 183 combine( 184 authenticationInteractor.authenticationMethod.map { 185 it == AuthenticationMethodModel.None 186 }, 187 isLockscreenEnabled, 188 deviceUnlockedInteractor.deviceUnlockStatus, 189 isDeviceEntered, 190 ) { isNoneAuthMethod, isLockscreenEnabled, deviceUnlockStatus, isDeviceEntered -> 191 val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled 192 (isSwipeAuthMethod || 193 (deviceUnlockStatus.isUnlocked && 194 deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == false)) && 195 !isDeviceEntered 196 } 197 .logDiffsForTable( 198 tableLogBuffer = tableLogBuffer, 199 columnName = "canSwipeToEnter", 200 initialValue = false, 201 ) 202 .stateIn( 203 scope = applicationScope, 204 started = SharingStarted.Eagerly, 205 // Starts as null to prevent downstream collectors from falsely assuming that the 206 // user can or cannot swipe to enter the device while the real value is being loaded 207 // from upstream data sources. 208 initialValue = null, 209 ) 210 } 211 212 /** 213 * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically 214 * dismissed once the authentication challenge is completed. For example, completing a biometric 215 * authentication challenge via face unlock or fingerprint sensor can automatically bypass the 216 * lockscreen. 217 */ 218 val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled 219 220 /** 221 * Attempt to enter the device and dismiss the lockscreen. If authentication is required to 222 * unlock the device it will transition to bouncer. 223 * 224 * @param callback An optional callback to invoke when the attempt succeeds, fails, or is 225 * canceled 226 */ 227 @JvmOverloads 228 fun attemptDeviceEntry(callback: IKeyguardDismissCallback? = null) { 229 callback?.let { dismissCallbackRegistry.addCallback(it) } 230 231 // TODO (b/307768356), 232 // 1. Check if the device is already authenticated by trust agent/passive biometrics 233 // 2. Show SPFS/UDFPS bouncer if it is available AlternateBouncerInteractor.show 234 // 3. For face auth only setups trigger face auth, delay transitioning to bouncer for 235 // a small amount of time. 236 // 4. Transition to bouncer scene 237 applicationScope.launch { 238 if (isAuthenticationRequired()) { 239 if (alternateBouncerInteractor.canShowAlternateBouncer.value) { 240 alternateBouncerInteractor.forceShow() 241 } else { 242 sceneInteractor.showOverlay( 243 overlay = Overlays.Bouncer, 244 loggingReason = "request to unlock device while authentication required", 245 ) 246 } 247 } else { 248 sceneInteractor.changeScene( 249 toScene = Scenes.Gone, 250 loggingReason = "request to unlock device while authentication isn't required", 251 ) 252 } 253 } 254 } 255 256 /** 257 * Returns `true` if the device currently requires authentication before entry is granted; 258 * `false` if the device can be entered without authenticating first. 259 */ 260 suspend fun isAuthenticationRequired(): Boolean { 261 return !deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked && 262 authenticationInteractor.getAuthenticationMethod().isSecure 263 } 264 265 /** 266 * Whether the lockscreen is enabled for the current user. This is `true` whenever the user has 267 * chosen any secure authentication method and even if they set the lockscreen to be dismissed 268 * when the user swipes on it. 269 */ 270 suspend fun isLockscreenEnabled(): Boolean { 271 return repository.isLockscreenEnabled() 272 } 273 274 /** 275 * Forces a refresh of the value of [isLockscreenEnabled] such that the flow emits the latest 276 * value. 277 * 278 * Without calling this method, the flow will have a stale value unless the collector is removed 279 * and re-added. 280 */ 281 suspend fun refreshLockscreenEnabled() { 282 isLockscreenEnabled() 283 } 284 285 /** Locks the device instantly. */ 286 fun lockNow(debuggingReason: String) { 287 deviceUnlockedInteractor.lockNow(debuggingReason) 288 } 289 } 290