• 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 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