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