• 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 @file:OptIn(ExperimentalCoroutinesApi::class)
18 
19 package com.android.systemui.bouncer.ui.viewmodel
20 
21 import android.content.Context
22 import android.view.KeyEvent.KEYCODE_0
23 import android.view.KeyEvent.KEYCODE_9
24 import android.view.KeyEvent.KEYCODE_DEL
25 import android.view.KeyEvent.KEYCODE_NUMPAD_0
26 import android.view.KeyEvent.KEYCODE_NUMPAD_9
27 import android.view.KeyEvent.isConfirmKey
28 import androidx.compose.ui.input.key.KeyEvent
29 import androidx.compose.ui.input.key.KeyEventType
30 import com.android.keyguard.PinShapeAdapter
31 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
32 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
33 import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
34 import com.android.systemui.res.R
35 import kotlinx.coroutines.CoroutineScope
36 import kotlinx.coroutines.ExperimentalCoroutinesApi
37 import kotlinx.coroutines.flow.MutableStateFlow
38 import kotlinx.coroutines.flow.SharingStarted
39 import kotlinx.coroutines.flow.StateFlow
40 import kotlinx.coroutines.flow.combine
41 import kotlinx.coroutines.flow.flowOf
42 import kotlinx.coroutines.flow.map
43 import kotlinx.coroutines.flow.stateIn
44 import kotlinx.coroutines.launch
45 
46 /** Holds UI state and handles user input for the PIN code bouncer UI. */
47 class PinBouncerViewModel(
48     applicationContext: Context,
49     viewModelScope: CoroutineScope,
50     interactor: BouncerInteractor,
51     isInputEnabled: StateFlow<Boolean>,
52     private val onIntentionalUserInput: () -> Unit,
53     private val simBouncerInteractor: SimBouncerInteractor,
54     authenticationMethod: AuthenticationMethodModel,
55 ) :
56     AuthMethodBouncerViewModel(
57         viewModelScope = viewModelScope,
58         interactor = interactor,
59         isInputEnabled = isInputEnabled,
60     ) {
61     /**
62      * Whether the sim-related UI in the pin view is showing.
63      *
64      * This UI is used to unlock a locked sim.
65      */
66     val isSimAreaVisible = authenticationMethod == AuthenticationMethodModel.Sim
67     val isLockedEsim: StateFlow<Boolean?> = simBouncerInteractor.isLockedEsim
68     val errorDialogMessage: StateFlow<String?> = simBouncerInteractor.errorDialogMessage
69     val isSimUnlockingDialogVisible: MutableStateFlow<Boolean> = MutableStateFlow(false)
70     val pinShapes = PinShapeAdapter(applicationContext)
71     private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty())
72 
73     /** Currently entered pin keys. */
74     val pinInput: StateFlow<PinInputViewModel> = mutablePinInput
75 
76     /** The length of the PIN for which we should show a hint. */
77     val hintedPinLength: StateFlow<Int?> =
78         if (isSimAreaVisible) {
79                 flowOf(null)
80             } else {
81                 interactor.hintedPinLength
82             }
83             .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
84 
85     /** Appearance of the backspace button. */
86     val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
87         combine(
88                 mutablePinInput,
89                 interactor.isAutoConfirmEnabled,
90             ) { mutablePinEntries, isAutoConfirmEnabled ->
91                 computeBackspaceButtonAppearance(
92                     pinInput = mutablePinEntries,
93                     isAutoConfirmEnabled = isAutoConfirmEnabled,
94                 )
95             }
96             .stateIn(
97                 scope = viewModelScope,
98                 // Make sure this is kept as WhileSubscribed or we can run into a bug where the
99                 // downstream continues to receive old/stale/cached values.
100                 started = SharingStarted.WhileSubscribed(),
101                 initialValue = ActionButtonAppearance.Hidden,
102             )
103 
104     /** Appearance of the confirm button. */
105     val confirmButtonAppearance: StateFlow<ActionButtonAppearance> =
106         interactor.isAutoConfirmEnabled
107             .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown }
108             .stateIn(
109                 scope = viewModelScope,
110                 started = SharingStarted.WhileSubscribed(),
111                 initialValue = ActionButtonAppearance.Hidden,
112             )
113 
114     override val authenticationMethod: AuthenticationMethodModel = authenticationMethod
115 
116     override val lockoutMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
117 
118     init {
119         viewModelScope.launch { simBouncerInteractor.subId.collect { onResetSimFlow() } }
120     }
121 
122     /** Notifies that the user dismissed the sim pin error dialog. */
123     fun onErrorDialogDismissed() {
124         viewModelScope.launch { simBouncerInteractor.onErrorDialogDismissed() }
125     }
126 
127     /**
128      * Whether the digit buttons should be animated when touched. Note that this doesn't affect the
129      * delete or enter buttons; those should always animate.
130      */
131     val isDigitButtonAnimationEnabled: StateFlow<Boolean> =
132         interactor.isPinEnhancedPrivacyEnabled
133             .map { !it }
134             .stateIn(
135                 scope = viewModelScope,
136                 started = SharingStarted.WhileSubscribed(),
137                 initialValue = !interactor.isPinEnhancedPrivacyEnabled.value,
138             )
139 
140     /** Notifies that the user clicked on a PIN button with the given digit value. */
141     fun onPinButtonClicked(input: Int) {
142         val pinInput = mutablePinInput.value
143 
144         onIntentionalUserInput()
145 
146         val maxInputLength = hintedPinLength.value ?: Int.MAX_VALUE
147         if (pinInput.getPin().size < maxInputLength) {
148             mutablePinInput.value = pinInput.append(input)
149             tryAuthenticate(useAutoConfirm = true)
150         }
151     }
152 
153     /** Notifies that the user clicked the backspace button. */
154     fun onBackspaceButtonClicked() {
155         mutablePinInput.value = mutablePinInput.value.deleteLast()
156     }
157 
158     /** Notifies that the user long-pressed the backspace button. */
159     fun onBackspaceButtonLongPressed() {
160         clearInput()
161     }
162 
163     /** Notifies that the user clicked the "enter" button. */
164     fun onAuthenticateButtonClicked() {
165         if (authenticationMethod == AuthenticationMethodModel.Sim) {
166             viewModelScope.launch {
167                 isSimUnlockingDialogVisible.value = true
168                 simBouncerInteractor.verifySim(getInput())
169                 isSimUnlockingDialogVisible.value = false
170                 clearInput()
171             }
172         } else {
173             tryAuthenticate(useAutoConfirm = false)
174         }
175     }
176 
177     fun onDisableEsimButtonClicked() {
178         viewModelScope.launch { simBouncerInteractor.disableEsim() }
179     }
180 
181     /** Resets the sim screen and shows a default message. */
182     private fun onResetSimFlow() {
183         simBouncerInteractor.resetSimPukUserInput()
184         clearInput()
185     }
186 
187     override fun clearInput() {
188         mutablePinInput.value = mutablePinInput.value.clearAll()
189     }
190 
191     override fun getInput(): List<Any> {
192         return mutablePinInput.value.getPin()
193     }
194 
195     private fun computeBackspaceButtonAppearance(
196         pinInput: PinInputViewModel,
197         isAutoConfirmEnabled: Boolean,
198     ): ActionButtonAppearance {
199         val isEmpty = pinInput.isEmpty()
200 
201         return when {
202             isAutoConfirmEnabled && isEmpty -> ActionButtonAppearance.Hidden
203             isAutoConfirmEnabled -> ActionButtonAppearance.Subtle
204             else -> ActionButtonAppearance.Shown
205         }
206     }
207 
208     /**
209      * Notifies that a key event has occurred.
210      *
211      * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise.
212      */
213     fun onKeyEvent(type: KeyEventType, keyCode: Int): Boolean {
214         return when (type) {
215             KeyEventType.KeyUp -> {
216                 if (isConfirmKey(keyCode)) {
217                     onAuthenticateButtonClicked()
218                     true
219                 } else {
220                     false
221                 }
222             }
223             KeyEventType.KeyDown -> {
224                 when (keyCode) {
225                     KEYCODE_DEL -> {
226                         onBackspaceButtonClicked()
227                         true
228                     }
229                     in KEYCODE_0..KEYCODE_9 -> {
230                         onPinButtonClicked(keyCode - KEYCODE_0)
231                         true
232                     }
233                     in KEYCODE_NUMPAD_0..KEYCODE_NUMPAD_9 -> {
234                         onPinButtonClicked(keyCode - KEYCODE_NUMPAD_0)
235                         true
236                     }
237                     else -> {
238                         false
239                     }
240                 }
241             }
242             else -> false
243         }
244     }
245 }
246 
247 /** Appearance of pin-pad action buttons. */
248 enum class ActionButtonAppearance {
249     /** Button must not be shown. */
250     Hidden,
251 
252     /** Button is shown, but with no background to make it less prominent. */
253     Subtle,
254 
255     /** Button is shown. */
256     Shown,
257 }
258