1 /* 2 * Copyright (C) 2023 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.interruption 18 19 import android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST 20 import android.app.Notification 21 import android.app.Notification.BubbleMetadata 22 import android.app.Notification.CATEGORY_EVENT 23 import android.app.Notification.CATEGORY_REMINDER 24 import android.app.Notification.VISIBILITY_PRIVATE 25 import android.app.NotificationManager.IMPORTANCE_DEFAULT 26 import android.app.NotificationManager.IMPORTANCE_HIGH 27 import android.content.pm.PackageManager 28 import android.content.pm.PackageManager.PERMISSION_GRANTED 29 import android.database.ContentObserver 30 import android.hardware.display.AmbientDisplayConfiguration 31 import android.os.Handler 32 import android.os.PowerManager 33 import android.provider.Settings 34 import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED 35 import android.provider.Settings.Global.HEADS_UP_OFF 36 import com.android.internal.logging.UiEvent 37 import com.android.internal.logging.UiEventLogger 38 import com.android.systemui.dagger.qualifiers.Main 39 import com.android.systemui.plugins.statusbar.StatusBarStateController 40 import com.android.systemui.settings.UserTracker 41 import com.android.systemui.statusbar.StatusBarState.SHADE 42 import com.android.systemui.statusbar.notification.collection.NotificationEntry 43 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS 44 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.HUN_SUPPRESSED_OLD_WHEN 45 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE 46 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK 47 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE 48 import com.android.systemui.statusbar.policy.BatteryController 49 import com.android.systemui.statusbar.policy.HeadsUpManager 50 import com.android.systemui.util.settings.GlobalSettings 51 import com.android.systemui.util.settings.SystemSettings 52 import com.android.systemui.util.time.SystemClock 53 import com.android.wm.shell.bubbles.Bubbles 54 import java.util.Optional 55 import kotlin.jvm.optionals.getOrElse 56 57 class PeekDisabledSuppressor( 58 private val globalSettings: GlobalSettings, 59 private val headsUpManager: HeadsUpManager, 60 private val logger: VisualInterruptionDecisionLogger, 61 @Main private val mainHandler: Handler, 62 ) : VisualInterruptionCondition(types = setOf(PEEK), reason = "peek disabled by global setting") { 63 private var isEnabled = false 64 shouldSuppressnull65 override fun shouldSuppress(): Boolean = !isEnabled 66 67 override fun start() { 68 val observer = 69 object : ContentObserver(mainHandler) { 70 override fun onChange(selfChange: Boolean) { 71 val wasEnabled = isEnabled 72 73 isEnabled = 74 globalSettings.getInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_OFF) != 75 HEADS_UP_OFF 76 77 // QQQ: Do we want to log this even if it hasn't changed? 78 logger.logHeadsUpFeatureChanged(isEnabled) 79 80 // QQQ: Is there a better place for this side effect? What if HeadsUpManager 81 // registered for it directly? 82 if (wasEnabled && !isEnabled) { 83 logger.logWillDismissAll() 84 headsUpManager.releaseAllImmediately() 85 } 86 } 87 } 88 89 globalSettings.registerContentObserverSync( 90 globalSettings.getUriFor(HEADS_UP_NOTIFICATIONS_ENABLED), 91 /* notifyForDescendants = */ true, 92 observer 93 ) 94 95 // QQQ: Do we need to register for SETTING_HEADS_UP_TICKER? It seems unused. 96 97 observer.onChange(/* selfChange= */ true) 98 } 99 } 100 101 class PulseDisabledSuppressor( 102 private val ambientDisplayConfiguration: AmbientDisplayConfiguration, 103 private val userTracker: UserTracker, 104 ) : VisualInterruptionCondition(types = setOf(PULSE), reason = "pulse disabled by user setting") { shouldSuppressnull105 override fun shouldSuppress(): Boolean = 106 !ambientDisplayConfiguration.pulseOnNotificationEnabled(userTracker.userId) 107 } 108 109 class PulseBatterySaverSuppressor(private val batteryController: BatteryController) : 110 VisualInterruptionCondition(types = setOf(PULSE), reason = "pulse disabled by battery saver") { 111 override fun shouldSuppress() = batteryController.isAodPowerSave() 112 } 113 114 class PeekPackageSnoozedSuppressor(private val headsUpManager: HeadsUpManager) : 115 VisualInterruptionFilter(types = setOf(PEEK), reason = "package snoozed") { shouldSuppressnull116 override fun shouldSuppress(entry: NotificationEntry) = 117 when { 118 // Assume any notification with an FSI is time-sensitive (like an alarm or incoming 119 // call) and ignore whether HUNs have been snoozed for the package. 120 entry.sbn.notification.fullScreenIntent != null -> false 121 122 // Otherwise, check if the package is snoozed. 123 else -> headsUpManager.isSnoozed(entry.sbn.packageName) 124 } 125 } 126 127 class PeekAlreadyBubbledSuppressor( 128 private val statusBarStateController: StatusBarStateController, 129 private val bubbles: Optional<Bubbles> 130 ) : VisualInterruptionFilter(types = setOf(PEEK), reason = "already bubbled") { shouldSuppressnull131 override fun shouldSuppress(entry: NotificationEntry) = 132 when { 133 statusBarStateController.state != SHADE -> false 134 else -> { 135 val bubblesCanShowNotification = 136 bubbles.map { it.canShowBubbleNotification() }.getOrElse { false } 137 entry.isBubble && bubblesCanShowNotification 138 } 139 } 140 } 141 142 class PeekDndSuppressor() : 143 VisualInterruptionFilter(types = setOf(PEEK), reason = "suppressed by DND") { shouldSuppressnull144 override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressPeek() 145 } 146 147 class PeekNotImportantSuppressor() : 148 VisualInterruptionFilter(types = setOf(PEEK), reason = "importance < HIGH") { 149 override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_HIGH 150 } 151 152 class PeekDeviceNotInUseSuppressor( 153 private val powerManager: PowerManager, 154 private val statusBarStateController: StatusBarStateController 155 ) : VisualInterruptionCondition(types = setOf(PEEK), reason = "device not in use") { shouldSuppressnull156 override fun shouldSuppress() = 157 when { 158 !powerManager.isScreenOn || statusBarStateController.isDreaming -> true 159 else -> false 160 } 161 } 162 163 class PeekOldWhenSuppressor(private val systemClock: SystemClock) : 164 VisualInterruptionFilter( 165 types = setOf(PEEK), 166 reason = "has old `when`", 167 uiEventId = HUN_SUPPRESSED_OLD_WHEN 168 ) { whenAgenull169 private fun whenAge(entry: NotificationEntry) = 170 systemClock.currentTimeMillis() - entry.sbn.notification.getWhen() 171 172 override fun shouldSuppress(entry: NotificationEntry): Boolean = 173 when { 174 // Ignore a "when" of 0, as it is unlikely to be a meaningful timestamp. 175 entry.sbn.notification.getWhen() <= 0L -> false 176 177 // Assume all HUNs with FSIs, foreground services, or user-initiated jobs are 178 // time-sensitive, regardless of their "when". 179 entry.sbn.notification.fullScreenIntent != null || 180 entry.sbn.notification.isForegroundService || 181 entry.sbn.notification.isUserInitiatedJob -> false 182 183 // Otherwise, check if the HUN's "when" is too old. 184 else -> whenAge(entry) >= MAX_HUN_WHEN_AGE_MS 185 } 186 } 187 188 class PulseEffectSuppressor() : 189 VisualInterruptionFilter(types = setOf(PULSE), reason = "suppressed by DND") { shouldSuppressnull190 override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressAmbient() 191 } 192 193 class PulseLockscreenVisibilityPrivateSuppressor() : 194 VisualInterruptionFilter( 195 types = setOf(PULSE), 196 reason = "hidden by lockscreen visibility override" 197 ) { 198 override fun shouldSuppress(entry: NotificationEntry) = 199 entry.ranking.lockscreenVisibilityOverride == VISIBILITY_PRIVATE 200 } 201 202 class PulseLowImportanceSuppressor() : 203 VisualInterruptionFilter(types = setOf(PULSE), reason = "importance < DEFAULT") { shouldSuppressnull204 override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_DEFAULT 205 } 206 207 class HunGroupAlertBehaviorSuppressor() : 208 VisualInterruptionFilter( 209 types = setOf(PEEK, PULSE), 210 reason = "suppressive group alert behavior" 211 ) { 212 override fun shouldSuppress(entry: NotificationEntry) = 213 entry.sbn.let { it.isGroup && it.notification.suppressAlertingDueToGrouping() } 214 } 215 216 class HunJustLaunchedFsiSuppressor() : 217 VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "just launched FSI") { shouldSuppressnull218 override fun shouldSuppress(entry: NotificationEntry) = entry.hasJustLaunchedFullScreenIntent() 219 } 220 221 class BubbleNotAllowedSuppressor() : 222 VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble") { 223 override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble() 224 } 225 226 class BubbleNoMetadataSuppressor() : 227 VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") { 228 isValidMetadatanull229 private fun isValidMetadata(metadata: BubbleMetadata?) = 230 metadata != null && (metadata.intent != null || metadata.shortcutId != null) 231 232 override fun shouldSuppress(entry: NotificationEntry) = !isValidMetadata(entry.bubbleMetadata) 233 } 234 235 class AlertAppSuspendedSuppressor : 236 VisualInterruptionFilter(types = setOf(PEEK, PULSE, BUBBLE), reason = "app is suspended") { 237 override fun shouldSuppress(entry: NotificationEntry) = entry.ranking.isSuspended 238 } 239 240 class AlertKeyguardVisibilitySuppressor( 241 private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider 242 ) : VisualInterruptionFilter(types = setOf(PEEK, PULSE, BUBBLE), reason = "hidden on keyguard") { shouldSuppressnull243 override fun shouldSuppress(entry: NotificationEntry) = 244 keyguardNotificationVisibilityProvider.shouldHideNotification(entry) 245 } 246 247 class AvalancheSuppressor( 248 private val avalancheProvider: AvalancheProvider, 249 private val systemClock: SystemClock, 250 private val systemSettings: SystemSettings, 251 private val packageManager: PackageManager, 252 private val uiEventLogger: UiEventLogger, 253 ) : 254 VisualInterruptionFilter( 255 types = setOf(PEEK, PULSE), 256 reason = "avalanche", 257 ) { 258 val TAG = "AvalancheSuppressor" 259 260 enum class State { 261 ALLOW_CONVERSATION_AFTER_AVALANCHE, 262 ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME, 263 ALLOW_CALLSTYLE, 264 ALLOW_CATEGORY_REMINDER, 265 ALLOW_CATEGORY_EVENT, 266 ALLOW_FSI_WITH_PERMISSION_ON, 267 ALLOW_COLORIZED, 268 ALLOW_EMERGENCY, 269 SUPPRESS 270 } 271 272 enum class AvalancheEvent(private val id: Int) : UiEventLogger.UiEventEnum { 273 @UiEvent(doc = "An avalanche event occurred, and a suppression period will start now.") 274 AVALANCHE_SUPPRESSOR_RECEIVED_TRIGGERING_EVENT(1824), 275 @UiEvent(doc = "HUN was suppressed in avalanche.") 276 AVALANCHE_SUPPRESSOR_HUN_SUPPRESSED(1825), 277 @UiEvent(doc = "HUN allowed during avalanche because conversation newer than the trigger.") 278 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_NEW_CONVERSATION(1826), 279 @UiEvent(doc = "HUN allowed during avalanche because it is a priority conversation.") 280 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_PRIORITY_CONVERSATION(1827), 281 @UiEvent(doc = "HUN allowed during avalanche because it is a CallStyle notification.") 282 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CALL_STYLE(1828), 283 @UiEvent(doc = "HUN allowed during avalanche because it is a reminder notification.") 284 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_REMINDER(1829), 285 @UiEvent(doc = "HUN allowed during avalanche because it is a calendar notification.") 286 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_EVENT(1830), 287 @UiEvent(doc = "HUN allowed during avalanche because it has FSI.") 288 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_FSI_WITH_PERMISSION(1831), 289 @UiEvent(doc = "HUN allowed during avalanche because it is colorized.") 290 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED(1832), 291 @UiEvent(doc = "HUN allowed during avalanche because it is an emergency notification.") 292 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY(1833); 293 294 override fun getId(): Int { 295 return id 296 } 297 } 298 299 override fun shouldSuppress(entry: NotificationEntry): Boolean { 300 if (!isCooldownEnabled()) { 301 return false 302 } 303 val timeSinceAvalancheMs = systemClock.currentTimeMillis() - avalancheProvider.startTime 304 val timedOut = timeSinceAvalancheMs >= avalancheProvider.timeoutMs 305 if (timedOut) { 306 return false 307 } 308 val state = calculateState(entry) 309 if (state != State.SUPPRESS) { 310 return false 311 } 312 return true 313 } 314 315 private fun calculateState(entry: NotificationEntry): State { 316 if ( 317 entry.ranking.isConversation && 318 entry.sbn.notification.getWhen() > avalancheProvider.startTime 319 ) { 320 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_NEW_CONVERSATION) 321 return State.ALLOW_CONVERSATION_AFTER_AVALANCHE 322 } 323 324 if (entry.channel?.isImportantConversation == true) { 325 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_PRIORITY_CONVERSATION) 326 return State.ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME 327 } 328 329 if (entry.sbn.notification.isStyle(Notification.CallStyle::class.java)) { 330 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CALL_STYLE) 331 return State.ALLOW_CALLSTYLE 332 } 333 334 if (entry.sbn.notification.category == CATEGORY_REMINDER) { 335 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_REMINDER) 336 return State.ALLOW_CATEGORY_REMINDER 337 } 338 339 if (entry.sbn.notification.category == CATEGORY_EVENT) { 340 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_EVENT) 341 return State.ALLOW_CATEGORY_EVENT 342 } 343 344 if (entry.sbn.notification.fullScreenIntent != null) { 345 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_FSI_WITH_PERMISSION) 346 return State.ALLOW_FSI_WITH_PERMISSION_ON 347 } 348 if (entry.sbn.notification.isColorized) { 349 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED) 350 return State.ALLOW_COLORIZED 351 } 352 if ( 353 packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) == 354 PERMISSION_GRANTED 355 ) { 356 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY) 357 return State.ALLOW_EMERGENCY 358 } 359 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_SUPPRESSED) 360 return State.SUPPRESS 361 } 362 363 private fun isCooldownEnabled(): Boolean { 364 return systemSettings.getInt(Settings.System.NOTIFICATION_COOLDOWN_ENABLED, /* def */ 1) == 365 1 366 } 367 } 368