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