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