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.view.KeyEvent 20 import androidx.annotation.VisibleForTesting 21 import androidx.compose.ui.input.key.KeyEventType 22 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel 23 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor 24 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags 25 import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor 26 import com.android.systemui.res.R 27 import com.android.systemui.user.domain.interactor.SelectedUserInteractor 28 import com.android.systemui.util.kotlin.onSubscriberAdded 29 import dagger.assisted.Assisted 30 import dagger.assisted.AssistedFactory 31 import dagger.assisted.AssistedInject 32 import kotlin.time.Duration.Companion.milliseconds 33 import kotlinx.coroutines.awaitCancellation 34 import kotlinx.coroutines.channels.Channel 35 import kotlinx.coroutines.coroutineScope 36 import kotlinx.coroutines.delay 37 import kotlinx.coroutines.flow.MutableStateFlow 38 import kotlinx.coroutines.flow.StateFlow 39 import kotlinx.coroutines.flow.asStateFlow 40 import kotlinx.coroutines.flow.combine 41 import kotlinx.coroutines.flow.onEach 42 import kotlinx.coroutines.flow.receiveAsFlow 43 import com.android.app.tracing.coroutines.launchTraced as launch 44 45 /** Holds UI state and handles user input for the password bouncer UI. */ 46 class PasswordBouncerViewModel 47 @AssistedInject 48 constructor( 49 interactor: BouncerInteractor, 50 private val inputMethodInteractor: InputMethodInteractor, 51 private val selectedUserInteractor: SelectedUserInteractor, 52 @Assisted isInputEnabled: StateFlow<Boolean>, 53 @Assisted private val onIntentionalUserInput: () -> Unit, 54 ) : 55 AuthMethodBouncerViewModel( 56 interactor = interactor, 57 isInputEnabled = isInputEnabled, 58 traceName = "PasswordBouncerViewModel", 59 ) { 60 61 private val _password = MutableStateFlow("") 62 63 /** The password entered so far. */ 64 val password: StateFlow<String> = _password.asStateFlow() 65 66 override val authenticationMethod = AuthenticationMethodModel.Password 67 68 override val lockoutMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message 69 70 private val _isImeSwitcherButtonVisible = MutableStateFlow(false) 71 /** Informs the UI whether the input method switcher button should be visible. */ 72 val isImeSwitcherButtonVisible: StateFlow<Boolean> = _isImeSwitcherButtonVisible.asStateFlow() 73 74 /** Whether the text field element currently has focus. */ 75 private val isTextFieldFocused = MutableStateFlow(false) 76 77 private val _isTextFieldFocusRequested = 78 MutableStateFlow(isInputEnabled.value && !isTextFieldFocused.value) 79 /** Whether the UI should request focus on the text field element. */ 80 val isTextFieldFocusRequested = _isTextFieldFocusRequested.asStateFlow() 81 82 private val _selectedUserId = MutableStateFlow(selectedUserInteractor.getSelectedUserId()) 83 /** The ID of the currently-selected user. */ 84 val selectedUserId: StateFlow<Int> = _selectedUserId.asStateFlow() 85 86 private val requests = Channel<Request>(Channel.BUFFERED) 87 private var wasSuccessfullyAuthenticated = false 88 89 override suspend fun onActivated(): Nothing { 90 try { 91 coroutineScope { 92 launch { super.onActivated() } 93 launch { 94 requests.receiveAsFlow().collect { request -> 95 when (request) { 96 is OnImeSwitcherButtonClicked -> { 97 inputMethodInteractor.showInputMethodPicker( 98 displayId = request.displayId, 99 showAuxiliarySubtypes = false, 100 ) 101 } 102 is OnImeDismissed -> { 103 interactor.onImeHiddenByUser() 104 } 105 } 106 } 107 } 108 launch { 109 combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> 110 hasInput && !hasFocus && !wasSuccessfullyAuthenticated 111 } 112 .collect { _isTextFieldFocusRequested.value = it } 113 } 114 launch { 115 selectedUserInteractor.selectedUser.collect { _selectedUserId.value = it } 116 } 117 launch { 118 // Re-fetch the currently-enabled IMEs whenever the selected user changes, and 119 // whenever 120 // the UI subscribes to the `isImeSwitcherButtonVisible` flow. 121 combine( 122 // InputMethodManagerService sometimes takes 123 // some time to update its internal state when the 124 // selected user changes. 125 // As a workaround, delay fetching the IME info. 126 selectedUserInteractor.selectedUser.onEach { 127 delay(DELAY_TO_FETCH_IMES) 128 }, 129 _isImeSwitcherButtonVisible.onSubscriberAdded(), 130 ) { selectedUserId, _ -> 131 inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId) 132 } 133 .collect { _isImeSwitcherButtonVisible.value = it } 134 } 135 awaitCancellation() 136 } 137 } finally { 138 // reset whenever the view model is "deactivated" 139 wasSuccessfullyAuthenticated = false 140 } 141 } 142 143 override fun onHidden() { 144 super.onHidden() 145 isTextFieldFocused.value = false 146 } 147 148 override fun clearInput() { 149 _password.value = "" 150 } 151 152 override fun getInput(): List<Any> { 153 return _password.value.toCharArray().toList() 154 } 155 156 override fun onKeyEvent(type: KeyEventType, keyCode: Int): Boolean { 157 // Ignore SPACE as a confirm key to allow the space character within passwords. 158 val isKeyboardEnterKey = 159 KeyEvent.isConfirmKey(keyCode) && 160 keyCode != KeyEvent.KEYCODE_SPACE && 161 type == KeyEventType.KeyUp 162 // consume confirm key events while on the bouncer. This prevents it from propagating 163 // and avoids other parent elements from receiving it. 164 return isKeyboardEnterKey && ComposeBouncerFlags.isOnlyComposeBouncerEnabled() 165 } 166 167 override fun onSuccessfulAuthentication() { 168 wasSuccessfullyAuthenticated = true 169 } 170 171 /** Notifies that the user has changed the password input. */ 172 fun onPasswordInputChanged(newPassword: String) { 173 if (newPassword.isNotEmpty()) { 174 onIntentionalUserInput() 175 } 176 177 _password.value = newPassword 178 } 179 180 /** Notifies that the user clicked the button to change the input method. */ 181 fun onImeSwitcherButtonClicked(displayId: Int) { 182 requests.trySend(OnImeSwitcherButtonClicked(displayId)) 183 } 184 185 /** Notifies that the user has pressed the key for attempting to authenticate the password. */ 186 fun onAuthenticateKeyPressed() { 187 if (_password.value.isNotEmpty()) { 188 tryAuthenticate() 189 } 190 } 191 192 /** Notifies that the user has dismissed the software keyboard (IME). */ 193 fun onImeDismissed() { 194 requests.trySend(OnImeDismissed) 195 } 196 197 /** Notifies that the password text field has gained or lost focus. */ 198 fun onTextFieldFocusChanged(isFocused: Boolean) { 199 isTextFieldFocused.value = isFocused 200 } 201 202 @AssistedFactory 203 interface Factory { 204 fun create( 205 isInputEnabled: StateFlow<Boolean>, 206 onIntentionalUserInput: () -> Unit, 207 ): PasswordBouncerViewModel 208 } 209 210 companion object { 211 @VisibleForTesting val DELAY_TO_FETCH_IMES = 300.milliseconds 212 } 213 214 private sealed interface Request 215 216 private data class OnImeSwitcherButtonClicked(val displayId: Int) : Request 217 218 private data object OnImeDismissed : Request 219 } 220