1 /* <lambda>null2 * 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.smartspace.SmartspaceTarget 20 import android.os.Parcelable 21 import com.android.systemui.dagger.qualifiers.Main 22 import com.android.systemui.plugins.statusbar.StatusBarStateController 23 import com.android.systemui.statusbar.StatusBarState 24 import com.android.systemui.statusbar.SysuiStatusBarStateController 25 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController 26 import com.android.systemui.statusbar.notification.collection.NotifPipeline 27 import com.android.systemui.statusbar.notification.collection.NotificationEntry 28 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope 29 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter 30 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener 31 import com.android.systemui.statusbar.notification.logKey 32 import com.android.systemui.util.concurrency.DelayableExecutor 33 import com.android.systemui.util.time.SystemClock 34 import java.util.concurrent.TimeUnit.SECONDS 35 import javax.inject.Inject 36 37 /** 38 * Hides notifications on the lockscreen if the content of those notifications is also visible 39 * in smartspace. This ONLY hides the notifications on the lockscreen: if the user pulls the shade 40 * down or unlocks the device, then the notifications are unhidden. 41 * 42 * In addition, notifications that have recently alerted aren't filtered. Tracking this in a way 43 * that involves the fewest pipeline invalidations requires some unfortunately complex logic. 44 */ 45 @CoordinatorScope 46 class SmartspaceDedupingCoordinator @Inject constructor( 47 private val statusBarStateController: SysuiStatusBarStateController, 48 private val smartspaceController: LockscreenSmartspaceController, 49 private val notifPipeline: NotifPipeline, 50 @Main private val executor: DelayableExecutor, 51 private val clock: SystemClock 52 ) : Coordinator { 53 private var isOnLockscreen = false 54 55 private var trackedSmartspaceTargets = mutableMapOf<String, TrackedSmartspaceTarget>() 56 57 override fun attach(pipeline: NotifPipeline) { 58 pipeline.addPreGroupFilter(filter) 59 pipeline.addCollectionListener(collectionListener) 60 statusBarStateController.addCallback(statusBarStateListener) 61 smartspaceController.addListener(this::onNewSmartspaceTargets) 62 63 recordStatusBarState(statusBarStateController.state) 64 } 65 66 private fun isDupedWithSmartspaceContent(entry: NotificationEntry): Boolean { 67 return trackedSmartspaceTargets[entry.key]?.shouldFilter ?: false 68 } 69 70 private val filter = object : NotifFilter("SmartspaceDedupingFilter") { 71 override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean { 72 return isOnLockscreen && isDupedWithSmartspaceContent(entry) 73 } 74 } 75 76 private val collectionListener = object : NotifCollectionListener { 77 override fun onEntryAdded(entry: NotificationEntry) { 78 trackedSmartspaceTargets[entry.key]?.let { 79 updateFilterStatus(it) 80 } 81 } 82 83 override fun onEntryUpdated(entry: NotificationEntry) { 84 trackedSmartspaceTargets[entry.key]?.let { 85 updateFilterStatus(it) 86 } 87 } 88 89 override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { 90 trackedSmartspaceTargets[entry.key]?.let { trackedTarget -> 91 cancelExceptionTimeout(trackedTarget) 92 } 93 } 94 } 95 96 private val statusBarStateListener = object : StatusBarStateController.StateListener { 97 override fun onStateChanged(newState: Int) { 98 recordStatusBarState(newState) 99 } 100 } 101 102 private fun onNewSmartspaceTargets(targets: List<Parcelable>) { 103 var changed = false 104 val newMap = mutableMapOf<String, TrackedSmartspaceTarget>() 105 val oldMap = trackedSmartspaceTargets 106 107 for (target in targets) { 108 // For all targets that are SmartspaceTargets and have non-null sourceNotificationKeys 109 (target as? SmartspaceTarget)?.sourceNotificationKey?.let { key -> 110 val trackedTarget = oldMap.getOrElse(key) { 111 TrackedSmartspaceTarget(key) 112 } 113 newMap[key] = trackedTarget 114 changed = changed || updateFilterStatus(trackedTarget) 115 } 116 // Currently, only filter out the first target 117 break 118 } 119 120 for (prevKey in oldMap.keys) { 121 if (!newMap.containsKey(prevKey)) { 122 oldMap[prevKey]?.cancelTimeoutRunnable?.run() 123 changed = true 124 } 125 } 126 127 if (changed) { 128 filter.invalidateList("onNewSmartspaceTargets") 129 } 130 131 trackedSmartspaceTargets = newMap 132 } 133 134 /** 135 * Returns true if the target's alert exception status has changed 136 */ 137 private fun updateFilterStatus(target: TrackedSmartspaceTarget): Boolean { 138 val prevShouldFilter = target.shouldFilter 139 140 val entry = notifPipeline.getEntry(target.key) 141 if (entry != null) { 142 updateAlertException(target, entry) 143 144 target.shouldFilter = !hasRecentlyAlerted(entry) 145 } 146 147 return target.shouldFilter != prevShouldFilter && isOnLockscreen 148 } 149 150 private fun updateAlertException(target: TrackedSmartspaceTarget, entry: NotificationEntry) { 151 val now = clock.currentTimeMillis() 152 val alertExceptionExpires = entry.ranking.lastAudiblyAlertedMillis + ALERT_WINDOW 153 154 if (alertExceptionExpires != target.alertExceptionExpires && 155 alertExceptionExpires > now) { 156 // If we got here, the target is subject to a new alert exception window, so we 157 // should update our timeout to fire at the end of the new window 158 159 target.cancelTimeoutRunnable?.run() 160 target.alertExceptionExpires = alertExceptionExpires 161 target.cancelTimeoutRunnable = executor.executeDelayed({ 162 target.cancelTimeoutRunnable = null 163 target.shouldFilter = true 164 filter.invalidateList("updateAlertException: ${entry.logKey}") 165 }, alertExceptionExpires - now) 166 } 167 } 168 169 private fun cancelExceptionTimeout(target: TrackedSmartspaceTarget) { 170 target.cancelTimeoutRunnable?.run() 171 target.cancelTimeoutRunnable = null 172 target.alertExceptionExpires = 0 173 } 174 175 private fun recordStatusBarState(newState: Int) { 176 val wasOnLockscreen = isOnLockscreen 177 isOnLockscreen = newState == StatusBarState.KEYGUARD 178 179 if (isOnLockscreen != wasOnLockscreen) { 180 filter.invalidateList("recordStatusBarState: " + StatusBarState.toString(newState)) 181 // No need to call notificationEntryManager.updateNotifications; something else already 182 // does it for us when the keyguard state changes 183 } 184 } 185 186 private fun hasRecentlyAlerted(entry: NotificationEntry): Boolean { 187 return clock.currentTimeMillis() - entry.ranking.lastAudiblyAlertedMillis <= ALERT_WINDOW 188 } 189 } 190 191 private class TrackedSmartspaceTarget( 192 val key: String 193 ) { 194 var cancelTimeoutRunnable: Runnable? = null 195 var alertExceptionExpires: Long = 0 196 var shouldFilter = false 197 } 198 199 private val ALERT_WINDOW = SECONDS.toMillis(30) 200