• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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