• 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.annotation.StringRes
20 import androidx.compose.ui.input.key.KeyEventType
21 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
22 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
23 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
24 import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
25 import com.android.systemui.lifecycle.ExclusiveActivatable
26 import kotlinx.coroutines.awaitCancellation
27 import kotlinx.coroutines.channels.Channel
28 import kotlinx.coroutines.flow.MutableStateFlow
29 import kotlinx.coroutines.flow.StateFlow
30 import kotlinx.coroutines.flow.asStateFlow
31 import kotlinx.coroutines.flow.collectLatest
32 import kotlinx.coroutines.flow.receiveAsFlow
33 
34 sealed class AuthMethodBouncerViewModel(
35     protected val interactor: BouncerInteractor,
36 
37     /**
38      * Whether user input is enabled.
39      *
40      * If `false`, user input should be completely ignored in the UI as the user is "locked out" of
41      * being able to attempt to unlock the device.
42      */
43     val isInputEnabled: StateFlow<Boolean>,
44 
45     /** Name to use for performance tracing purposes. */
46     val traceName: String,
47     protected val bouncerHapticPlayer: BouncerHapticPlayer? = null,
48 ) : ExclusiveActivatable() {
49 
50     private val _animateFailure = MutableStateFlow(false)
51     /**
52      * Whether a failure animation should be shown. Once consumed, the UI must call
53      * [onFailureAnimationShown] to consume this state.
54      */
55     val animateFailure: StateFlow<Boolean> = _animateFailure.asStateFlow()
56 
57     /** The authentication method that corresponds to this view model. */
58     abstract val authenticationMethod: AuthenticationMethodModel
59 
60     /**
61      * String resource ID of the failure message to be shown during lockout.
62      *
63      * The message must include 2 number parameters: the first one indicating how many unsuccessful
64      * attempts were made, and the second one indicating in how many seconds lockout will expire.
65      */
66     @get:StringRes abstract val lockoutMessageId: Int
67 
68     private val authenticationRequests = Channel<AuthenticationRequest>(Channel.BUFFERED)
69 
70     override suspend fun onActivated(): Nothing {
71         authenticationRequests.receiveAsFlow().collectLatest { request ->
72             if (!isInputEnabled.value) {
73                 return@collectLatest
74             }
75 
76             val authenticationResult =
77                 interactor.authenticate(
78                     input = request.input,
79                     tryAutoConfirm = request.useAutoConfirm,
80                 )
81 
82             if (authenticationResult == AuthenticationResult.SKIPPED && request.useAutoConfirm) {
83                 return@collectLatest
84             }
85 
86             performAuthenticationHapticFeedback(authenticationResult)
87 
88             _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
89             clearInput()
90             if (authenticationResult == AuthenticationResult.SUCCEEDED) {
91                 onSuccessfulAuthentication()
92             }
93         }
94         awaitCancellation()
95     }
96 
97     /**
98      * Notifies that the UI has been hidden from the user (after any transitions have completed).
99      */
100     open fun onHidden() {
101         clearInput()
102     }
103 
104     /** Notifies that the user has placed down a pointer. */
105     fun onDown() {
106         interactor.onDown()
107     }
108 
109     /**
110      * Notifies that the failure animation has been shown. This should be called to consume a `true`
111      * value in [animateFailure].
112      */
113     fun onFailureAnimationShown() {
114         _animateFailure.value = false
115     }
116 
117     /** Clears any previously-entered input. */
118     protected abstract fun clearInput()
119 
120     /** Returns the input entered so far. */
121     protected abstract fun getInput(): List<Any>
122 
123     /** Invoked after a successful authentication. */
124     protected open fun onSuccessfulAuthentication() = Unit
125 
126     /**
127      * Invoked for any key events on the bouncer.
128      *
129      * @return whether the event was consumed by this method and should not be propagated further.
130      */
131     open fun onKeyEvent(type: KeyEventType, keyCode: Int): Boolean = false
132 
133     /** Perform authentication result haptics */
134     private fun performAuthenticationHapticFeedback(result: AuthenticationResult) {
135         if (result == AuthenticationResult.SKIPPED) return
136 
137         bouncerHapticPlayer?.playAuthenticationFeedback(
138             authenticationSucceeded = result == AuthenticationResult.SUCCEEDED
139         )
140     }
141 
142     /**
143      * Attempts to authenticate the user using the current input value.
144      *
145      * @see BouncerInteractor.authenticate
146      */
147     protected fun tryAuthenticate(input: List<Any> = getInput(), useAutoConfirm: Boolean = false) {
148         authenticationRequests.trySend(AuthenticationRequest(input, useAutoConfirm))
149     }
150 
151     private data class AuthenticationRequest(val input: List<Any>, val useAutoConfirm: Boolean)
152 }
153