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