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.statusbar.notification.collection.coordinator 18 19 import android.annotation.SuppressLint 20 import androidx.annotation.VisibleForTesting 21 import com.android.app.tracing.coroutines.launchTraced as launch 22 import com.android.systemui.Dumpable 23 import com.android.systemui.dagger.qualifiers.Application 24 import com.android.systemui.dump.DumpManager 25 import com.android.systemui.keyguard.data.repository.KeyguardRepository 26 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 27 import com.android.systemui.keyguard.shared.model.KeyguardState 28 import com.android.systemui.plugins.statusbar.StatusBarStateController 29 import com.android.systemui.scene.domain.interactor.SceneInteractor 30 import com.android.systemui.scene.shared.flag.SceneContainerFlag 31 import com.android.systemui.scene.shared.model.Scenes 32 import com.android.systemui.statusbar.expansionChanges 33 import com.android.systemui.statusbar.notification.collection.GroupEntry 34 import com.android.systemui.statusbar.notification.collection.NotifPipeline 35 import com.android.systemui.statusbar.notification.collection.NotificationEntry 36 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope 37 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter 38 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener 39 import com.android.systemui.statusbar.notification.collection.notifcollection.UpdateSource 40 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor 41 import com.android.systemui.statusbar.notification.headsup.HeadsUpManager 42 import com.android.systemui.statusbar.notification.headsup.headsUpEvents 43 import com.android.systemui.util.asIndenting 44 import com.android.systemui.util.indentIfPossible 45 import java.io.PrintWriter 46 import javax.inject.Inject 47 import kotlin.time.Duration.Companion.seconds 48 import kotlinx.coroutines.CoroutineScope 49 import kotlinx.coroutines.Job 50 import kotlinx.coroutines.coroutineScope 51 import kotlinx.coroutines.delay 52 import kotlinx.coroutines.flow.Flow 53 import kotlinx.coroutines.flow.MutableSharedFlow 54 import kotlinx.coroutines.flow.collectLatest 55 import kotlinx.coroutines.flow.distinctUntilChanged 56 import kotlinx.coroutines.flow.map 57 import kotlinx.coroutines.flow.onEach 58 import kotlinx.coroutines.yield 59 60 /** 61 * If the setting is enabled, this will track and hide seen notifications on the lockscreen. 62 * 63 * This is the "original" unseen keyguard coordinator because this is the logic originally developed 64 * for large screen devices where showing "seen" notifications on the lock screen was distracting. 65 * Moreover, this file was created during a project that will replace this logic, so the 66 * [LockScreenMinimalismCoordinator] is the expected replacement of this file. 67 */ 68 @CoordinatorScope 69 @SuppressLint("SharedFlowCreation") 70 class OriginalUnseenKeyguardCoordinator 71 @Inject 72 constructor( 73 private val dumpManager: DumpManager, 74 private val headsUpManager: HeadsUpManager, 75 private val keyguardRepository: KeyguardRepository, 76 private val keyguardTransitionInteractor: KeyguardTransitionInteractor, 77 private val logger: KeyguardCoordinatorLogger, 78 @Application private val scope: CoroutineScope, 79 private val seenNotificationsInteractor: SeenNotificationsInteractor, 80 private val statusBarStateController: StatusBarStateController, 81 private val sceneInteractor: SceneInteractor, 82 ) : Coordinator, Dumpable { 83 84 private val unseenNotifications = mutableSetOf<NotificationEntry>() 85 private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) 86 private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) 87 private var unseenFilterEnabled = false 88 89 override fun attach(pipeline: NotifPipeline) { 90 pipeline.addFinalizeFilter(unseenNotifFilter) 91 pipeline.addCollectionListener(collectionListener) 92 scope.launch { trackUnseenFilterSettingChanges() } 93 dumpManager.registerDumpable(this) 94 } 95 96 private suspend fun trackSeenNotifications() { 97 // Whether or not keyguard is visible (or occluded). 98 @Suppress("DEPRECATION") 99 val isKeyguardPresentFlow: Flow<Boolean> = 100 if (SceneContainerFlag.isEnabled) { 101 sceneInteractor.transitionState.map { 102 !it.isTransitioning(to = Scenes.Gone) && !it.isIdle(Scenes.Gone) 103 } 104 } else { 105 keyguardTransitionInteractor.transitions.map { step -> 106 step.to != KeyguardState.GONE 107 } 108 } 109 .distinctUntilChanged() 110 .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } 111 112 // Separately track seen notifications while the device is locked, applying once the device 113 // is unlocked. 114 val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>() 115 116 // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes. 117 isKeyguardPresentFlow.collectLatest { isKeyguardPresent: Boolean -> 118 if (isKeyguardPresent) { 119 // Keyguard is not gone, notifications need to be visible for a certain threshold 120 // before being marked as seen 121 trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked) 122 } else { 123 // Mark all seen-while-locked notifications as seen for real. 124 if (notificationsSeenWhileLocked.isNotEmpty()) { 125 unseenNotifications.removeAll(notificationsSeenWhileLocked) 126 logger.logAllMarkedSeenOnUnlock( 127 seenCount = notificationsSeenWhileLocked.size, 128 remainingUnseenCount = unseenNotifications.size, 129 ) 130 notificationsSeenWhileLocked.clear() 131 } 132 unseenNotifFilter.invalidateList("keyguard no longer showing") 133 // Keyguard is gone, notifications can be immediately marked as seen when they 134 // become visible. 135 trackSeenNotificationsWhileUnlocked() 136 } 137 } 138 } 139 140 /** 141 * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually 142 * been "seen" while the device is on the keyguard. 143 */ 144 private suspend fun trackSeenNotificationsWhileLocked( 145 notificationsSeenWhileLocked: MutableSet<NotificationEntry> 146 ) = coroutineScope { 147 // Remove removed notifications from the set 148 launch { 149 unseenEntryRemoved.collect { entry -> 150 if (notificationsSeenWhileLocked.remove(entry)) { 151 logger.logRemoveSeenOnLockscreen(entry) 152 } 153 } 154 } 155 // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and 156 // is restarted when doze ends. 157 keyguardRepository.isDozing.collectLatest { isDozing -> 158 if (!isDozing) { 159 trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked) 160 } 161 } 162 } 163 164 /** 165 * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually 166 * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen 167 * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration. 168 */ 169 private suspend fun trackSeenNotificationsWhileLockedAndNotDozing( 170 notificationsSeenWhileLocked: MutableSet<NotificationEntry> 171 ) = coroutineScope { 172 // All child tracking jobs will be cancelled automatically when this is cancelled. 173 val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>() 174 175 /** 176 * Wait for the user to spend enough time on the lock screen before removing notification 177 * from unseen set upon unlock. 178 */ 179 suspend fun trackSeenDurationThreshold(entry: NotificationEntry) { 180 if (notificationsSeenWhileLocked.remove(entry)) { 181 logger.logResetSeenOnLockscreen(entry) 182 } 183 delay(SEEN_TIMEOUT) 184 notificationsSeenWhileLocked.add(entry) 185 trackingJobsByEntry.remove(entry) 186 logger.logSeenOnLockscreen(entry) 187 } 188 189 /** Stop any unseen tracking when a notification is removed. */ 190 suspend fun stopTrackingRemovedNotifs(): Nothing = 191 unseenEntryRemoved.collect { entry -> 192 trackingJobsByEntry.remove(entry)?.let { 193 it.cancel() 194 logger.logStopTrackingLockscreenSeenDuration(entry) 195 } 196 } 197 198 /** Start tracking new notifications when they are posted. */ 199 suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope { 200 unseenEntryAdded.collect { entry -> 201 logger.logTrackingLockscreenSeenDuration(entry) 202 // If this is an update, reset the tracking. 203 trackingJobsByEntry[entry]?.let { 204 it.cancel() 205 logger.logResetSeenOnLockscreen(entry) 206 } 207 trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } 208 } 209 } 210 211 // Start tracking for all notifications that are currently unseen. 212 logger.logTrackingLockscreenSeenDuration(unseenNotifications) 213 unseenNotifications.forEach { entry -> 214 trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } 215 } 216 217 launch { trackNewUnseenNotifs() } 218 launch { stopTrackingRemovedNotifs() } 219 } 220 221 // Track "seen" notifications, marking them as such when either shade is expanded or the 222 // notification becomes heads up. 223 private suspend fun trackSeenNotificationsWhileUnlocked() { 224 coroutineScope { 225 launch { clearUnseenNotificationsWhenShadeIsExpanded() } 226 launch { markHeadsUpNotificationsAsSeen() } 227 } 228 } 229 230 private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { 231 statusBarStateController.expansionChanges.collectLatest { isExpanded -> 232 // Give keyguard events time to propagate, in case this expansion is part of the 233 // keyguard transition and not the user expanding the shade 234 yield() 235 if (isExpanded) { 236 logger.logShadeExpanded() 237 unseenNotifications.clear() 238 } 239 } 240 } 241 242 private suspend fun markHeadsUpNotificationsAsSeen() { 243 headsUpManager.allEntries 244 .filter { it.isRowPinned } 245 .forEach { unseenNotifications.remove(it) } 246 headsUpManager.headsUpEvents.collect { (entry, isHun) -> 247 if (isHun) { 248 logger.logUnseenHun(entry.key) 249 unseenNotifications.remove(entry) 250 } 251 } 252 } 253 254 private fun unseenFeatureEnabled(): Flow<Boolean> { 255 return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled() 256 } 257 258 private suspend fun trackUnseenFilterSettingChanges() { 259 unseenFeatureEnabled().collectLatest { setting -> 260 // update local field and invalidate if necessary 261 if (setting != unseenFilterEnabled) { 262 unseenFilterEnabled = setting 263 unseenNotifFilter.invalidateList("unseen setting changed") 264 } 265 // if the setting is enabled, then start tracking and filtering unseen notifications 266 if (setting) { 267 trackSeenNotifications() 268 } 269 } 270 } 271 272 private val collectionListener = 273 object : NotifCollectionListener { 274 override fun onEntryAdded(entry: NotificationEntry) { 275 if ( 276 keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded 277 ) { 278 logger.logUnseenAdded(entry.key, entry.sbn.postTime) 279 unseenNotifications.add(entry) 280 unseenEntryAdded.tryEmit(entry) 281 } 282 } 283 284 override fun onEntryUpdated(entry: NotificationEntry, source: UpdateSource) { 285 if ( 286 keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded 287 ) { 288 logger.logUnseenUpdated(entry.key, source, entry.sbn.postTime) 289 // We are not marking a notif as unseen if it's updated by the SystemServer 290 // (for example, auto-grouping), or the SystemUi, not the App. 291 if (source != UpdateSource.App) { 292 return 293 } 294 unseenNotifications.add(entry) 295 unseenEntryAdded.tryEmit(entry) 296 } 297 } 298 299 override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { 300 if (unseenNotifications.remove(entry)) { 301 logger.logUnseenRemoved(entry.key) 302 unseenEntryRemoved.tryEmit(entry) 303 } 304 } 305 } 306 307 @VisibleForTesting 308 val unseenNotifFilter = 309 object : NotifFilter(TAG) { 310 311 var hasFilteredAnyNotifs = false 312 313 override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = 314 when { 315 // Don't apply filter if the setting is disabled 316 !unseenFilterEnabled -> false 317 // Don't apply filter if the keyguard isn't currently showing 318 !keyguardRepository.isKeyguardShowing() -> false 319 // Don't apply the filter if the notification is unseen 320 unseenNotifications.contains(entry) -> false 321 // Don't apply the filter to (non-promoted) group summaries 322 // - summary will be pruned if necessary, depending on if children are filtered 323 (entry.parent as? GroupEntry)?.summary == entry -> false 324 // Check that the entry satisfies certain characteristics that would bypass the 325 // filter 326 shouldIgnoreUnseenCheck(entry) -> false 327 else -> true 328 }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered } 329 330 override fun onCleanup() { 331 logger.logProviderHasFilteredOutSeenNotifs(hasFilteredAnyNotifs) 332 seenNotificationsInteractor.setHasFilteredOutSeenNotifications(hasFilteredAnyNotifs) 333 hasFilteredAnyNotifs = false 334 } 335 } 336 337 private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean = 338 when { 339 entry.isMediaNotification -> true 340 entry.sbn.isOngoing -> true 341 else -> false 342 } 343 344 override fun dump(pw: PrintWriter, args: Array<out String>) = 345 with(pw.asIndenting()) { 346 println( 347 "notificationListInteractor.hasFilteredOutSeenNotifications.value=" + 348 seenNotificationsInteractor.hasFilteredOutSeenNotifications.value 349 ) 350 println("unseen notifications:") 351 indentIfPossible { 352 for (notification in unseenNotifications) { 353 println(notification.key) 354 } 355 } 356 } 357 358 companion object { 359 private const val TAG = "OriginalUnseenKeyguardCoordinator" 360 private val SEEN_TIMEOUT = 5.seconds 361 } 362 } 363