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 com.android.keyguard.PinShapeAdapter 21 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor 22 import kotlinx.coroutines.CoroutineScope 23 import kotlinx.coroutines.flow.MutableStateFlow 24 import kotlinx.coroutines.flow.SharingStarted 25 import kotlinx.coroutines.flow.StateFlow 26 import kotlinx.coroutines.flow.combine 27 import kotlinx.coroutines.flow.map 28 import kotlinx.coroutines.flow.stateIn 29 import kotlinx.coroutines.launch 30 31 /** Holds UI state and handles user input for the PIN code bouncer UI. */ 32 class PinBouncerViewModel( 33 applicationContext: Context, 34 private val applicationScope: CoroutineScope, 35 private val interactor: BouncerInteractor, 36 isInputEnabled: StateFlow<Boolean>, 37 ) : 38 AuthMethodBouncerViewModel( 39 isInputEnabled = isInputEnabled, 40 ) { 41 42 val pinShapes = PinShapeAdapter(applicationContext) 43 private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty()) 44 45 /** Currently entered pin keys. */ 46 val pinInput: StateFlow<PinInputViewModel> = mutablePinInput 47 48 /** The length of the PIN for which we should show a hint. */ 49 val hintedPinLength: StateFlow<Int?> = interactor.hintedPinLength 50 51 /** Appearance of the backspace button. */ 52 val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> = 53 combine( 54 mutablePinInput, 55 interactor.isAutoConfirmEnabled, 56 ) { mutablePinEntries, isAutoConfirmEnabled -> 57 computeBackspaceButtonAppearance( 58 pinInput = mutablePinEntries, 59 isAutoConfirmEnabled = isAutoConfirmEnabled, 60 ) 61 } 62 .stateIn( 63 scope = applicationScope, 64 // Make sure this is kept as WhileSubscribed or we can run into a bug where the 65 // downstream continues to receive old/stale/cached values. 66 started = SharingStarted.WhileSubscribed(), 67 initialValue = ActionButtonAppearance.Hidden, 68 ) 69 70 /** Appearance of the confirm button. */ 71 val confirmButtonAppearance: StateFlow<ActionButtonAppearance> = 72 interactor.isAutoConfirmEnabled 73 .map { 74 if (it) { 75 ActionButtonAppearance.Hidden 76 } else { 77 ActionButtonAppearance.Shown 78 } 79 } 80 .stateIn( 81 scope = applicationScope, 82 started = SharingStarted.Eagerly, 83 initialValue = ActionButtonAppearance.Hidden, 84 ) 85 86 /** Notifies that the UI has been shown to the user. */ 87 fun onShown() { 88 interactor.resetMessage() 89 } 90 91 /** Notifies that the user clicked on a PIN button with the given digit value. */ 92 fun onPinButtonClicked(input: Int) { 93 val pinInput = mutablePinInput.value 94 if (pinInput.isEmpty()) { 95 interactor.clearMessage() 96 } 97 98 mutablePinInput.value = pinInput.append(input) 99 tryAuthenticate(useAutoConfirm = true) 100 } 101 102 /** Notifies that the user clicked the backspace button. */ 103 fun onBackspaceButtonClicked() { 104 mutablePinInput.value = mutablePinInput.value.deleteLast() 105 } 106 107 /** Notifies that the user long-pressed the backspace button. */ 108 fun onBackspaceButtonLongPressed() { 109 mutablePinInput.value = mutablePinInput.value.clearAll() 110 } 111 112 /** Notifies that the user clicked the "enter" button. */ 113 fun onAuthenticateButtonClicked() { 114 tryAuthenticate(useAutoConfirm = false) 115 } 116 117 private fun tryAuthenticate(useAutoConfirm: Boolean) { 118 val pinCode = mutablePinInput.value.getPin() 119 120 applicationScope.launch { 121 val isSuccess = interactor.authenticate(pinCode, useAutoConfirm) ?: return@launch 122 123 if (!isSuccess) { 124 showFailureAnimation() 125 } 126 127 // TODO(b/291528545): this should not be cleared on success (at least until the view 128 // is animated away). 129 mutablePinInput.value = mutablePinInput.value.clearAll() 130 } 131 } 132 133 private fun computeBackspaceButtonAppearance( 134 pinInput: PinInputViewModel, 135 isAutoConfirmEnabled: Boolean, 136 ): ActionButtonAppearance { 137 val isEmpty = pinInput.isEmpty() 138 139 return when { 140 isAutoConfirmEnabled && isEmpty -> ActionButtonAppearance.Hidden 141 isAutoConfirmEnabled -> ActionButtonAppearance.Subtle 142 else -> ActionButtonAppearance.Shown 143 } 144 } 145 } 146 147 /** Appearance of pin-pad action buttons. */ 148 enum class ActionButtonAppearance { 149 /** Button must not be shown. */ 150 Hidden, 151 /** Button is shown, but with no background to make it less prominent. */ 152 Subtle, 153 /** Button is shown. */ 154 Shown, 155 } 156