• 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 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