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