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.keyguard.ui.viewmodel
18 
19 import android.animation.ValueAnimator
20 import android.content.Context
21 import android.graphics.Point
22 import androidx.annotation.VisibleForTesting
23 import androidx.core.animation.addListener
24 import com.android.systemui.Flags
25 import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
26 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
27 import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
28 import com.android.systemui.biometrics.shared.model.AuthenticationReason
29 import com.android.systemui.biometrics.shared.model.DisplayRotation
30 import com.android.systemui.biometrics.shared.model.isDefaultOrientation
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.dagger.qualifiers.Application
33 import com.android.systemui.dagger.qualifiers.Main
34 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
35 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
36 import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
37 import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
38 import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
39 import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
40 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
41 import com.android.systemui.power.domain.interactor.PowerInteractor
42 import com.android.systemui.res.R
43 import com.android.systemui.statusbar.phone.DozeServiceHost
44 import javax.inject.Inject
45 import kotlinx.coroutines.CoroutineDispatcher
46 import kotlinx.coroutines.CoroutineScope
47 import kotlinx.coroutines.ExperimentalCoroutinesApi
48 import kotlinx.coroutines.Job
49 import kotlinx.coroutines.flow.Flow
50 import kotlinx.coroutines.flow.MutableStateFlow
51 import kotlinx.coroutines.flow.asStateFlow
52 import kotlinx.coroutines.flow.collectLatest
53 import kotlinx.coroutines.flow.combine
54 import kotlinx.coroutines.flow.distinctUntilChanged
55 import kotlinx.coroutines.flow.filter
56 import kotlinx.coroutines.flow.flatMapLatest
57 import kotlinx.coroutines.flow.flowOn
58 import kotlinx.coroutines.flow.launchIn
59 import kotlinx.coroutines.flow.map
60 import kotlinx.coroutines.flow.merge
61 import kotlinx.coroutines.flow.onCompletion
62 import kotlinx.coroutines.launch
63 
64 @ExperimentalCoroutinesApi
65 @SysUISingleton
66 class SideFpsProgressBarViewModel
67 @Inject
68 constructor(
69     private val context: Context,
70     biometricStatusInteractor: BiometricStatusInteractor,
71     deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
72     private val sfpsSensorInteractor: SideFpsSensorInteractor,
73     // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through
74     //  DozeInteractor as DozeServiceHost already depends on DozeInteractor.
75     private val dozeServiceHost: DozeServiceHost,
76     private val keyguardInteractor: KeyguardInteractor,
77     displayStateInteractor: DisplayStateInteractor,
78     @Main private val mainDispatcher: CoroutineDispatcher,
79     @Application private val applicationScope: CoroutineScope,
80     private val powerInteractor: PowerInteractor,
81 ) {
82     private val _progress = MutableStateFlow(0.0f)
83     private val _visible = MutableStateFlow(false)
84     private var _animator: ValueAnimator? = null
85     private var animatorJob: Job? = null
86 
87     private fun onFingerprintCaptureCompleted() {
88         _visible.value = false
89         _progress.value = 0.0f
90     }
91 
92     // Merged [FingerprintAuthenticationStatus] from BiometricPrompt acquired messages and
93     // device entry authentication messages
94     private val mergedFingerprintAuthenticationStatus =
95         merge(
96                 biometricStatusInteractor.fingerprintAcquiredStatus,
97                 deviceEntryFingerprintAuthInteractor.authenticationStatus
98             )
99             .distinctUntilChanged()
100             .filter {
101                 if (it is AcquiredFingerprintAuthenticationStatus) {
102                     it.authenticationReason == AuthenticationReason.DeviceEntryAuthentication ||
103                         it.authenticationReason ==
104                             AuthenticationReason.BiometricPromptAuthentication
105                 } else {
106                     true
107                 }
108             }
109 
110     val isVisible: Flow<Boolean> = _visible.asStateFlow()
111 
112     val progress: Flow<Float> = _progress.asStateFlow()
113 
114     val progressBarLength: Flow<Int> =
115         sfpsSensorInteractor.sensorLocation.map { it.length }.distinctUntilChanged()
116 
117     val progressBarThickness =
118         context.resources.getDimension(R.dimen.sfps_progress_bar_thickness).toInt()
119 
120     val progressBarLocation =
121         combine(displayStateInteractor.currentRotation, sfpsSensorInteractor.sensorLocation, ::Pair)
122             .map { (rotation, sensorLocation) ->
123                 val paddingFromEdge =
124                     context.resources
125                         .getDimension(R.dimen.sfps_progress_bar_padding_from_edge)
126                         .toInt()
127                 val viewLeftTop = Point(sensorLocation.left, sensorLocation.top)
128                 val totalDistanceFromTheEdge = paddingFromEdge + progressBarThickness
129 
130                 val isSensorVerticalNow =
131                     sensorLocation.isSensorVerticalInDefaultOrientation ==
132                         rotation.isDefaultOrientation()
133                 if (isSensorVerticalNow) {
134                     // Sensor is vertical to the current orientation, we rotate it 270 deg
135                     // around the (left,top) point as the pivot. We need to push it down the
136                     // length of the progress bar so that it is still aligned to the sensor
137                     viewLeftTop.y += sensorLocation.length
138                     val isSensorOnTheNearEdge =
139                         rotation == DisplayRotation.ROTATION_180 ||
140                             rotation == DisplayRotation.ROTATION_90
141                     if (isSensorOnTheNearEdge) {
142                         // Add just the padding from the edge to push the progress bar right
143                         viewLeftTop.x += paddingFromEdge
144                     } else {
145                         // View left top is pushed left from the edge by the progress bar thickness
146                         // and the padding.
147                         viewLeftTop.x -= totalDistanceFromTheEdge
148                     }
149                 } else {
150                     // Sensor is horizontal to the current orientation.
151                     val isSensorOnTheNearEdge =
152                         rotation == DisplayRotation.ROTATION_0 ||
153                             rotation == DisplayRotation.ROTATION_90
154                     if (isSensorOnTheNearEdge) {
155                         // Add just the padding from the edge to push the progress bar down
156                         viewLeftTop.y += paddingFromEdge
157                     } else {
158                         // Sensor is now at the bottom edge of the device in the current rotation.
159                         // We want to push it up from the bottom edge by the padding and
160                         // the thickness of the progressbar.
161                         viewLeftTop.y -= totalDistanceFromTheEdge
162                     }
163                 }
164                 viewLeftTop
165             }
166 
167     val isFingerprintAuthRunning: Flow<Boolean> =
168         combine(
169             deviceEntryFingerprintAuthInteractor.isRunning,
170             biometricStatusInteractor.sfpsAuthenticationReason
171         ) { deviceEntryAuthIsRunning, sfpsAuthReason ->
172             deviceEntryAuthIsRunning ||
173                 sfpsAuthReason == AuthenticationReason.BiometricPromptAuthentication
174         }
175 
176     val rotation: Flow<Float> =
177         combine(displayStateInteractor.currentRotation, sfpsSensorInteractor.sensorLocation, ::Pair)
178             .map { (rotation, sensorLocation) ->
179                 if (
180                     rotation.isDefaultOrientation() ==
181                         sensorLocation.isSensorVerticalInDefaultOrientation
182                 ) {
183                     // We should rotate the progress bar 270 degrees in the clockwise direction with
184                     // the left top point as the pivot so that it fills up from bottom to top
185                     270.0f
186                 } else {
187                     0.0f
188                 }
189             }
190 
191     val isProlongedTouchRequiredForAuthentication: Flow<Boolean> =
192         sfpsSensorInteractor.isProlongedTouchRequiredForAuthentication
193 
194     init {
195         if (Flags.restToUnlock()) {
196             launchAnimator()
197         }
198     }
199 
200     private fun launchAnimator() {
201         applicationScope.launch {
202             sfpsSensorInteractor.isProlongedTouchRequiredForAuthentication.collectLatest { enabled
203                 ->
204                 if (!enabled) {
205                     animatorJob?.cancel()
206                     return@collectLatest
207                 }
208                 animatorJob =
209                     sfpsSensorInteractor.authenticationDuration
210                         .flatMapLatest { authDuration ->
211                             _animator?.cancel()
212                             mergedFingerprintAuthenticationStatus.map {
213                                 authStatus: FingerprintAuthenticationStatus ->
214                                 when (authStatus) {
215                                     is AcquiredFingerprintAuthenticationStatus -> {
216                                         if (authStatus.fingerprintCaptureStarted) {
217                                             if (keyguardInteractor.isDozing.value) {
218                                                 dozeServiceHost.fireSideFpsAcquisitionStarted()
219                                             } else {
220                                                 powerInteractor
221                                                     .wakeUpForSideFingerprintAcquisition()
222                                             }
223                                             _animator?.cancel()
224                                             _animator =
225                                                 ValueAnimator.ofFloat(0.0f, 1.0f)
226                                                     .setDuration(authDuration)
227                                                     .apply {
228                                                         addUpdateListener {
229                                                             _progress.value =
230                                                                 it.animatedValue as Float
231                                                         }
232                                                         addListener(
233                                                             onEnd = {
234                                                                 if (_progress.value == 0.0f) {
235                                                                     _visible.value = false
236                                                                 }
237                                                             },
238                                                             onStart = { _visible.value = true },
239                                                             onCancel = { _visible.value = false }
240                                                         )
241                                                     }
242                                             _animator?.start()
243                                         } else if (authStatus.fingerprintCaptureCompleted) {
244                                             onFingerprintCaptureCompleted()
245                                         } else {
246                                             // Abandoned FP Auth attempt
247                                             _animator?.reverse()
248                                         }
249                                     }
250                                     is ErrorFingerprintAuthenticationStatus ->
251                                         onFingerprintCaptureCompleted()
252                                     is FailFingerprintAuthenticationStatus ->
253                                         onFingerprintCaptureCompleted()
254                                     is SuccessFingerprintAuthenticationStatus ->
255                                         onFingerprintCaptureCompleted()
256                                     else -> Unit
257                                 }
258                             }
259                         }
260                         .flowOn(mainDispatcher)
261                         .onCompletion { _animator?.cancel() }
262                         .launchIn(applicationScope)
263             }
264         }
265     }
266 
267     @VisibleForTesting
268     fun setVisible(isVisible: Boolean) {
269         _visible.value = isVisible
270     }
271 }
272