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.os.UserHandle
20 import com.android.keyguard.KeyguardUpdateMonitor
21 import com.android.systemui.plugins.statusbar.StatusBarStateController
22 import com.android.systemui.statusbar.NotificationLockscreenUserManager
23 import com.android.systemui.statusbar.StatusBarState
24 import com.android.systemui.statusbar.notification.DynamicPrivacyController
25 import com.android.systemui.statusbar.notification.collection.GroupEntry
26 import com.android.systemui.statusbar.notification.collection.ListEntry
27 import com.android.systemui.statusbar.notification.collection.NotifPipeline
28 import com.android.systemui.statusbar.notification.collection.NotificationEntry
29 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
30 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
31 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
32 import com.android.systemui.statusbar.policy.KeyguardStateController
33 import dagger.Binds
34 import dagger.Module
35 import javax.inject.Inject
36 
37 @Module(includes = [PrivateSensitiveContentCoordinatorModule::class])
38 interface SensitiveContentCoordinatorModule
39 
40 @Module
41 private interface PrivateSensitiveContentCoordinatorModule {
42     @Binds
43     fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
44 }
45 
46 /** Coordinates re-inflation and post-processing of sensitive notification content. */
47 interface SensitiveContentCoordinator : Coordinator
48 
49 @CoordinatorScope
50 private class SensitiveContentCoordinatorImpl @Inject constructor(
51     private val dynamicPrivacyController: DynamicPrivacyController,
52     private val lockscreenUserManager: NotificationLockscreenUserManager,
53     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
54     private val statusBarStateController: StatusBarStateController,
55     private val keyguardStateController: KeyguardStateController
56 ) : Invalidator("SensitiveContentInvalidator"),
57         SensitiveContentCoordinator,
58         DynamicPrivacyController.Listener,
59         OnBeforeRenderListListener {
60 
61     override fun attach(pipeline: NotifPipeline) {
62         dynamicPrivacyController.addListener(this)
63         pipeline.addOnBeforeRenderListListener(this)
64         pipeline.addPreRenderInvalidator(this)
65     }
66 
67     override fun onDynamicPrivacyChanged(): Unit = invalidateList("onDynamicPrivacyChanged")
68 
69     override fun onBeforeRenderList(entries: List<ListEntry>) {
70         if (keyguardStateController.isKeyguardGoingAway() ||
71                 statusBarStateController.getState() == StatusBarState.KEYGUARD &&
72                 keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
73                         KeyguardUpdateMonitor.getCurrentUser())) {
74             // don't update yet if:
75             // - the keyguard is currently going away
76             // - LS is about to be dismissed by a biometric that bypasses LS (avoid notif flash)
77 
78             // TODO(b/206118999): merge this class with KeyguardCoordinator which ensures the
79             // dependent state changes invalidate the pipeline
80             return
81         }
82 
83         val currentUserId = lockscreenUserManager.currentUserId
84         val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId)
85         val deviceSensitive = devicePublic &&
86                 !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)
87         val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked
88         for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) {
89             val notifUserId = entry.sbn.user.identifier
90             val userLockscreen = devicePublic ||
91                     lockscreenUserManager.isLockscreenPublicMode(notifUserId)
92             val userPublic = when {
93                 // if we're not on the lockscreen, we're definitely private
94                 !userLockscreen -> false
95                 // we are on the lockscreen, so unless we're dynamically unlocked, we're
96                 // definitely public
97                 !dynamicallyUnlocked -> true
98                 // we're dynamically unlocked, but check if the notification needs
99                 // a separate challenge if it's from a work profile
100                 else -> when (notifUserId) {
101                     currentUserId -> false
102                     UserHandle.USER_ALL -> false
103                     else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
104                 }
105             }
106             val needsRedaction = lockscreenUserManager.needsRedaction(entry)
107             val isSensitive = userPublic && needsRedaction
108             entry.setSensitive(isSensitive, deviceSensitive)
109         }
110     }
111 }
112 
113 private fun extractAllRepresentativeEntries(
114     entries: List<ListEntry>
115 ): Sequence<NotificationEntry> =
116     entries.asSequence().flatMap(::extractAllRepresentativeEntries)
117 
118 private fun extractAllRepresentativeEntries(listEntry: ListEntry): Sequence<NotificationEntry> =
119     sequence {
120         listEntry.representativeEntry?.let { yield(it) }
121         if (listEntry is GroupEntry) {
122             yieldAll(extractAllRepresentativeEntries(listEntry.children))
123         }
124     }
125