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.os.CountDownTimer
20 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
21 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
22 import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT
23 import com.android.systemui.bouncer.data.factory.BouncerMessageFactory
24 import com.android.systemui.bouncer.data.repository.BouncerMessageRepository
25 import com.android.systemui.bouncer.shared.model.BouncerMessageModel
26 import com.android.systemui.dagger.SysUISingleton
27 import com.android.systemui.flags.FeatureFlags
28 import com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES
29 import com.android.systemui.user.data.repository.UserRepository
30 import javax.inject.Inject
31 import kotlin.math.roundToInt
32 import kotlinx.coroutines.flow.Flow
33 import kotlinx.coroutines.flow.combine
34 import kotlinx.coroutines.flow.distinctUntilChanged
35 import kotlinx.coroutines.flow.flowOf
36 import kotlinx.coroutines.flow.map
37 
38 @SysUISingleton
39 class BouncerMessageInteractor
40 @Inject
41 constructor(
42     private val repository: BouncerMessageRepository,
43     private val factory: BouncerMessageFactory,
44     private val userRepository: UserRepository,
45     private val countDownTimerUtil: CountDownTimerUtil,
46     private val featureFlags: FeatureFlags,
47 ) {
48     fun onPrimaryAuthLockedOut(secondsBeforeLockoutReset: Long) {
49         if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
50 
51         val callback =
52             object : CountDownTimerCallback {
53                 override fun onFinish() {
54                     repository.clearMessage()
55                 }
56 
57                 override fun onTick(millisUntilFinished: Long) {
58                     val secondsRemaining = (millisUntilFinished / 1000.0).roundToInt()
59                     val message =
60                         factory.createFromPromptReason(
61                             reason = PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT,
62                             userId = userRepository.getSelectedUserInfo().id
63                         )
64                     message?.message?.animate = false
65                     message?.message?.formatterArgs =
66                         mutableMapOf<String, Any>(Pair("count", secondsRemaining))
67                     repository.setPrimaryAuthMessage(message)
68                 }
69             }
70         countDownTimerUtil.startNewTimer(secondsBeforeLockoutReset * 1000, 1000, callback)
71     }
72 
73     fun onPrimaryAuthIncorrectAttempt() {
74         if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
75 
76         repository.setPrimaryAuthMessage(
77             factory.createFromPromptReason(
78                 PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
79                 userRepository.getSelectedUserInfo().id
80             )
81         )
82     }
83 
84     fun setFingerprintAcquisitionMessage(value: String?) {
85         if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
86 
87         repository.setFingerprintAcquisitionMessage(
88             if (value != null) {
89                 factory.createFromPromptReason(
90                     PROMPT_REASON_DEFAULT,
91                     userRepository.getSelectedUserInfo().id,
92                     secondaryMsgOverride = value
93                 )
94             } else {
95                 null
96             }
97         )
98     }
99 
100     fun setFaceAcquisitionMessage(value: String?) {
101         if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
102 
103         repository.setFaceAcquisitionMessage(
104             if (value != null) {
105                 factory.createFromPromptReason(
106                     PROMPT_REASON_DEFAULT,
107                     userRepository.getSelectedUserInfo().id,
108                     secondaryMsgOverride = value
109                 )
110             } else {
111                 null
112             }
113         )
114     }
115 
116     fun setCustomMessage(value: String?) {
117         if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
118 
119         repository.setCustomMessage(
120             if (value != null) {
121                 factory.createFromPromptReason(
122                     PROMPT_REASON_DEFAULT,
123                     userRepository.getSelectedUserInfo().id,
124                     secondaryMsgOverride = value
125                 )
126             } else {
127                 null
128             }
129         )
130     }
131 
132     fun onPrimaryBouncerUserInput() {
133         if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
134 
135         repository.clearMessage()
136     }
137 
138     fun onBouncerBeingHidden() {
139         if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
140 
141         repository.clearMessage()
142     }
143 
144     private fun firstNonNullMessage(
145         oneMessageModel: Flow<BouncerMessageModel?>,
146         anotherMessageModel: Flow<BouncerMessageModel?>
147     ): Flow<BouncerMessageModel?> {
148         return oneMessageModel.combine(anotherMessageModel) { a, b -> a ?: b }
149     }
150 
151     // Null if feature flag is enabled which gets ignored always or empty bouncer message model that
152     // always maps to an empty string.
153     private fun nullOrEmptyMessage() =
154         flowOf(
155             if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null else factory.emptyMessage()
156         )
157 
158     val bouncerMessage =
159         listOf(
160                 nullOrEmptyMessage(),
161                 repository.primaryAuthMessage,
162                 repository.biometricAuthMessage,
163                 repository.fingerprintAcquisitionMessage,
164                 repository.faceAcquisitionMessage,
165                 repository.customMessage,
166                 repository.authFlagsMessage,
167                 repository.biometricLockedOutMessage,
168                 userRepository.selectedUserInfo.map {
169                     factory.createFromPromptReason(PROMPT_REASON_DEFAULT, it.id)
170                 },
171             )
172             .reduce(::firstNonNullMessage)
173             .distinctUntilChanged()
174 }
175 
176 interface CountDownTimerCallback {
177     fun onFinish()
178     fun onTick(millisUntilFinished: Long)
179 }
180 
181 @SysUISingleton
182 open class CountDownTimerUtil @Inject constructor() {
183 
184     /**
185      * Start a new count down timer that runs for [millisInFuture] with a tick every
186      * [millisInterval]
187      */
188     fun startNewTimer(
189         millisInFuture: Long,
190         millisInterval: Long,
191         callback: CountDownTimerCallback,
192     ): CountDownTimer {
193         return object : CountDownTimer(millisInFuture, millisInterval) {
194                 override fun onFinish() = callback.onFinish()
195 
196                 override fun onTick(millisUntilFinished: Long) =
197                     callback.onTick(millisUntilFinished)
198             }
199             .start()
200     }
201 }
202