• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 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 @file:OptIn(ExperimentalCoroutinesApi::class)
18 
19 package com.android.systemui.statusbar.notification.collection.coordinator
20 
21 import android.os.UserHandle
22 import android.provider.Settings
23 import androidx.annotation.VisibleForTesting
24 import com.android.systemui.dagger.qualifiers.Application
25 import com.android.systemui.dagger.qualifiers.Background
26 import com.android.systemui.keyguard.data.repository.KeyguardRepository
27 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
28 import com.android.systemui.keyguard.shared.model.KeyguardState
29 import com.android.systemui.keyguard.shared.model.TransitionState
30 import com.android.systemui.keyguard.shared.model.TransitionStep
31 import com.android.systemui.plugins.statusbar.StatusBarStateController
32 import com.android.systemui.statusbar.StatusBarState
33 import com.android.systemui.statusbar.expansionChanges
34 import com.android.systemui.statusbar.notification.NotifPipelineFlags
35 import com.android.systemui.statusbar.notification.collection.NotifPipeline
36 import com.android.systemui.statusbar.notification.collection.NotificationEntry
37 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
38 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
39 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
40 import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
41 import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl
42 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
43 import com.android.systemui.statusbar.policy.HeadsUpManager
44 import com.android.systemui.statusbar.policy.headsUpEvents
45 import com.android.systemui.util.settings.SecureSettings
46 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
47 import kotlinx.coroutines.CoroutineDispatcher
48 import kotlinx.coroutines.CoroutineScope
49 import kotlinx.coroutines.ExperimentalCoroutinesApi
50 import kotlinx.coroutines.coroutineScope
51 import kotlinx.coroutines.delay
52 import kotlinx.coroutines.flow.Flow
53 import kotlinx.coroutines.flow.collectLatest
54 import kotlinx.coroutines.flow.conflate
55 import kotlinx.coroutines.flow.distinctUntilChanged
56 import kotlinx.coroutines.flow.emitAll
57 import kotlinx.coroutines.flow.first
58 import kotlinx.coroutines.flow.flowOn
59 import kotlinx.coroutines.flow.map
60 import kotlinx.coroutines.flow.onStart
61 import kotlinx.coroutines.flow.transformLatest
62 import kotlinx.coroutines.launch
63 import kotlinx.coroutines.yield
64 import javax.inject.Inject
65 import kotlin.time.Duration
66 import kotlin.time.Duration.Companion.seconds
67 
68 /**
69  * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
70  * headers on the lockscreen.
71  */
72 @CoordinatorScope
73 class KeyguardCoordinator
74 @Inject
75 constructor(
76     @Background private val bgDispatcher: CoroutineDispatcher,
77     private val headsUpManager: HeadsUpManager,
78     private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
79     private val keyguardRepository: KeyguardRepository,
80     private val keyguardTransitionRepository: KeyguardTransitionRepository,
81     private val notifPipelineFlags: NotifPipelineFlags,
82     @Application private val scope: CoroutineScope,
83     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
84     private val secureSettings: SecureSettings,
85     private val seenNotifsProvider: SeenNotificationsProviderImpl,
86     private val statusBarStateController: StatusBarStateController,
87 ) : Coordinator {
88 
89     private val unseenNotifications = mutableSetOf<NotificationEntry>()
90     private var unseenFilterEnabled = false
91 
92     override fun attach(pipeline: NotifPipeline) {
93         setupInvalidateNotifListCallbacks()
94         // Filter at the "finalize" stage so that views remain bound by PreparationCoordinator
95         pipeline.addFinalizeFilter(notifFilter)
96         keyguardNotificationVisibilityProvider.addOnStateChangedListener(::invalidateListFromFilter)
97         updateSectionHeadersVisibility()
98         if (notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard) {
99             attachUnseenFilter(pipeline)
100         }
101     }
102 
103     private fun attachUnseenFilter(pipeline: NotifPipeline) {
104         pipeline.addFinalizeFilter(unseenNotifFilter)
105         pipeline.addCollectionListener(collectionListener)
106         scope.launch { trackUnseenNotificationsWhileUnlocked() }
107         scope.launch { invalidateWhenUnseenSettingChanges() }
108     }
109 
110     private suspend fun trackUnseenNotificationsWhileUnlocked() {
111         // Whether or not we're actively tracking unseen notifications to mark them as seen when
112         // appropriate.
113         val isTrackingUnseen: Flow<Boolean> =
114             keyguardRepository.isKeyguardShowing
115                 // transformLatest so that we can cancel listening to keyguard transitions once
116                 // isKeyguardShowing changes (after a successful transition to the keyguard).
117                 .transformLatest { isShowing ->
118                     if (isShowing) {
119                         // If the keyguard is showing, we're not tracking unseen.
120                         emit(false)
121                     } else {
122                         // If the keyguard stops showing, then start tracking unseen notifications.
123                         emit(true)
124                         // If the screen is turning off, stop tracking, but if that transition is
125                         // cancelled, then start again.
126                         emitAll(
127                             keyguardTransitionRepository.transitions
128                                 .map { step -> !step.isScreenTurningOff }
129                         )
130                     }
131                 }
132                 // Prevent double emit of `false` caused by transition to AOD, followed by keyguard
133                 // showing
134                 .distinctUntilChanged()
135 
136         // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is
137         // showing again
138         var clearUnseenOnBeginTracking = false
139         isTrackingUnseen.collectLatest { trackingUnseen ->
140             if (!trackingUnseen) {
141                 // Wait for the user to spend enough time on the lock screen before clearing unseen
142                 // set when unlocked
143                 awaitTimeSpentNotDozing(SEEN_TIMEOUT)
144                 clearUnseenOnBeginTracking = true
145             } else {
146                 if (clearUnseenOnBeginTracking) {
147                     clearUnseenOnBeginTracking = false
148                     unseenNotifications.clear()
149                 }
150                 unseenNotifFilter.invalidateList("keyguard no longer showing")
151                 trackUnseenNotifications()
152             }
153         }
154     }
155 
156     private suspend fun awaitTimeSpentNotDozing(duration: Duration) {
157         keyguardRepository.isDozing
158             // Use transformLatest so that the timeout delay is cancelled if the device enters doze,
159             // and is restarted when doze ends.
160             .transformLatest { isDozing ->
161                 if (!isDozing) {
162                     delay(duration)
163                     // Signal timeout has completed
164                     emit(Unit)
165                 }
166             }
167             // Suspend until the first emission
168             .first()
169     }
170 
171     private suspend fun trackUnseenNotifications() {
172         coroutineScope {
173             launch { clearUnseenNotificationsWhenShadeIsExpanded() }
174             launch { markHeadsUpNotificationsAsSeen() }
175         }
176     }
177 
178     private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
179         statusBarStateController.expansionChanges.collectLatest { isExpanded ->
180             // Give keyguard events time to propagate, in case this expansion is part of the
181             // keyguard transition and not the user expanding the shade
182             yield()
183             if (isExpanded) {
184                 unseenNotifications.clear()
185             }
186         }
187     }
188 
189     private suspend fun markHeadsUpNotificationsAsSeen() {
190         headsUpManager.allEntries
191             .filter { it.isRowPinned }
192             .forEach { unseenNotifications.remove(it) }
193         headsUpManager.headsUpEvents.collect { (entry, isHun) ->
194             if (isHun) {
195                 unseenNotifications.remove(entry)
196             }
197         }
198     }
199 
200     private suspend fun invalidateWhenUnseenSettingChanges() {
201         secureSettings
202             // emit whenever the setting has changed
203             .observerFlow(
204                 UserHandle.USER_ALL,
205                 Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
206             )
207             // perform a query immediately
208             .onStart { emit(Unit) }
209             // for each change, lookup the new value
210             .map {
211                 secureSettings.getIntForUser(
212                     Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
213                     UserHandle.USER_CURRENT,
214                 ) == 1
215             }
216             // perform lookups on the bg thread pool
217             .flowOn(bgDispatcher)
218             // only track the most recent emission, if events are happening faster than they can be
219             // consumed
220             .conflate()
221             // update local field and invalidate if necessary
222             .collect { setting ->
223                 if (setting != unseenFilterEnabled) {
224                     unseenFilterEnabled = setting
225                     unseenNotifFilter.invalidateList("unseen setting changed")
226                 }
227             }
228     }
229 
230     private val collectionListener =
231         object : NotifCollectionListener {
232             override fun onEntryAdded(entry: NotificationEntry) {
233                 if (
234                     keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
235                 ) {
236                     unseenNotifications.add(entry)
237                 }
238             }
239 
240             override fun onEntryUpdated(entry: NotificationEntry) {
241                 if (
242                     keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
243                 ) {
244                     unseenNotifications.add(entry)
245                 }
246             }
247 
248             override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
249                 unseenNotifications.remove(entry)
250             }
251         }
252 
253     @VisibleForTesting
254     internal val unseenNotifFilter =
255         object : NotifFilter("$TAG-unseen") {
256 
257             var hasFilteredAnyNotifs = false
258 
259             override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
260                 when {
261                     // Don't apply filter if the setting is disabled
262                     !unseenFilterEnabled -> false
263                     // Don't apply filter if the keyguard isn't currently showing
264                     !keyguardRepository.isKeyguardShowing() -> false
265                     // Don't apply the filter if the notification is unseen
266                     unseenNotifications.contains(entry) -> false
267                     // Don't apply the filter to (non-promoted) group summaries
268                     //  - summary will be pruned if necessary, depending on if children are filtered
269                     entry.parent?.summary == entry -> false
270                     // Check that the entry satisfies certain characteristics that would bypass the
271                     // filter
272                     shouldIgnoreUnseenCheck(entry) -> false
273                     else -> true
274                 }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered }
275 
276             override fun onCleanup() {
277                 seenNotifsProvider.hasFilteredOutSeenNotifications = hasFilteredAnyNotifs
278                 hasFilteredAnyNotifs = false
279             }
280         }
281 
282     private val notifFilter: NotifFilter =
283         object : NotifFilter(TAG) {
284             override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
285                 keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
286         }
287 
288     private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean =
289         when {
290             entry.isMediaNotification -> true
291             entry.sbn.isOngoing -> true
292             else -> false
293         }
294 
295     // TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on
296     //  these same updates
297     private fun setupInvalidateNotifListCallbacks() {}
298 
299     private fun invalidateListFromFilter(reason: String) {
300         updateSectionHeadersVisibility()
301         notifFilter.invalidateList(reason)
302     }
303 
304     private fun updateSectionHeadersVisibility() {
305         val onKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD
306         val neverShowSections = sectionHeaderVisibilityProvider.neverShowSectionHeaders
307         val showSections = !onKeyguard && !neverShowSections
308         sectionHeaderVisibilityProvider.sectionHeadersVisible = showSections
309     }
310 
311     companion object {
312         private const val TAG = "KeyguardCoordinator"
313         private val SEEN_TIMEOUT = 5.seconds
314     }
315 }
316 
317 private val TransitionStep.isScreenTurningOff: Boolean get() =
318     transitionState == TransitionState.STARTED && to != KeyguardState.GONE