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 androidx.annotation.VisibleForTesting 20 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel 21 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor 22 import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor 23 import com.android.systemui.res.R 24 import com.android.systemui.user.domain.interactor.SelectedUserInteractor 25 import com.android.systemui.util.kotlin.onSubscriberAdded 26 import kotlin.time.Duration.Companion.milliseconds 27 import kotlinx.coroutines.CoroutineScope 28 import kotlinx.coroutines.delay 29 import kotlinx.coroutines.flow.MutableStateFlow 30 import kotlinx.coroutines.flow.SharingStarted 31 import kotlinx.coroutines.flow.StateFlow 32 import kotlinx.coroutines.flow.asStateFlow 33 import kotlinx.coroutines.flow.combine 34 import kotlinx.coroutines.flow.onEach 35 import kotlinx.coroutines.flow.stateIn 36 import kotlinx.coroutines.launch 37 38 /** Holds UI state and handles user input for the password bouncer UI. */ 39 class PasswordBouncerViewModel( 40 viewModelScope: CoroutineScope, 41 isInputEnabled: StateFlow<Boolean>, 42 interactor: BouncerInteractor, 43 private val onIntentionalUserInput: () -> Unit, 44 private val inputMethodInteractor: InputMethodInteractor, 45 private val selectedUserInteractor: SelectedUserInteractor, 46 ) : 47 AuthMethodBouncerViewModel( 48 viewModelScope = viewModelScope, 49 interactor = interactor, 50 isInputEnabled = isInputEnabled, 51 ) { 52 53 private val _password = MutableStateFlow("") 54 55 /** The password entered so far. */ 56 val password: StateFlow<String> = _password.asStateFlow() 57 58 override val authenticationMethod = AuthenticationMethodModel.Password 59 60 override val lockoutMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message 61 62 /** Informs the UI whether the input method switcher button should be visible. */ 63 val isImeSwitcherButtonVisible: StateFlow<Boolean> = imeSwitcherRefreshingFlow() 64 65 /** Whether the text field element currently has focus. */ 66 private val isTextFieldFocused = MutableStateFlow(false) 67 68 /** Whether the UI should request focus on the text field element. */ 69 val isTextFieldFocusRequested = 70 combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> hasInput && !hasFocus } 71 .stateIn( 72 scope = viewModelScope, 73 started = SharingStarted.WhileSubscribed(), 74 initialValue = isInputEnabled.value && !isTextFieldFocused.value, 75 ) 76 77 /** The ID of the currently-selected user. */ 78 val selectedUserId: StateFlow<Int> = 79 selectedUserInteractor.selectedUser.stateIn( 80 scope = viewModelScope, 81 started = SharingStarted.WhileSubscribed(), 82 initialValue = selectedUserInteractor.getSelectedUserId(), 83 ) 84 85 override fun onHidden() { 86 super.onHidden() 87 isTextFieldFocused.value = false 88 } 89 90 override fun clearInput() { 91 _password.value = "" 92 } 93 94 override fun getInput(): List<Any> { 95 return _password.value.toCharArray().toList() 96 } 97 98 /** Notifies that the user has changed the password input. */ 99 fun onPasswordInputChanged(newPassword: String) { 100 if (newPassword.isNotEmpty()) { 101 onIntentionalUserInput() 102 } 103 104 _password.value = newPassword 105 } 106 107 /** Notifies that the user clicked the button to change the input method. */ 108 fun onImeSwitcherButtonClicked(displayId: Int) { 109 viewModelScope.launch { 110 inputMethodInteractor.showInputMethodPicker(displayId, showAuxiliarySubtypes = false) 111 } 112 } 113 114 /** Notifies that the user has pressed the key for attempting to authenticate the password. */ 115 fun onAuthenticateKeyPressed() { 116 if (_password.value.isNotEmpty()) { 117 tryAuthenticate() 118 } 119 } 120 121 /** Notifies that the user has dismissed the software keyboard (IME). */ 122 fun onImeDismissed() { 123 viewModelScope.launch { interactor.onImeHiddenByUser() } 124 } 125 126 /** Notifies that the password text field has gained or lost focus. */ 127 fun onTextFieldFocusChanged(isFocused: Boolean) { 128 isTextFieldFocused.value = isFocused 129 } 130 131 /** 132 * Whether the input method switcher button should be displayed in the password bouncer UI. The 133 * value may be stale at the moment of subscription to this flow, but it is guaranteed to be 134 * shortly updated with a fresh value. 135 * 136 * Note: Each added subscription triggers an IPC call in the background, so this should only be 137 * subscribed to by the UI once in its lifecycle (i.e. when the bouncer is shown). 138 */ 139 private fun imeSwitcherRefreshingFlow(): StateFlow<Boolean> { 140 val isImeSwitcherButtonVisible = MutableStateFlow(value = false) 141 viewModelScope.launch { 142 // Re-fetch the currently-enabled IMEs whenever the selected user changes, and whenever 143 // the UI subscribes to the `isImeSwitcherButtonVisible` flow. 144 combine( 145 // InputMethodManagerService sometimes takes some time to update its internal 146 // state when the selected user changes. As a workaround, delay fetching the IME 147 // info. 148 selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) }, 149 isImeSwitcherButtonVisible.onSubscriberAdded() 150 ) { selectedUserId, _ -> 151 inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId) 152 } 153 .collect { isImeSwitcherButtonVisible.value = it } 154 } 155 return isImeSwitcherButtonVisible.asStateFlow() 156 } 157 158 companion object { 159 @VisibleForTesting val DELAY_TO_FETCH_IMES = 300.milliseconds 160 } 161 } 162