1 /* <lambda>null2 * Copyright (C) 2024 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.ui.viewmodel 18 19 import android.content.Context 20 import android.util.PluralsMessageFormatter 21 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor 22 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel 23 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor 24 import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor 25 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags 26 import com.android.systemui.bouncer.shared.model.BouncerMessagePair 27 import com.android.systemui.bouncer.shared.model.BouncerMessageStrings 28 import com.android.systemui.bouncer.shared.model.primaryMessage 29 import com.android.systemui.bouncer.shared.model.secondaryMessage 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dagger.qualifiers.Application 32 import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor 33 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor 34 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor 35 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor 36 import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason 37 import com.android.systemui.deviceentry.shared.model.FaceFailureMessage 38 import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage 39 import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage 40 import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage 41 import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage 42 import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown 43 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel 44 import com.android.systemui.user.ui.viewmodel.UserViewModel 45 import com.android.systemui.util.kotlin.Utils.Companion.sample 46 import com.android.systemui.util.time.SystemClock 47 import dagger.Module 48 import dagger.Provides 49 import kotlin.math.ceil 50 import kotlin.math.max 51 import kotlin.time.Duration.Companion.seconds 52 import kotlinx.coroutines.CoroutineScope 53 import kotlinx.coroutines.ExperimentalCoroutinesApi 54 import kotlinx.coroutines.Job 55 import kotlinx.coroutines.delay 56 import kotlinx.coroutines.flow.Flow 57 import kotlinx.coroutines.flow.MutableSharedFlow 58 import kotlinx.coroutines.flow.MutableStateFlow 59 import kotlinx.coroutines.flow.collectLatest 60 import kotlinx.coroutines.flow.combine 61 import kotlinx.coroutines.flow.emptyFlow 62 import kotlinx.coroutines.flow.flatMapLatest 63 import kotlinx.coroutines.flow.map 64 import kotlinx.coroutines.launch 65 66 /** Holds UI state for the 2-line status message shown on the bouncer. */ 67 @OptIn(ExperimentalCoroutinesApi::class) 68 class BouncerMessageViewModel( 69 @Application private val applicationContext: Context, 70 @Application private val applicationScope: CoroutineScope, 71 private val bouncerInteractor: BouncerInteractor, 72 private val simBouncerInteractor: SimBouncerInteractor, 73 private val authenticationInteractor: AuthenticationInteractor, 74 selectedUser: Flow<UserViewModel>, 75 private val clock: SystemClock, 76 private val biometricMessageInteractor: BiometricMessageInteractor, 77 private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, 78 private val deviceEntryInteractor: DeviceEntryInteractor, 79 private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, 80 flags: ComposeBouncerFlags, 81 ) { 82 /** 83 * A message shown when the user has attempted the wrong credential too many times and now must 84 * wait a while before attempting to authenticate again. 85 * 86 * This is updated every second (countdown) during the lockout. When lockout is not active, this 87 * is `null` and no lockout message should be shown. 88 */ 89 private val lockoutMessage: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) 90 91 /** Whether there is a lockout message that is available to be shown in the status message. */ 92 val isLockoutMessagePresent: Flow<Boolean> = lockoutMessage.map { it != null } 93 94 /** The user-facing message to show in the bouncer. */ 95 val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) 96 97 /** Initializes the bouncer message to default whenever it is shown. */ 98 fun onShown() { 99 showDefaultMessage() 100 } 101 102 /** Reset the message shown on the bouncer to the default message. */ 103 fun showDefaultMessage() { 104 resetToDefault.tryEmit(Unit) 105 } 106 107 private val resetToDefault = MutableSharedFlow<Unit>(replay = 1) 108 109 private var lockoutCountdownJob: Job? = null 110 111 private fun defaultBouncerMessageInitializer() { 112 applicationScope.launch { 113 resetToDefault.emit(Unit) 114 authenticationInteractor.authenticationMethod 115 .flatMapLatest { authMethod -> 116 if (authMethod == AuthenticationMethodModel.Sim) { 117 resetToDefault.map { 118 MessageViewModel(simBouncerInteractor.getDefaultMessage()) 119 } 120 } else if (authMethod.isSecure) { 121 combine( 122 deviceEntryInteractor.deviceEntryRestrictionReason, 123 lockoutMessage, 124 fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer, 125 resetToDefault, 126 ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ -> 127 lockoutMsg 128 ?: deviceEntryRestrictedReason.toMessage( 129 authMethod, 130 isFpAllowedInBouncer 131 ) 132 } 133 } else { 134 emptyFlow() 135 } 136 } 137 .collectLatest { messageViewModel -> message.value = messageViewModel } 138 } 139 } 140 141 private fun listenForSimBouncerEvents() { 142 // Listen for any events from the SIM bouncer and update the message shown on the bouncer. 143 applicationScope.launch { 144 authenticationInteractor.authenticationMethod 145 .flatMapLatest { authMethod -> 146 if (authMethod == AuthenticationMethodModel.Sim) { 147 simBouncerInteractor.bouncerMessageChanged.map { simMsg -> 148 simMsg?.let { MessageViewModel(it) } 149 } 150 } else { 151 emptyFlow() 152 } 153 } 154 .collectLatest { 155 if (it != null) { 156 message.value = it 157 } else { 158 resetToDefault.emit(Unit) 159 } 160 } 161 } 162 } 163 164 private fun listenForFaceMessages() { 165 // Listen for any events from face authentication and update the message shown on the 166 // bouncer. 167 applicationScope.launch { 168 biometricMessageInteractor.faceMessage 169 .sample( 170 authenticationInteractor.authenticationMethod, 171 fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer, 172 ) 173 .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) -> 174 val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong() 175 val defaultPrimaryMessage = 176 BouncerMessageStrings.defaultMessage( 177 authMethod, 178 fingerprintAllowedOnBouncer 179 ) 180 .primaryMessage 181 .toResString() 182 message.value = 183 when (faceMessage) { 184 is FaceTimeoutMessage -> 185 MessageViewModel( 186 text = defaultPrimaryMessage, 187 secondaryText = faceMessage.message, 188 isUpdateAnimated = true 189 ) 190 is FaceLockoutMessage -> 191 if (isFaceAuthStrong) 192 BouncerMessageStrings.class3AuthLockedOut(authMethod) 193 .toMessage() 194 else 195 BouncerMessageStrings.faceLockedOut( 196 authMethod, 197 fingerprintAllowedOnBouncer 198 ) 199 .toMessage() 200 is FaceFailureMessage -> 201 BouncerMessageStrings.incorrectFaceInput( 202 authMethod, 203 fingerprintAllowedOnBouncer 204 ) 205 .toMessage() 206 else -> 207 MessageViewModel( 208 text = defaultPrimaryMessage, 209 secondaryText = faceMessage.message, 210 isUpdateAnimated = false 211 ) 212 } 213 delay(MESSAGE_DURATION) 214 resetToDefault.emit(Unit) 215 } 216 } 217 } 218 219 private fun listenForFingerprintMessages() { 220 applicationScope.launch { 221 // Listen for any events from fingerprint authentication and update the message shown 222 // on the bouncer. 223 biometricMessageInteractor.fingerprintMessage 224 .sample( 225 authenticationInteractor.authenticationMethod, 226 fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer 227 ) 228 .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) -> 229 val defaultPrimaryMessage = 230 BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed) 231 .primaryMessage 232 .toResString() 233 message.value = 234 when (fingerprintMessage) { 235 is FingerprintLockoutMessage -> 236 BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage() 237 is FingerprintFailureMessage -> 238 BouncerMessageStrings.incorrectFingerprintInput(authMethod) 239 .toMessage() 240 else -> 241 MessageViewModel( 242 text = defaultPrimaryMessage, 243 secondaryText = fingerprintMessage.message, 244 isUpdateAnimated = false 245 ) 246 } 247 delay(MESSAGE_DURATION) 248 resetToDefault.emit(Unit) 249 } 250 } 251 } 252 253 private fun listenForBouncerEvents() { 254 // Keeps the lockout message up-to-date. 255 applicationScope.launch { 256 bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() } 257 } 258 259 // Listens to relevant bouncer events 260 applicationScope.launch { 261 bouncerInteractor.onIncorrectBouncerInput 262 .sample( 263 authenticationInteractor.authenticationMethod, 264 fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer 265 ) 266 .collectLatest { (_, authMethod, isFingerprintAllowed) -> 267 message.emit( 268 BouncerMessageStrings.incorrectSecurityInput( 269 authMethod, 270 isFingerprintAllowed 271 ) 272 .toMessage() 273 ) 274 delay(MESSAGE_DURATION) 275 resetToDefault.emit(Unit) 276 } 277 } 278 } 279 280 private fun DeviceEntryRestrictionReason?.toMessage( 281 authMethod: AuthenticationMethodModel, 282 isFingerprintAllowedOnBouncer: Boolean, 283 ): MessageViewModel { 284 return when (this) { 285 DeviceEntryRestrictionReason.UserLockdown -> 286 BouncerMessageStrings.authRequiredAfterUserLockdown(authMethod) 287 DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -> 288 BouncerMessageStrings.authRequiredAfterReboot(authMethod) 289 DeviceEntryRestrictionReason.PolicyLockdown -> 290 BouncerMessageStrings.authRequiredAfterAdminLockdown(authMethod) 291 DeviceEntryRestrictionReason.UnattendedUpdate -> 292 BouncerMessageStrings.authRequiredForUnattendedUpdate(authMethod) 293 DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate -> 294 BouncerMessageStrings.authRequiredForMainlineUpdate(authMethod) 295 DeviceEntryRestrictionReason.SecurityTimeout -> 296 BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout(authMethod) 297 DeviceEntryRestrictionReason.StrongBiometricsLockedOut -> 298 BouncerMessageStrings.class3AuthLockedOut(authMethod) 299 DeviceEntryRestrictionReason.NonStrongFaceLockedOut -> 300 BouncerMessageStrings.faceLockedOut(authMethod, isFingerprintAllowedOnBouncer) 301 DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -> 302 BouncerMessageStrings.nonStrongAuthTimeout( 303 authMethod, 304 isFingerprintAllowedOnBouncer 305 ) 306 DeviceEntryRestrictionReason.TrustAgentDisabled -> 307 BouncerMessageStrings.trustAgentDisabled(authMethod, isFingerprintAllowedOnBouncer) 308 DeviceEntryRestrictionReason.AdaptiveAuthRequest -> 309 BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest( 310 authMethod, 311 isFingerprintAllowedOnBouncer 312 ) 313 else -> BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowedOnBouncer) 314 }.toMessage() 315 } 316 317 private fun BouncerMessagePair.toMessage(): MessageViewModel { 318 val primaryMsg = this.primaryMessage.toResString() 319 val secondaryMsg = 320 if (this.secondaryMessage == 0) "" else this.secondaryMessage.toResString() 321 return MessageViewModel(primaryMsg, secondaryText = secondaryMsg, isUpdateAnimated = true) 322 } 323 324 /** Shows the countdown message and refreshes it every second. */ 325 private fun startLockoutCountdown() { 326 lockoutCountdownJob?.cancel() 327 lockoutCountdownJob = 328 applicationScope.launch { 329 authenticationInteractor.authenticationMethod.collectLatest { authMethod -> 330 do { 331 val remainingSeconds = remainingLockoutSeconds() 332 val authLockedOutMsg = 333 BouncerMessageStrings.primaryAuthLockedOut(authMethod) 334 lockoutMessage.value = 335 if (remainingSeconds > 0) { 336 MessageViewModel( 337 text = 338 kg_too_many_failed_attempts_countdown.toPluralString( 339 mutableMapOf<String, Any>( 340 Pair("count", remainingSeconds) 341 ) 342 ), 343 secondaryText = authLockedOutMsg.secondaryMessage.toResString(), 344 isUpdateAnimated = false 345 ) 346 } else { 347 null 348 } 349 delay(1.seconds) 350 } while (remainingSeconds > 0) 351 lockoutCountdownJob = null 352 } 353 } 354 } 355 356 private fun remainingLockoutSeconds(): Int { 357 val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0 358 val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime()) 359 return ceil(remainingMs / 1000f).toInt() 360 } 361 362 private fun Int.toPluralString(formatterArgs: Map<String, Any>): String = 363 PluralsMessageFormatter.format(applicationContext.resources, formatterArgs, this) 364 365 private fun Int.toResString(): String = applicationContext.getString(this) 366 367 init { 368 if (flags.isComposeBouncerOrSceneContainerEnabled()) { 369 applicationScope.launch { 370 // Update the lockout countdown whenever the selected user is switched. 371 selectedUser.collect { startLockoutCountdown() } 372 } 373 374 defaultBouncerMessageInitializer() 375 376 listenForSimBouncerEvents() 377 listenForBouncerEvents() 378 listenForFaceMessages() 379 listenForFingerprintMessages() 380 } 381 } 382 383 companion object { 384 private const val MESSAGE_DURATION = 2000L 385 } 386 } 387 388 /** Data class that represents the status message show on the bouncer. */ 389 data class MessageViewModel( 390 val text: String, 391 val secondaryText: String? = null, 392 /** 393 * Whether updates to the message should be cross-animated from one message to another. 394 * 395 * If `false`, no animation should be applied, the message text should just be replaced 396 * instantly. 397 */ 398 val isUpdateAnimated: Boolean = true, 399 ) 400 401 @OptIn(ExperimentalCoroutinesApi::class) 402 @Module 403 object BouncerMessageViewModelModule { 404 405 @Provides 406 @SysUISingleton viewModelnull407 fun viewModel( 408 @Application applicationContext: Context, 409 @Application applicationScope: CoroutineScope, 410 bouncerInteractor: BouncerInteractor, 411 simBouncerInteractor: SimBouncerInteractor, 412 authenticationInteractor: AuthenticationInteractor, 413 clock: SystemClock, 414 biometricMessageInteractor: BiometricMessageInteractor, 415 faceAuthInteractor: DeviceEntryFaceAuthInteractor, 416 deviceEntryInteractor: DeviceEntryInteractor, 417 fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, 418 flags: ComposeBouncerFlags, 419 userSwitcherViewModel: UserSwitcherViewModel, 420 ): BouncerMessageViewModel { 421 return BouncerMessageViewModel( 422 applicationContext = applicationContext, 423 applicationScope = applicationScope, 424 bouncerInteractor = bouncerInteractor, 425 simBouncerInteractor = simBouncerInteractor, 426 authenticationInteractor = authenticationInteractor, 427 clock = clock, 428 biometricMessageInteractor = biometricMessageInteractor, 429 faceAuthInteractor = faceAuthInteractor, 430 deviceEntryInteractor = deviceEntryInteractor, 431 fingerprintInteractor = fingerprintInteractor, 432 flags = flags, 433 selectedUser = userSwitcherViewModel.selectedUser, 434 ) 435 } 436 } 437