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.keyguard.domain.interactor
18 
19 import android.content.Context
20 import android.content.Intent
21 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
22 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dagger.qualifiers.Application
25 import com.android.systemui.flags.FeatureFlags
26 import com.android.systemui.flags.Flags
27 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
28 import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
29 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
30 import com.android.systemui.plugins.ActivityStarter
31 import com.android.systemui.power.domain.interactor.PowerInteractor
32 import com.android.systemui.util.kotlin.sample
33 import javax.inject.Inject
34 import kotlinx.coroutines.CoroutineScope
35 import kotlinx.coroutines.ExperimentalCoroutinesApi
36 import kotlinx.coroutines.flow.Flow
37 import kotlinx.coroutines.flow.combine
38 import kotlinx.coroutines.flow.distinctUntilChanged
39 import kotlinx.coroutines.flow.emptyFlow
40 import kotlinx.coroutines.flow.filter
41 import kotlinx.coroutines.flow.filterNot
42 import kotlinx.coroutines.flow.flatMapLatest
43 import kotlinx.coroutines.flow.flowOf
44 import kotlinx.coroutines.flow.map
45 import kotlinx.coroutines.flow.merge
46 import kotlinx.coroutines.launch
47 
48 /** Business logic for handling authentication events when an app is occluding the lockscreen. */
49 @ExperimentalCoroutinesApi
50 @SysUISingleton
51 class OccludingAppDeviceEntryInteractor
52 @Inject
53 constructor(
54     biometricMessageInteractor: BiometricMessageInteractor,
55     fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
56     keyguardInteractor: KeyguardInteractor,
57     primaryBouncerInteractor: PrimaryBouncerInteractor,
58     alternateBouncerInteractor: AlternateBouncerInteractor,
59     @Application scope: CoroutineScope,
60     private val context: Context,
61     activityStarter: ActivityStarter,
62     powerInteractor: PowerInteractor,
63     featureFlags: FeatureFlags,
64 ) {
65     private val keyguardOccludedByApp: Flow<Boolean> =
66         combine(
67                 keyguardInteractor.isKeyguardOccluded,
68                 keyguardInteractor.isKeyguardShowing,
69                 primaryBouncerInteractor.isShowing,
70                 alternateBouncerInteractor.isVisible,
71             ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible ->
72                 occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible
73             }
74             .distinctUntilChanged()
75     private val fingerprintUnlockSuccessEvents: Flow<Unit> =
76         fingerprintAuthRepository.authenticationStatus
77             .ifKeyguardOccludedByApp()
78             .filter { it is SuccessFingerprintAuthenticationStatus }
79             .map {} // maps FingerprintAuthenticationStatus => Unit
80     private val fingerprintLockoutEvents: Flow<Unit> =
81         fingerprintAuthRepository.authenticationStatus
82             .ifKeyguardOccludedByApp()
83             .filter { it is ErrorFingerprintAuthenticationStatus && it.isLockoutMessage() }
84             .map {} // maps FingerprintAuthenticationStatus => Unit
85     val message: Flow<BiometricMessage?> =
86         merge(
87                 biometricMessageInteractor.fingerprintErrorMessage.filterNot {
88                     it.isFingerprintLockoutMessage()
89                 },
90                 biometricMessageInteractor.fingerprintFailMessage,
91                 biometricMessageInteractor.fingerprintHelpMessage,
92             )
93             .ifKeyguardOccludedByApp(/* elseFlow */ flowOf(null))
94 
95     init {
96         if (featureFlags.isEnabled(Flags.FP_LISTEN_OCCLUDING_APPS)) {
97             scope.launch {
98                 // On fingerprint success when the screen is on, go to the home screen
99                 fingerprintUnlockSuccessEvents.sample(
100                     combine(powerInteractor.isInteractive, keyguardInteractor.isDreaming, ::Pair),
101                 )
102                 .collect { (interactive, dreaming) ->
103                     if (interactive && !dreaming) {
104                         goToHomeScreen()
105                     }
106                     // don't go to the home screen if the authentication is from
107                     // AOD/dozing/off/dreaming
108                 }
109             }
110 
111             scope.launch {
112                 // On device fingerprint lockout, request the bouncer with a runnable to
113                 // go to the home screen. Without this, the bouncer won't proceed to the home
114                 // screen.
115                 fingerprintLockoutEvents.collect {
116                     activityStarter.dismissKeyguardThenExecute(
117                         object : ActivityStarter.OnDismissAction {
118                             override fun onDismiss(): Boolean {
119                                 goToHomeScreen()
120                                 return false
121                             }
122 
123                             override fun willRunAnimationOnKeyguard(): Boolean {
124                                 return false
125                             }
126                         },
127                         /* cancel= */ null,
128                         /* afterKeyguardGone */ false
129                     )
130                 }
131             }
132         }
133     }
134 
135     /** Launches an Activity which forces the current app to background by going home. */
136     private fun goToHomeScreen() {
137         context.startActivity(
138             Intent(Intent.ACTION_MAIN).apply {
139                 addCategory(Intent.CATEGORY_HOME)
140                 flags = Intent.FLAG_ACTIVITY_NEW_TASK
141             }
142         )
143     }
144 
145     private fun <T> Flow<T>.ifKeyguardOccludedByApp(elseFlow: Flow<T> = emptyFlow()): Flow<T> {
146         return keyguardOccludedByApp.flatMapLatest { keyguardOccludedByApp ->
147             if (keyguardOccludedByApp) {
148                 this
149             } else {
150                 elseFlow
151             }
152         }
153     }
154 }
155