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.ui.viewmodel 18 19 import android.annotation.StringRes 20 import androidx.compose.ui.input.key.KeyEventType 21 import com.android.systemui.authentication.domain.interactor.AuthenticationResult 22 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel 23 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor 24 import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer 25 import com.android.systemui.lifecycle.ExclusiveActivatable 26 import kotlinx.coroutines.awaitCancellation 27 import kotlinx.coroutines.channels.Channel 28 import kotlinx.coroutines.flow.MutableStateFlow 29 import kotlinx.coroutines.flow.StateFlow 30 import kotlinx.coroutines.flow.asStateFlow 31 import kotlinx.coroutines.flow.collectLatest 32 import kotlinx.coroutines.flow.receiveAsFlow 33 34 sealed class AuthMethodBouncerViewModel( 35 protected val interactor: BouncerInteractor, 36 37 /** 38 * Whether user input is enabled. 39 * 40 * If `false`, user input should be completely ignored in the UI as the user is "locked out" of 41 * being able to attempt to unlock the device. 42 */ 43 val isInputEnabled: StateFlow<Boolean>, 44 45 /** Name to use for performance tracing purposes. */ 46 val traceName: String, 47 protected val bouncerHapticPlayer: BouncerHapticPlayer? = null, 48 ) : ExclusiveActivatable() { 49 50 private val _animateFailure = MutableStateFlow(false) 51 /** 52 * Whether a failure animation should be shown. Once consumed, the UI must call 53 * [onFailureAnimationShown] to consume this state. 54 */ 55 val animateFailure: StateFlow<Boolean> = _animateFailure.asStateFlow() 56 57 /** The authentication method that corresponds to this view model. */ 58 abstract val authenticationMethod: AuthenticationMethodModel 59 60 /** 61 * String resource ID of the failure message to be shown during lockout. 62 * 63 * The message must include 2 number parameters: the first one indicating how many unsuccessful 64 * attempts were made, and the second one indicating in how many seconds lockout will expire. 65 */ 66 @get:StringRes abstract val lockoutMessageId: Int 67 68 private val authenticationRequests = Channel<AuthenticationRequest>(Channel.BUFFERED) 69 70 override suspend fun onActivated(): Nothing { 71 authenticationRequests.receiveAsFlow().collectLatest { request -> 72 if (!isInputEnabled.value) { 73 return@collectLatest 74 } 75 76 val authenticationResult = 77 interactor.authenticate( 78 input = request.input, 79 tryAutoConfirm = request.useAutoConfirm, 80 ) 81 82 if (authenticationResult == AuthenticationResult.SKIPPED && request.useAutoConfirm) { 83 return@collectLatest 84 } 85 86 performAuthenticationHapticFeedback(authenticationResult) 87 88 _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED 89 clearInput() 90 if (authenticationResult == AuthenticationResult.SUCCEEDED) { 91 onSuccessfulAuthentication() 92 } 93 } 94 awaitCancellation() 95 } 96 97 /** 98 * Notifies that the UI has been hidden from the user (after any transitions have completed). 99 */ 100 open fun onHidden() { 101 clearInput() 102 } 103 104 /** Notifies that the user has placed down a pointer. */ 105 fun onDown() { 106 interactor.onDown() 107 } 108 109 /** 110 * Notifies that the failure animation has been shown. This should be called to consume a `true` 111 * value in [animateFailure]. 112 */ 113 fun onFailureAnimationShown() { 114 _animateFailure.value = false 115 } 116 117 /** Clears any previously-entered input. */ 118 protected abstract fun clearInput() 119 120 /** Returns the input entered so far. */ 121 protected abstract fun getInput(): List<Any> 122 123 /** Invoked after a successful authentication. */ 124 protected open fun onSuccessfulAuthentication() = Unit 125 126 /** 127 * Invoked for any key events on the bouncer. 128 * 129 * @return whether the event was consumed by this method and should not be propagated further. 130 */ 131 open fun onKeyEvent(type: KeyEventType, keyCode: Int): Boolean = false 132 133 /** Perform authentication result haptics */ 134 private fun performAuthenticationHapticFeedback(result: AuthenticationResult) { 135 if (result == AuthenticationResult.SKIPPED) return 136 137 bouncerHapticPlayer?.playAuthenticationFeedback( 138 authenticationSucceeded = result == AuthenticationResult.SUCCEEDED 139 ) 140 } 141 142 /** 143 * Attempts to authenticate the user using the current input value. 144 * 145 * @see BouncerInteractor.authenticate 146 */ 147 protected fun tryAuthenticate(input: List<Any> = getInput(), useAutoConfirm: Boolean = false) { 148 authenticationRequests.trySend(AuthenticationRequest(input, useAutoConfirm)) 149 } 150 151 private data class AuthenticationRequest(val input: List<Any>, val useAutoConfirm: Boolean) 152 } 153