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