• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.deviceentry.domain.interactor
18 
19 import android.content.Context
20 import android.content.Intent
21 import com.android.app.tracing.coroutines.launchTraced as launch
22 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
23 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
24 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dagger.qualifiers.Application
27 import com.android.systemui.deviceentry.shared.model.BiometricMessage
28 import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
29 import com.android.systemui.keyguard.KeyguardWmStateRefactor
30 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
31 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
32 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
33 import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
34 import com.android.systemui.keyguard.shared.model.KeyguardState
35 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
36 import com.android.systemui.plugins.ActivityStarter
37 import com.android.systemui.power.domain.interactor.PowerInteractor
38 import com.android.systemui.res.R
39 import com.android.systemui.util.kotlin.combine
40 import com.android.systemui.util.kotlin.sample
41 import javax.inject.Inject
42 import kotlinx.coroutines.CoroutineScope
43 import kotlinx.coroutines.flow.Flow
44 import kotlinx.coroutines.flow.combine
45 import kotlinx.coroutines.flow.distinctUntilChanged
46 import kotlinx.coroutines.flow.emptyFlow
47 import kotlinx.coroutines.flow.filter
48 import kotlinx.coroutines.flow.filterNot
49 import kotlinx.coroutines.flow.flatMapLatest
50 import kotlinx.coroutines.flow.flowOf
51 import kotlinx.coroutines.flow.map
52 
53 /** Business logic for handling authentication events when an app is occluding the lockscreen. */
54 @SysUISingleton
55 class OccludingAppDeviceEntryInteractor
56 @Inject
57 constructor(
58     biometricMessageInteractor: BiometricMessageInteractor,
59     fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
60     keyguardInteractor: KeyguardInteractor,
61     primaryBouncerInteractor: PrimaryBouncerInteractor,
62     alternateBouncerInteractor: AlternateBouncerInteractor,
63     @Application scope: CoroutineScope,
64     private val context: Context,
65     activityStarter: ActivityStarter,
66     powerInteractor: PowerInteractor,
67     keyguardTransitionInteractor: KeyguardTransitionInteractor,
68     communalSceneInteractor: CommunalSceneInteractor,
69 ) {
70     private val keyguardOccludedByApp: Flow<Boolean> =
71         if (KeyguardWmStateRefactor.isEnabled) {
72             combine(
73                     keyguardTransitionInteractor.currentKeyguardState,
74                     communalSceneInteractor.isIdleOnCommunal,
75                     ::Pair,
76                 )
77                 .map { (currentState, isIdleOnCommunal) ->
78                     currentState == KeyguardState.OCCLUDED && !isIdleOnCommunal
79                 }
80         } else {
81             combine(
82                     keyguardInteractor.isKeyguardOccluded,
83                     keyguardInteractor.isKeyguardShowing,
84                     primaryBouncerInteractor.isShowing,
85                     alternateBouncerInteractor.isVisible,
86                     keyguardInteractor.isDozing,
87                     communalSceneInteractor.isIdleOnCommunal,
88                 ) {
89                     occluded,
90                     showing,
91                     primaryBouncerShowing,
92                     alternateBouncerVisible,
93                     dozing,
94                     isIdleOnCommunal ->
95                     occluded &&
96                         showing &&
97                         !primaryBouncerShowing &&
98                         !alternateBouncerVisible &&
99                         !dozing &&
100                         !isIdleOnCommunal
101                 }
102                 .distinctUntilChanged()
103         }
104 
105     private val fingerprintUnlockSuccessEvents: Flow<Unit> =
106         fingerprintAuthRepository.authenticationStatus
107             .ifKeyguardOccludedByApp()
108             .filter { it is SuccessFingerprintAuthenticationStatus }
109             .map {} // maps FingerprintAuthenticationStatus => Unit
110     private val fingerprintLockoutEvents: Flow<Unit> =
111         fingerprintAuthRepository.authenticationStatus
112             .ifKeyguardOccludedByApp()
113             .filter { it is ErrorFingerprintAuthenticationStatus && it.isLockoutError() }
114             .map {} // maps FingerprintAuthenticationStatus => Unit
115     val message: Flow<BiometricMessage?> =
116         biometricMessageInteractor.fingerprintMessage
117             .filterNot { fingerprintMessage ->
118                 // On lockout, the device will show the bouncer. Let's not show the message
119                 // before the transition or else it'll look flickery.
120                 fingerprintMessage is FingerprintLockoutMessage
121             }
122             .ifKeyguardOccludedByApp(/* elseFlow */ flowOf(null))
123 
124     init {
125         // This seems undesirable in most cases, except when a video is playing and can PiP when
126         // unlocked. It was originally added for tablets, so allow it there
127         if (context.resources.getBoolean(R.bool.config_goToHomeFromOccludedApps)) {
128             scope.launch {
129                 // On fingerprint success when the screen is on and not dreaming, go to the home
130                 // screen
131                 fingerprintUnlockSuccessEvents
132                     .sample(
133                         combine(
134                             powerInteractor.isInteractive,
135                             keyguardInteractor.isDreaming,
136                             ::Pair,
137                         )
138                     )
139                     .collect { (interactive, dreaming) ->
140                         if (interactive && !dreaming) {
141                             goToHomeScreen()
142                         }
143                         // don't go to the home screen if the authentication is from
144                         // AOD/dozing/off/dreaming
145                     }
146             }
147         }
148 
149         scope.launch {
150             // On device fingerprint lockout, request the bouncer with a runnable to
151             // go to the home screen. Without this, the bouncer won't proceed to the home
152             // screen.
153             fingerprintLockoutEvents.collect {
154                 activityStarter.dismissKeyguardThenExecute(
155                     object : ActivityStarter.OnDismissAction {
156                         override fun onDismiss(): Boolean {
157                             goToHomeScreen()
158                             return false
159                         }
160 
161                         override fun willRunAnimationOnKeyguard(): Boolean {
162                             return false
163                         }
164                     },
165                     /* cancel= */ null,
166                     /* afterKeyguardGone */ false,
167                 )
168             }
169         }
170     }
171 
172     /** Launches an Activity which forces the current app to background by going home. */
173     private fun goToHomeScreen() {
174         context.startActivity(
175             Intent(Intent.ACTION_MAIN).apply {
176                 addCategory(Intent.CATEGORY_HOME)
177                 flags = Intent.FLAG_ACTIVITY_NEW_TASK
178             }
179         )
180     }
181 
182     private fun <T> Flow<T>.ifKeyguardOccludedByApp(elseFlow: Flow<T> = emptyFlow()): Flow<T> {
183         return keyguardOccludedByApp.flatMapLatest { keyguardOccludedByApp ->
184             if (keyguardOccludedByApp) {
185                 this
186             } else {
187                 elseFlow
188             }
189         }
190     }
191 }
192