1 /*
2 * Copyright (C) 2021 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.statusbar.notification.collection.coordinator
18
19 import android.app.Notification
20 import android.os.UserHandle
21 import com.android.app.tracing.coroutines.launchTraced as launch
22 import com.android.keyguard.KeyguardUpdateMonitor
23 import com.android.server.notification.Flags.screenshareNotificationHiding
24 import com.android.systemui.dagger.qualifiers.Application
25 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
26 import com.android.systemui.plugins.statusbar.StatusBarStateController
27 import com.android.systemui.scene.domain.interactor.SceneInteractor
28 import com.android.systemui.scene.shared.flag.SceneContainerFlag
29 import com.android.systemui.scene.shared.model.Scenes
30 import com.android.systemui.statusbar.NotificationLockscreenUserManager
31 import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
32 import com.android.systemui.statusbar.StatusBarState
33 import com.android.systemui.statusbar.notification.DynamicPrivacyController
34 import com.android.systemui.statusbar.notification.collection.GroupEntry
35 import com.android.systemui.statusbar.notification.collection.PipelineEntry
36 import com.android.systemui.statusbar.notification.collection.NotifPipeline
37 import com.android.systemui.statusbar.notification.collection.NotificationEntry
38 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
39 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
40 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
41 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
42 import com.android.systemui.statusbar.policy.KeyguardStateController
43 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
44 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
45 import dagger.Binds
46 import dagger.Module
47 import javax.inject.Inject
48 import kotlinx.coroutines.CoroutineScope
49 import kotlinx.coroutines.flow.distinctUntilChanged
50 import kotlinx.coroutines.flow.mapNotNull
51
52 @Module(includes = [PrivateSensitiveContentCoordinatorModule::class])
53 interface SensitiveContentCoordinatorModule
54
55 @Module
56 interface PrivateSensitiveContentCoordinatorModule {
bindCoordinatornull57 @Binds fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
58 }
59
60 /** Coordinates re-inflation and post-processing of sensitive notification content. */
61 interface SensitiveContentCoordinator : Coordinator
62
63 @CoordinatorScope
64 class SensitiveContentCoordinatorImpl
65 @Inject
66 constructor(
67 private val dynamicPrivacyController: DynamicPrivacyController,
68 private val lockscreenUserManager: NotificationLockscreenUserManager,
69 private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
70 private val statusBarStateController: StatusBarStateController,
71 private val keyguardStateController: KeyguardStateController,
72 private val selectedUserInteractor: SelectedUserInteractor,
73 private val sensitiveNotificationProtectionController:
74 SensitiveNotificationProtectionController,
75 private val deviceEntryInteractor: DeviceEntryInteractor,
76 private val sceneInteractor: SceneInteractor,
77 @Application private val scope: CoroutineScope,
78 ) :
79 Invalidator("SensitiveContentInvalidator"),
80 SensitiveContentCoordinator,
81 DynamicPrivacyController.Listener,
82 OnBeforeRenderListListener {
83 private var inTransitionFromLockedToGone = false
84 private var canSwipeToEnter = false
85
86 private val onSensitiveStateChanged = Runnable() { invalidateList("onSensitiveStateChanged") }
87
88 private val screenshareSecretFilter =
89 object : NotifFilter("ScreenshareSecretFilter") {
90 val NotificationEntry.isSecret
91 get() =
92 channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET ||
93 sbn.notification?.visibility == Notification.VISIBILITY_SECRET
94
95 override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
96 return screenshareNotificationHiding() &&
97 sensitiveNotificationProtectionController.isSensitiveStateActive &&
98 entry.isSecret
99 }
100 }
101
102 override fun attach(pipeline: NotifPipeline) {
103 if (!SceneContainerFlag.isEnabled) {
104 dynamicPrivacyController.addListener(this)
105 }
106 if (screenshareNotificationHiding()) {
107 sensitiveNotificationProtectionController.registerSensitiveStateListener(
108 onSensitiveStateChanged
109 )
110 }
111 pipeline.addOnBeforeRenderListListener(this)
112 pipeline.addPreRenderInvalidator(this)
113 if (screenshareNotificationHiding()) {
114 pipeline.addFinalizeFilter(screenshareSecretFilter)
115 }
116
117 if (SceneContainerFlag.isEnabled) {
118 scope.launch {
119 sceneInteractor.transitionState
120 .mapNotNull {
121 val transitioningToGone = it.isTransitioning(to = Scenes.Gone)
122 val deviceEntered = deviceEntryInteractor.isDeviceEntered.value
123 when {
124 transitioningToGone && !deviceEntered -> true
125 !transitioningToGone -> false
126 else -> null
127 }
128 }
129 .distinctUntilChanged()
130 .collect {
131 inTransitionFromLockedToGone = it
132 invalidateList("inTransitionFromLockedToGoneChanged")
133 }
134 }
135 scope.launch {
136 deviceEntryInteractor.canSwipeToEnter.collect {
137 val canSwipeToEnter = it ?: false
138 if (this@SensitiveContentCoordinatorImpl.canSwipeToEnter != canSwipeToEnter) {
139 this@SensitiveContentCoordinatorImpl.canSwipeToEnter = canSwipeToEnter
140 invalidateList("canSwipeToEnterChanged")
141 }
142 }
143 }
144 }
145 }
146
147 override fun onDynamicPrivacyChanged(): Unit = invalidateList("onDynamicPrivacyChanged")
148
149 private val isKeyguardGoingAway: Boolean
150 get() {
151 if (SceneContainerFlag.isEnabled) {
152 return inTransitionFromLockedToGone
153 } else {
154 return keyguardStateController.isKeyguardGoingAway
155 }
156 }
157
158 override fun onBeforeRenderList(entries: List<PipelineEntry>) {
159 if (
160 isKeyguardGoingAway ||
161 statusBarStateController.state == StatusBarState.KEYGUARD &&
162 keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
163 selectedUserInteractor.getSelectedUserId()
164 )
165 ) {
166 // don't update yet if:
167 // - the keyguard is currently going away
168 // - LS is about to be dismissed by a biometric that bypasses LS (avoid notif flash)
169
170 // TODO(b/206118999): merge this class with KeyguardCoordinator which ensures the
171 // dependent state changes invalidate the pipeline
172 return
173 }
174
175 val isSensitiveContentProtectionActive =
176 screenshareNotificationHiding() &&
177 sensitiveNotificationProtectionController.isSensitiveStateActive
178 val currentUserId = lockscreenUserManager.currentUserId
179 val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId)
180 val deviceSensitive =
181 (devicePublic &&
182 !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) ||
183 isSensitiveContentProtectionActive
184 val dynamicallyUnlocked =
185 if (SceneContainerFlag.isEnabled) canSwipeToEnter
186 else dynamicPrivacyController.isDynamicallyUnlocked
187 for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) {
188 val notifUserId = entry.sbn.user.identifier
189 val userLockscreen =
190 devicePublic || lockscreenUserManager.isLockscreenPublicMode(notifUserId)
191 val userPublic =
192 when {
193 // if we're not on the lockscreen, we're definitely private
194 !userLockscreen -> false
195 // we are on the lockscreen, so unless we're dynamically unlocked, we're
196 // definitely public
197 !dynamicallyUnlocked -> true
198 // we're dynamically unlocked, but check if the notification needs
199 // a separate challenge if it's from a work profile
200 else ->
201 when (notifUserId) {
202 currentUserId -> false
203 UserHandle.USER_ALL -> false
204 else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
205 }
206 }
207
208 val shouldProtectNotification =
209 screenshareNotificationHiding() &&
210 sensitiveNotificationProtectionController.shouldProtectNotification(entry)
211
212 val needsRedaction =
213 lockscreenUserManager.getRedactionType(entry) != REDACTION_TYPE_NONE
214 val isSensitive = userPublic && needsRedaction
215 entry.setSensitive(isSensitive || shouldProtectNotification, deviceSensitive)
216 if (screenshareNotificationHiding()) {
217 entry.row?.setPublicExpanderVisible(!shouldProtectNotification)
218 }
219 }
220 }
221 }
222
extractAllRepresentativeEntriesnull223 private fun extractAllRepresentativeEntries(entries: List<PipelineEntry>): Sequence<NotificationEntry> =
224 entries.asSequence().flatMap(::extractAllRepresentativeEntries)
225
226 private fun extractAllRepresentativeEntries(
227 pipelineEntry: PipelineEntry,
228 ): Sequence<NotificationEntry> =
229 sequence {
230 pipelineEntry.representativeEntry?.let { yield(it) }
231 if (pipelineEntry is GroupEntry) {
232 yieldAll(extractAllRepresentativeEntries(pipelineEntry.children))
233 }
234 }
235