1 /* 2 * 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.content.Context 20 import com.android.systemui.R 21 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor 22 import com.android.systemui.authentication.domain.model.AuthenticationMethodModel 23 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel 24 import com.android.systemui.bouncer.data.repository.BouncerRepository 25 import com.android.systemui.dagger.SysUISingleton 26 import com.android.systemui.dagger.qualifiers.Application 27 import com.android.systemui.flags.FeatureFlags 28 import com.android.systemui.flags.Flags 29 import com.android.systemui.scene.domain.interactor.SceneInteractor 30 import com.android.systemui.scene.shared.model.SceneKey 31 import com.android.systemui.scene.shared.model.SceneModel 32 import com.android.systemui.util.kotlin.pairwise 33 import javax.inject.Inject 34 import kotlin.time.Duration.Companion.milliseconds 35 import kotlinx.coroutines.CoroutineScope 36 import kotlinx.coroutines.flow.SharingStarted 37 import kotlinx.coroutines.flow.StateFlow 38 import kotlinx.coroutines.flow.combine 39 import kotlinx.coroutines.flow.stateIn 40 import kotlinx.coroutines.launch 41 42 /** Encapsulates business logic and application state accessing use-cases. */ 43 @SysUISingleton 44 class BouncerInteractor 45 @Inject 46 constructor( 47 @Application private val applicationScope: CoroutineScope, 48 @Application private val applicationContext: Context, 49 private val repository: BouncerRepository, 50 private val authenticationInteractor: AuthenticationInteractor, 51 private val sceneInteractor: SceneInteractor, 52 featureFlags: FeatureFlags, 53 ) { 54 55 /** The user-facing message to show in the bouncer. */ 56 val message: StateFlow<String?> = 57 combine( 58 repository.message, 59 authenticationInteractor.isThrottled, 60 authenticationInteractor.throttling, 61 ) { message, isThrottled, throttling -> 62 messageOrThrottlingMessage(message, isThrottled, throttling) 63 } 64 .stateIn( 65 scope = applicationScope, 66 started = SharingStarted.WhileSubscribed(), 67 initialValue = 68 messageOrThrottlingMessage( 69 repository.message.value, 70 authenticationInteractor.isThrottled.value, 71 authenticationInteractor.throttling.value, 72 ) 73 ) 74 75 /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */ 76 val throttling: StateFlow<AuthenticationThrottlingModel> = authenticationInteractor.throttling 77 78 /** 79 * Whether currently throttled and the user has to wait before being able to try another 80 * authentication attempt. 81 */ 82 val isThrottled: StateFlow<Boolean> = authenticationInteractor.isThrottled 83 84 /** Whether the auto confirm feature is enabled for the currently-selected user. */ 85 val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled 86 87 /** The length of the hinted PIN, or `null`, if pin length hint should not be shown. */ 88 val hintedPinLength: StateFlow<Int?> = authenticationInteractor.hintedPinLength 89 90 /** Whether the pattern should be visible for the currently-selected user. */ 91 val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible 92 93 init { 94 if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) { 95 // Clear the message if moved from throttling to no-longer throttling. 96 applicationScope.launch { 97 isThrottled.pairwise().collect { (wasThrottled, currentlyThrottled) -> 98 if (wasThrottled && !currentlyThrottled) { 99 clearMessage() 100 } 101 } 102 } 103 } 104 } 105 106 /** 107 * Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown. 108 * 109 * @param message An optional message to show to the user in the bouncer. 110 */ 111 fun showOrUnlockDevice( 112 message: String? = null, 113 ) { 114 applicationScope.launch { 115 if (authenticationInteractor.isAuthenticationRequired()) { 116 repository.setMessage( 117 message ?: promptMessage(authenticationInteractor.getAuthenticationMethod()) 118 ) 119 sceneInteractor.changeScene( 120 scene = SceneModel(SceneKey.Bouncer), 121 loggingReason = "request to unlock device while authentication required", 122 ) 123 } else { 124 sceneInteractor.changeScene( 125 scene = SceneModel(SceneKey.Gone), 126 loggingReason = "request to unlock device while authentication isn't required", 127 ) 128 } 129 } 130 } 131 132 /** 133 * Resets the user-facing message back to the default according to the current authentication 134 * method. 135 */ 136 fun resetMessage() { 137 applicationScope.launch { 138 repository.setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod())) 139 } 140 } 141 142 /** Removes the user-facing message. */ 143 fun clearMessage() { 144 repository.setMessage(null) 145 } 146 147 /** 148 * Attempts to authenticate based on the given user input. 149 * 150 * If the input is correct, the device will be unlocked and the lock screen and bouncer will be 151 * dismissed and hidden. 152 * 153 * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method 154 * supports auto-confirming, and the input's length is at least the code's length. Otherwise, 155 * `null` is returned. 156 * 157 * @param input The input from the user to try to authenticate with. This can be a list of 158 * different things, based on the current authentication method. 159 * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit 160 * request to validate. 161 * @return `true` if the authentication succeeded and the device is now unlocked; `false` when 162 * authentication failed, `null` if the check was not performed. 163 */ 164 suspend fun authenticate( 165 input: List<Any>, 166 tryAutoConfirm: Boolean = false, 167 ): Boolean? { 168 val isAuthenticated = 169 authenticationInteractor.authenticate(input, tryAutoConfirm) ?: return null 170 171 if (isAuthenticated) { 172 sceneInteractor.changeScene( 173 scene = SceneModel(SceneKey.Gone), 174 loggingReason = "successful authentication", 175 ) 176 } else { 177 repository.setMessage(errorMessage(authenticationInteractor.getAuthenticationMethod())) 178 } 179 180 return isAuthenticated 181 } 182 183 private fun promptMessage(authMethod: AuthenticationMethodModel): String { 184 return when (authMethod) { 185 is AuthenticationMethodModel.Pin -> 186 applicationContext.getString(R.string.keyguard_enter_your_pin) 187 is AuthenticationMethodModel.Password -> 188 applicationContext.getString(R.string.keyguard_enter_your_password) 189 is AuthenticationMethodModel.Pattern -> 190 applicationContext.getString(R.string.keyguard_enter_your_pattern) 191 else -> "" 192 } 193 } 194 195 private fun errorMessage(authMethod: AuthenticationMethodModel): String { 196 return when (authMethod) { 197 is AuthenticationMethodModel.Pin -> applicationContext.getString(R.string.kg_wrong_pin) 198 is AuthenticationMethodModel.Password -> 199 applicationContext.getString(R.string.kg_wrong_password) 200 is AuthenticationMethodModel.Pattern -> 201 applicationContext.getString(R.string.kg_wrong_pattern) 202 else -> "" 203 } 204 } 205 206 private fun messageOrThrottlingMessage( 207 message: String?, 208 isThrottled: Boolean, 209 throttlingModel: AuthenticationThrottlingModel, 210 ): String { 211 return when { 212 isThrottled -> 213 applicationContext.getString( 214 com.android.internal.R.string.lockscreen_too_many_failed_attempts_countdown, 215 throttlingModel.remainingMs.milliseconds.inWholeSeconds, 216 ) 217 message != null -> message 218 else -> "" 219 } 220 } 221 } 222