1 /*
2  * 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.domain.interactor
18 
19 import android.content.Context
20 import com.android.systemui.R
21 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
22 import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
23 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
24 import com.android.systemui.bouncer.data.repository.BouncerRepository
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dagger.qualifiers.Application
27 import com.android.systemui.flags.FeatureFlags
28 import com.android.systemui.flags.Flags
29 import com.android.systemui.scene.domain.interactor.SceneInteractor
30 import com.android.systemui.scene.shared.model.SceneKey
31 import com.android.systemui.scene.shared.model.SceneModel
32 import com.android.systemui.util.kotlin.pairwise
33 import javax.inject.Inject
34 import kotlin.time.Duration.Companion.milliseconds
35 import kotlinx.coroutines.CoroutineScope
36 import kotlinx.coroutines.flow.SharingStarted
37 import kotlinx.coroutines.flow.StateFlow
38 import kotlinx.coroutines.flow.combine
39 import kotlinx.coroutines.flow.stateIn
40 import kotlinx.coroutines.launch
41 
42 /** Encapsulates business logic and application state accessing use-cases. */
43 @SysUISingleton
44 class BouncerInteractor
45 @Inject
46 constructor(
47     @Application private val applicationScope: CoroutineScope,
48     @Application private val applicationContext: Context,
49     private val repository: BouncerRepository,
50     private val authenticationInteractor: AuthenticationInteractor,
51     private val sceneInteractor: SceneInteractor,
52     featureFlags: FeatureFlags,
53 ) {
54 
55     /** The user-facing message to show in the bouncer. */
56     val message: StateFlow<String?> =
57         combine(
58                 repository.message,
59                 authenticationInteractor.isThrottled,
60                 authenticationInteractor.throttling,
61             ) { message, isThrottled, throttling ->
62                 messageOrThrottlingMessage(message, isThrottled, throttling)
63             }
64             .stateIn(
65                 scope = applicationScope,
66                 started = SharingStarted.WhileSubscribed(),
67                 initialValue =
68                     messageOrThrottlingMessage(
69                         repository.message.value,
70                         authenticationInteractor.isThrottled.value,
71                         authenticationInteractor.throttling.value,
72                     )
73             )
74 
75     /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */
76     val throttling: StateFlow<AuthenticationThrottlingModel> = authenticationInteractor.throttling
77 
78     /**
79      * Whether currently throttled and the user has to wait before being able to try another
80      * authentication attempt.
81      */
82     val isThrottled: StateFlow<Boolean> = authenticationInteractor.isThrottled
83 
84     /** Whether the auto confirm feature is enabled for the currently-selected user. */
85     val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled
86 
87     /** The length of the hinted PIN, or `null`, if pin length hint should not be shown. */
88     val hintedPinLength: StateFlow<Int?> = authenticationInteractor.hintedPinLength
89 
90     /** Whether the pattern should be visible for the currently-selected user. */
91     val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible
92 
93     init {
94         if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
95             // Clear the message if moved from throttling to no-longer throttling.
96             applicationScope.launch {
97                 isThrottled.pairwise().collect { (wasThrottled, currentlyThrottled) ->
98                     if (wasThrottled && !currentlyThrottled) {
99                         clearMessage()
100                     }
101                 }
102             }
103         }
104     }
105 
106     /**
107      * Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown.
108      *
109      * @param message An optional message to show to the user in the bouncer.
110      */
111     fun showOrUnlockDevice(
112         message: String? = null,
113     ) {
114         applicationScope.launch {
115             if (authenticationInteractor.isAuthenticationRequired()) {
116                 repository.setMessage(
117                     message ?: promptMessage(authenticationInteractor.getAuthenticationMethod())
118                 )
119                 sceneInteractor.changeScene(
120                     scene = SceneModel(SceneKey.Bouncer),
121                     loggingReason = "request to unlock device while authentication required",
122                 )
123             } else {
124                 sceneInteractor.changeScene(
125                     scene = SceneModel(SceneKey.Gone),
126                     loggingReason = "request to unlock device while authentication isn't required",
127                 )
128             }
129         }
130     }
131 
132     /**
133      * Resets the user-facing message back to the default according to the current authentication
134      * method.
135      */
136     fun resetMessage() {
137         applicationScope.launch {
138             repository.setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
139         }
140     }
141 
142     /** Removes the user-facing message. */
143     fun clearMessage() {
144         repository.setMessage(null)
145     }
146 
147     /**
148      * Attempts to authenticate based on the given user input.
149      *
150      * If the input is correct, the device will be unlocked and the lock screen and bouncer will be
151      * dismissed and hidden.
152      *
153      * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
154      * supports auto-confirming, and the input's length is at least the code's length. Otherwise,
155      * `null` is returned.
156      *
157      * @param input The input from the user to try to authenticate with. This can be a list of
158      *   different things, based on the current authentication method.
159      * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
160      *   request to validate.
161      * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
162      *   authentication failed, `null` if the check was not performed.
163      */
164     suspend fun authenticate(
165         input: List<Any>,
166         tryAutoConfirm: Boolean = false,
167     ): Boolean? {
168         val isAuthenticated =
169             authenticationInteractor.authenticate(input, tryAutoConfirm) ?: return null
170 
171         if (isAuthenticated) {
172             sceneInteractor.changeScene(
173                 scene = SceneModel(SceneKey.Gone),
174                 loggingReason = "successful authentication",
175             )
176         } else {
177             repository.setMessage(errorMessage(authenticationInteractor.getAuthenticationMethod()))
178         }
179 
180         return isAuthenticated
181     }
182 
183     private fun promptMessage(authMethod: AuthenticationMethodModel): String {
184         return when (authMethod) {
185             is AuthenticationMethodModel.Pin ->
186                 applicationContext.getString(R.string.keyguard_enter_your_pin)
187             is AuthenticationMethodModel.Password ->
188                 applicationContext.getString(R.string.keyguard_enter_your_password)
189             is AuthenticationMethodModel.Pattern ->
190                 applicationContext.getString(R.string.keyguard_enter_your_pattern)
191             else -> ""
192         }
193     }
194 
195     private fun errorMessage(authMethod: AuthenticationMethodModel): String {
196         return when (authMethod) {
197             is AuthenticationMethodModel.Pin -> applicationContext.getString(R.string.kg_wrong_pin)
198             is AuthenticationMethodModel.Password ->
199                 applicationContext.getString(R.string.kg_wrong_password)
200             is AuthenticationMethodModel.Pattern ->
201                 applicationContext.getString(R.string.kg_wrong_pattern)
202             else -> ""
203         }
204     }
205 
206     private fun messageOrThrottlingMessage(
207         message: String?,
208         isThrottled: Boolean,
209         throttlingModel: AuthenticationThrottlingModel,
210     ): String {
211         return when {
212             isThrottled ->
213                 applicationContext.getString(
214                     com.android.internal.R.string.lockscreen_too_many_failed_attempts_countdown,
215                     throttlingModel.remainingMs.milliseconds.inWholeSeconds,
216                 )
217             message != null -> message
218             else -> ""
219         }
220     }
221 }
222