• 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.content.Context
20 import android.view.HapticFeedbackConstants
21 import android.view.KeyEvent.KEYCODE_0
22 import android.view.KeyEvent.KEYCODE_9
23 import android.view.KeyEvent.KEYCODE_DEL
24 import android.view.KeyEvent.KEYCODE_NUMPAD_0
25 import android.view.KeyEvent.KEYCODE_NUMPAD_9
26 import android.view.KeyEvent.isConfirmKey
27 import android.view.View
28 import androidx.compose.ui.input.key.KeyEvent
29 import androidx.compose.ui.input.key.KeyEventType
30 import com.android.app.tracing.coroutines.launchTraced as launch
31 import com.android.keyguard.PinShapeAdapter
32 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
33 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
34 import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
35 import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
36 import com.android.systemui.res.R
37 import dagger.assisted.Assisted
38 import dagger.assisted.AssistedFactory
39 import dagger.assisted.AssistedInject
40 import kotlinx.coroutines.awaitCancellation
41 import kotlinx.coroutines.channels.Channel
42 import kotlinx.coroutines.coroutineScope
43 import kotlinx.coroutines.flow.MutableStateFlow
44 import kotlinx.coroutines.flow.StateFlow
45 import kotlinx.coroutines.flow.asStateFlow
46 import kotlinx.coroutines.flow.combine
47 import kotlinx.coroutines.flow.flowOf
48 import kotlinx.coroutines.flow.map
49 import kotlinx.coroutines.flow.receiveAsFlow
50 
51 /** Holds UI state and handles user input for the PIN code bouncer UI. */
52 class PinBouncerViewModel
53 @AssistedInject
54 constructor(
55     applicationContext: Context,
56     interactor: BouncerInteractor,
57     private val simBouncerInteractor: SimBouncerInteractor,
58     @Assisted bouncerHapticPlayer: BouncerHapticPlayer,
59     @Assisted isInputEnabled: StateFlow<Boolean>,
60     @Assisted private val onIntentionalUserInput: () -> Unit,
61     @Assisted override val authenticationMethod: AuthenticationMethodModel,
62 ) :
63     AuthMethodBouncerViewModel(
64         interactor = interactor,
65         isInputEnabled = isInputEnabled,
66         traceName = "PinBouncerViewModel",
67         bouncerHapticPlayer = bouncerHapticPlayer,
68     ) {
69     /**
70      * Whether the sim-related UI in the pin view is showing.
71      *
72      * This UI is used to unlock a locked sim.
73      */
74     val isSimAreaVisible = authenticationMethod == AuthenticationMethodModel.Sim
75     val isLockedEsim: StateFlow<Boolean?> = simBouncerInteractor.isLockedEsim
76     val errorDialogMessage: StateFlow<String?> = simBouncerInteractor.errorDialogMessage
77     val isSimUnlockingDialogVisible: MutableStateFlow<Boolean> = MutableStateFlow(false)
78     val pinShapes = PinShapeAdapter(applicationContext)
79     private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty())
80 
81     /** Currently entered pin keys. */
82     val pinInput: StateFlow<PinInputViewModel> = mutablePinInput
83 
84     private val _hintedPinLength = MutableStateFlow<Int?>(null)
85     /** The length of the PIN for which we should show a hint. */
86     val hintedPinLength: StateFlow<Int?> = _hintedPinLength.asStateFlow()
87 
88     private val _backspaceButtonAppearance = MutableStateFlow(ActionButtonAppearance.Hidden)
89     /** Appearance of the backspace button. */
90     val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
91         _backspaceButtonAppearance.asStateFlow()
92 
93     private val _confirmButtonAppearance = MutableStateFlow(ActionButtonAppearance.Hidden)
94     /** Appearance of the confirm button. */
95     val confirmButtonAppearance: StateFlow<ActionButtonAppearance> =
96         _confirmButtonAppearance.asStateFlow()
97 
98     override val lockoutMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
99 
100     private val requests = Channel<Request>(Channel.BUFFERED)
101 
102     override suspend fun onActivated(): Nothing {
103         coroutineScope {
104             launch { super.onActivated() }
105             launch {
106                 requests.receiveAsFlow().collect { request ->
107                     when (request) {
108                         is OnErrorDialogDismissed -> {
109                             simBouncerInteractor.onErrorDialogDismissed()
110                         }
111                         is OnAuthenticateButtonClickedForSim -> {
112                             isSimUnlockingDialogVisible.value = true
113                             simBouncerInteractor.verifySim(getInput())
114                             isSimUnlockingDialogVisible.value = false
115                             clearInput()
116                         }
117                     }
118                 }
119             }
120             launch { simBouncerInteractor.subId.collect { onResetSimFlow() } }
121             launch {
122                 if (isSimAreaVisible) {
123                         flowOf(null)
124                     } else {
125                         interactor.hintedPinLength
126                     }
127                     .collect { _hintedPinLength.value = it }
128             }
129             launch {
130                 combine(mutablePinInput, interactor.isAutoConfirmEnabled) {
131                         mutablePinEntries,
132                         isAutoConfirmEnabled ->
133                         computeBackspaceButtonAppearance(
134                             pinInput = mutablePinEntries,
135                             isAutoConfirmEnabled = isAutoConfirmEnabled,
136                         )
137                     }
138                     .collect { _backspaceButtonAppearance.value = it }
139             }
140             launch {
141                 interactor.isAutoConfirmEnabled
142                     .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown }
143                     .collect { _confirmButtonAppearance.value = it }
144             }
145             launch {
146                 interactor.isPinEnhancedPrivacyEnabled
147                     .map { !it }
148                     .collect { _isDigitButtonAnimationEnabled.value = it }
149             }
150             awaitCancellation()
151         }
152     }
153 
154     /** Notifies that the user dismissed the sim pin error dialog. */
155     fun onErrorDialogDismissed() {
156         requests.trySend(OnErrorDialogDismissed)
157     }
158 
159     private val _isDigitButtonAnimationEnabled =
160         MutableStateFlow(!interactor.isPinEnhancedPrivacyEnabled.value)
161     /**
162      * Whether the digit buttons should be animated when touched. Note that this doesn't affect the
163      * delete or enter buttons; those should always animate.
164      */
165     val isDigitButtonAnimationEnabled: StateFlow<Boolean> =
166         _isDigitButtonAnimationEnabled.asStateFlow()
167 
168     /** Notifies that the user clicked on a PIN button with the given digit value. */
169     fun onPinButtonClicked(input: Int) {
170         val pinInput = mutablePinInput.value
171 
172         onIntentionalUserInput()
173 
174         val maxInputLength = hintedPinLength.value ?: Int.MAX_VALUE
175         if (pinInput.getPin().size < maxInputLength) {
176             mutablePinInput.value = pinInput.append(input)
177             tryAuthenticate(useAutoConfirm = true)
178         }
179     }
180 
181     /** Notifies that the user clicked the backspace button. */
182     fun onBackspaceButtonClicked() {
183         mutablePinInput.value = mutablePinInput.value.deleteLast()
184     }
185 
186     fun onBackspaceButtonPressed(view: View?) {
187         if (bouncerHapticPlayer?.isEnabled == true) {
188             bouncerHapticPlayer.playDeleteKeyPressFeedback()
189         } else {
190             view?.performHapticFeedback(
191                 HapticFeedbackConstants.VIRTUAL_KEY,
192                 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
193             )
194         }
195     }
196 
197     /** Notifies that the user long-pressed the backspace button. */
198     fun onBackspaceButtonLongPressed() {
199         if (bouncerHapticPlayer?.isEnabled == true) {
200             bouncerHapticPlayer.playDeleteKeyLongPressedFeedback()
201         }
202         clearInput()
203     }
204 
205     /** Notifies that the user clicked the "enter" button. */
206     fun onAuthenticateButtonClicked() {
207         if (authenticationMethod == AuthenticationMethodModel.Sim) {
208             requests.trySend(OnAuthenticateButtonClickedForSim)
209         } else {
210             tryAuthenticate(useAutoConfirm = false)
211         }
212     }
213 
214     fun onDisableEsimButtonClicked() {
215         simBouncerInteractor.disableEsim()
216     }
217 
218     /** Resets the sim screen and shows a default message. */
219     private fun onResetSimFlow() {
220         simBouncerInteractor.resetSimPukUserInput()
221         clearInput()
222     }
223 
224     override fun clearInput() {
225         mutablePinInput.value = mutablePinInput.value.clearAll()
226     }
227 
228     override fun getInput(): List<Any> {
229         return mutablePinInput.value.getPin()
230     }
231 
232     private fun computeBackspaceButtonAppearance(
233         pinInput: PinInputViewModel,
234         isAutoConfirmEnabled: Boolean,
235     ): ActionButtonAppearance {
236         val isEmpty = pinInput.isEmpty()
237 
238         return when {
239             isAutoConfirmEnabled && isEmpty -> ActionButtonAppearance.Hidden
240             isAutoConfirmEnabled -> ActionButtonAppearance.Subtle
241             else -> ActionButtonAppearance.Shown
242         }
243     }
244 
245     /**
246      * Notifies that a key event has occurred.
247      *
248      * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise.
249      */
250     override fun onKeyEvent(type: KeyEventType, keyCode: Int): Boolean {
251         return when (type) {
252             KeyEventType.KeyUp -> {
253                 if (isConfirmKey(keyCode)) {
254                     onAuthenticateButtonClicked()
255                     true
256                 } else {
257                     false
258                 }
259             }
260             KeyEventType.KeyDown -> {
261                 when (keyCode) {
262                     KEYCODE_DEL -> {
263                         onBackspaceButtonClicked()
264                         true
265                     }
266                     in KEYCODE_0..KEYCODE_9 -> {
267                         onPinButtonClicked(keyCode - KEYCODE_0)
268                         true
269                     }
270                     in KEYCODE_NUMPAD_0..KEYCODE_NUMPAD_9 -> {
271                         onPinButtonClicked(keyCode - KEYCODE_NUMPAD_0)
272                         true
273                     }
274                     else -> {
275                         false
276                     }
277                 }
278             }
279             else -> false
280         }
281     }
282 
283     /**
284      * Notifies that the user has pressed down on a digit button. This function also performs haptic
285      * feedback on the view.
286      */
287     fun onDigitButtonDown(view: View?) {
288         // This ends up calling FalsingInteractor#avoidGesture() each time a PIN button is touched.
289         // It helps make sure that legitimate touch in the PIN bouncer isn't treated as false touch.
290         super.onDown()
291 
292         if (bouncerHapticPlayer?.isEnabled == true) {
293             bouncerHapticPlayer.playNumpadKeyFeedback()
294         } else {
295             view?.performHapticFeedback(
296                 HapticFeedbackConstants.VIRTUAL_KEY,
297                 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
298             )
299         }
300     }
301 
302     @AssistedFactory
303     interface Factory {
304         fun create(
305             isInputEnabled: StateFlow<Boolean>,
306             onIntentionalUserInput: () -> Unit,
307             authenticationMethod: AuthenticationMethodModel,
308             bouncerHapticPlayer: BouncerHapticPlayer,
309         ): PinBouncerViewModel
310     }
311 
312     private sealed interface Request
313 
314     private data object OnErrorDialogDismissed : Request
315 
316     private data object OnAuthenticateButtonClickedForSim : Request
317 }
318 
319 /** Appearance of pin-pad action buttons. */
320 enum class ActionButtonAppearance {
321     /** Button must not be shown. */
322     Hidden,
323 
324     /** Button is shown, but with no background to make it less prominent. */
325     Subtle,
326 
327     /** Button is shown. */
328     Shown,
329 }
330