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_ALARM 23 import android.app.Notification.CATEGORY_CAR_EMERGENCY 24 import android.app.Notification.CATEGORY_CAR_WARNING 25 import android.app.Notification.CATEGORY_EVENT 26 import android.app.Notification.CATEGORY_REMINDER 27 import android.app.Notification.VISIBILITY_PRIVATE 28 import android.app.NotificationManager 29 import android.app.NotificationManager.IMPORTANCE_DEFAULT 30 import android.app.NotificationManager.IMPORTANCE_HIGH 31 import android.app.PendingIntent 32 import android.content.Context 33 import android.content.Intent 34 import android.content.pm.PackageManager 35 import android.content.pm.PackageManager.PERMISSION_GRANTED 36 import android.database.ContentObserver 37 import android.hardware.display.AmbientDisplayConfiguration 38 import android.os.Bundle 39 import android.os.Handler 40 import android.os.PowerManager 41 import android.os.SystemProperties 42 import android.provider.Settings 43 import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED 44 import android.provider.Settings.Global.HEADS_UP_OFF 45 import android.service.notification.Flags 46 import com.android.internal.logging.UiEvent 47 import com.android.internal.logging.UiEventLogger 48 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage 49 import com.android.systemui.dagger.qualifiers.Main 50 import com.android.systemui.plugins.statusbar.StatusBarStateController 51 import com.android.systemui.settings.UserTracker 52 import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor 53 import com.android.systemui.statusbar.StatusBarState.SHADE 54 import com.android.systemui.statusbar.notification.collection.NotificationEntry 55 import com.android.systemui.statusbar.notification.headsup.HeadsUpManager 56 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS 57 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.HUN_SUPPRESSED_OLD_WHEN 58 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE 59 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK 60 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE 61 import com.android.systemui.statusbar.policy.BatteryController 62 import com.android.systemui.util.NotificationChannels 63 import com.android.systemui.util.settings.GlobalSettings 64 import com.android.systemui.util.settings.SystemSettings 65 import com.android.systemui.util.time.SystemClock 66 import com.android.wm.shell.bubbles.Bubbles 67 import java.util.Optional 68 import kotlin.jvm.optionals.getOrElse 69 70 class PeekDisabledSuppressor( 71 private val globalSettings: GlobalSettings, 72 private val headsUpManager: HeadsUpManager, 73 private val logger: VisualInterruptionDecisionLogger, 74 @Main private val mainHandler: Handler, 75 ) : VisualInterruptionCondition(types = setOf(PEEK), reason = "peek disabled by global setting") { 76 private var isEnabled = false 77 shouldSuppressnull78 override fun shouldSuppress(): Boolean = !isEnabled 79 80 override fun start() { 81 val observer = 82 object : ContentObserver(mainHandler) { 83 override fun onChange(selfChange: Boolean) { 84 val wasEnabled = isEnabled 85 86 isEnabled = 87 globalSettings.getInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_OFF) != 88 HEADS_UP_OFF 89 90 // QQQ: Do we want to log this even if it hasn't changed? 91 logger.logHeadsUpFeatureChanged(isEnabled) 92 93 // QQQ: Is there a better place for this side effect? What if HeadsUpManager 94 // registered for it directly? 95 if (wasEnabled && !isEnabled) { 96 logger.logWillDismissAll() 97 headsUpManager.releaseAllImmediately() 98 } 99 } 100 } 101 102 globalSettings.registerContentObserverSync( 103 globalSettings.getUriFor(HEADS_UP_NOTIFICATIONS_ENABLED), 104 /* notifyForDescendants = */ true, 105 observer, 106 ) 107 108 // QQQ: Do we need to register for SETTING_HEADS_UP_TICKER? It seems unused. 109 110 observer.onChange(/* selfChange= */ true) 111 } 112 } 113 114 class PulseDisabledSuppressor( 115 private val ambientDisplayConfiguration: AmbientDisplayConfiguration, 116 private val userTracker: UserTracker, 117 ) : VisualInterruptionCondition(types = setOf(PULSE), reason = "pulse disabled by user setting") { shouldSuppressnull118 override fun shouldSuppress(): Boolean = 119 !ambientDisplayConfiguration.pulseOnNotificationEnabled(userTracker.userId) 120 } 121 122 class PulseBatterySaverSuppressor(private val batteryController: BatteryController) : 123 VisualInterruptionCondition(types = setOf(PULSE), reason = "pulse disabled by battery saver") { 124 override fun shouldSuppress() = batteryController.isAodPowerSave() 125 } 126 127 class PeekPackageSnoozedSuppressor(private val headsUpManager: HeadsUpManager) : 128 VisualInterruptionFilter(types = setOf(PEEK), reason = "package snoozed") { shouldSuppressnull129 override fun shouldSuppress(entry: NotificationEntry) = 130 when { 131 // Assume any notification with an FSI is time-sensitive (like an alarm or incoming 132 // call) and ignore whether HUNs have been snoozed for the package. 133 entry.sbn.notification.fullScreenIntent != null -> false 134 135 // Otherwise, check if the package is snoozed. 136 else -> headsUpManager.isSnoozed(entry.sbn.packageName) 137 } 138 } 139 140 class PeekAlreadyBubbledSuppressor( 141 private val statusBarStateController: StatusBarStateController, 142 private val bubbles: Optional<Bubbles>, 143 ) : VisualInterruptionFilter(types = setOf(PEEK), reason = "already bubbled") { shouldSuppressnull144 override fun shouldSuppress(entry: NotificationEntry) = 145 when { 146 statusBarStateController.state != SHADE -> false 147 else -> { 148 val bubblesCanShowNotification = 149 bubbles.map { it.canShowBubbleNotification() }.getOrElse { false } 150 entry.isBubble && bubblesCanShowNotification 151 } 152 } 153 } 154 155 class PeekDndSuppressor : 156 VisualInterruptionFilter(types = setOf(PEEK), reason = "suppressed by DND") { shouldSuppressnull157 override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressPeek() 158 } 159 160 class PeekNotImportantSuppressor : 161 VisualInterruptionFilter(types = setOf(PEEK), reason = "importance < HIGH") { 162 override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_HIGH 163 } 164 165 class PeekDeviceNotInUseSuppressor( 166 private val powerManager: PowerManager, 167 private val statusBarStateController: StatusBarStateController, 168 ) : VisualInterruptionCondition(types = setOf(PEEK), reason = "device not in use") { shouldSuppressnull169 override fun shouldSuppress() = 170 when { 171 !powerManager.isScreenOn || statusBarStateController.isDreaming -> true 172 else -> false 173 } 174 } 175 176 class PeekOldWhenSuppressor(private val systemClock: SystemClock) : 177 VisualInterruptionFilter( 178 types = setOf(PEEK), 179 reason = "has old `when`", 180 uiEventId = HUN_SUPPRESSED_OLD_WHEN, 181 ) { whenAgenull182 private fun whenAge(entry: NotificationEntry) = 183 systemClock.currentTimeMillis() - entry.sbn.notification.getWhen() 184 185 override fun shouldSuppress(entry: NotificationEntry): Boolean = 186 when { 187 // Ignore a "when" of 0, as it is unlikely to be a meaningful timestamp. 188 entry.sbn.notification.getWhen() <= 0L -> false 189 190 // Assume all HUNs with FSIs, foreground services, or user-initiated jobs are 191 // time-sensitive, regardless of their "when". 192 entry.sbn.notification.fullScreenIntent != null || 193 entry.sbn.notification.isForegroundService || 194 entry.sbn.notification.isUserInitiatedJob -> false 195 196 // Otherwise, check if the HUN's "when" is too old. 197 else -> whenAge(entry) >= MAX_HUN_WHEN_AGE_MS 198 } 199 } 200 201 class PulseEffectSuppressor : 202 VisualInterruptionFilter(types = setOf(PULSE), reason = "suppressed by DND") { shouldSuppressnull203 override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressAmbient() 204 } 205 206 class PulseLockscreenVisibilityPrivateSuppressor : 207 VisualInterruptionFilter( 208 types = setOf(PULSE), 209 reason = "hidden by lockscreen visibility override", 210 ) { 211 override fun shouldSuppress(entry: NotificationEntry) = 212 entry.ranking.lockscreenVisibilityOverride == VISIBILITY_PRIVATE 213 } 214 215 class PulseLowImportanceSuppressor : 216 VisualInterruptionFilter(types = setOf(PULSE), reason = "importance < DEFAULT") { shouldSuppressnull217 override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_DEFAULT 218 } 219 220 class HunGroupAlertBehaviorSuppressor : 221 VisualInterruptionFilter( 222 types = setOf(PEEK, PULSE), 223 reason = "suppressive group alert behavior", 224 ) { 225 override fun shouldSuppress(entry: NotificationEntry) = 226 entry.sbn.let { it.isGroup && it.notification.suppressAlertingDueToGrouping() } 227 } 228 229 class HunSilentNotificationSuppressor : 230 VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "notification isSilent") { shouldSuppressnull231 override fun shouldSuppress(entry: NotificationEntry) = 232 entry.sbn.let { Flags.notificationSilentFlag() && it.notification.isSilent } 233 } 234 235 class HunJustLaunchedFsiSuppressor : 236 VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "just launched FSI") { shouldSuppressnull237 override fun shouldSuppress(entry: NotificationEntry) = entry.hasJustLaunchedFullScreenIntent() 238 } 239 240 class BubbleNotAllowedSuppressor : 241 VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble", isSpammy = true) { 242 override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble() 243 } 244 245 class BubbleNoMetadataSuppressor : 246 VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") { 247 isValidMetadatanull248 private fun isValidMetadata(metadata: BubbleMetadata?) = 249 metadata != null && (metadata.intent != null || metadata.shortcutId != null) 250 251 override fun shouldSuppress(entry: NotificationEntry) = !isValidMetadata(entry.bubbleMetadata) 252 } 253 254 class AlertAppSuspendedSuppressor : 255 VisualInterruptionFilter(types = setOf(PEEK, PULSE, BUBBLE), reason = "app is suspended") { 256 override fun shouldSuppress(entry: NotificationEntry) = entry.ranking.isSuspended 257 } 258 259 class AlertKeyguardVisibilitySuppressor( 260 private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider 261 ) : VisualInterruptionFilter(types = setOf(PEEK, PULSE, BUBBLE), reason = "hidden on keyguard") { shouldSuppressnull262 override fun shouldSuppress(entry: NotificationEntry) = 263 keyguardNotificationVisibilityProvider.shouldHideNotification(entry) 264 } 265 266 /** 267 * Set with: 268 * 269 * adb shell setprop persist.force_show_avalanche_edu_once 1 && adb shell stop; adb shell start 270 */ 271 private const val FORCE_SHOW_AVALANCHE_EDU_ONCE = "persist.force_show_avalanche_edu_once" 272 273 private const val PREF_HAS_SEEN_AVALANCHE_EDU = "has_seen_avalanche_edu" 274 275 class AvalancheSuppressor( 276 private val avalancheProvider: AvalancheProvider, 277 private val systemClock: SystemClock, 278 private val settingsInteractor: NotificationSettingsInteractor, 279 private val packageManager: PackageManager, 280 private val uiEventLogger: UiEventLogger, 281 private val context: Context, 282 private val notificationManager: NotificationManager, 283 private val systemSettings: SystemSettings, 284 ) : VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "avalanche") { 285 val TAG = "AvalancheSuppressor" 286 287 private val prefs = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) 288 289 // SharedPreferences are persisted across reboots 290 var hasSeenEdu: Boolean 291 get() = prefs.getBoolean(PREF_HAS_SEEN_AVALANCHE_EDU, false) 292 set(value) = prefs.edit().putBoolean(PREF_HAS_SEEN_AVALANCHE_EDU, value).apply() 293 294 // Reset on reboot. 295 // The pipeline runs these suppressors many times very fast, so we must use a separate bool 296 // to force show for debug so that phone does not get stuck sending out infinite number of 297 // education HUNs. 298 private var hasShownOnceForDebug = false 299 300 // Sometimes the kotlin flow value is false even when the cooldown setting is true (b/356768397) 301 // so let's directly check settings until we confirm that the flow is initialized and in sync 302 // with the real settings value. 303 private var isCooldownFlowInSync = false 304 305 private fun shouldShowEdu(): Boolean { 306 val forceShowOnce = SystemProperties.get(FORCE_SHOW_AVALANCHE_EDU_ONCE, "").equals("1") 307 return !hasSeenEdu || (forceShowOnce && !hasShownOnceForDebug) 308 } 309 310 enum class State { 311 ALLOW_CONVERSATION_AFTER_AVALANCHE, 312 ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME, 313 ALLOW_CALLSTYLE, 314 ALLOW_CATEGORY_REMINDER, 315 ALLOW_CATEGORY_EVENT, 316 ALLOW_CATEGORY_ALARM, 317 ALLOW_CATEGORY_CAR_EMERGENCY, 318 ALLOW_CATEGORY_CAR_WARNING, 319 ALLOW_FSI_WITH_PERMISSION_ON, 320 ALLOW_COLORIZED, 321 ALLOW_EMERGENCY, 322 SUPPRESS, 323 } 324 325 enum class AvalancheEvent(private val id: Int) : UiEventLogger.UiEventEnum { 326 @UiEvent(doc = "An avalanche event occurred, and a suppression period will start now.") 327 AVALANCHE_SUPPRESSOR_RECEIVED_TRIGGERING_EVENT(1824), 328 @UiEvent(doc = "HUN was suppressed in avalanche.") 329 AVALANCHE_SUPPRESSOR_HUN_SUPPRESSED(1825), 330 @UiEvent(doc = "HUN allowed during avalanche because conversation newer than the trigger.") 331 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_NEW_CONVERSATION(1826), 332 @UiEvent(doc = "HUN allowed during avalanche because it is a priority conversation.") 333 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_PRIORITY_CONVERSATION(1827), 334 @UiEvent(doc = "HUN allowed during avalanche because it is a CallStyle notification.") 335 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CALL_STYLE(1828), 336 @UiEvent(doc = "HUN allowed during avalanche because it is a reminder notification.") 337 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_REMINDER(1829), 338 @UiEvent(doc = "HUN allowed during avalanche because it is a calendar notification.") 339 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_EVENT(1830), 340 @UiEvent(doc = "HUN allowed during avalanche because it has FSI.") 341 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_FSI_WITH_PERMISSION(1831), 342 @UiEvent(doc = "HUN allowed during avalanche because it is colorized.") 343 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED(1832), 344 @UiEvent(doc = "HUN allowed during avalanche because it is an emergency notification.") 345 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY(1833), 346 @UiEvent(doc = "HUN allowed during avalanche because it is an alarm.") 347 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_ALARM(1867), 348 @UiEvent(doc = "HUN allowed during avalanche because it is a car emergency.") 349 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_EMERGENCY(1868), 350 @UiEvent(doc = "HUN allowed during avalanche because it is a car warning") 351 AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_WARNING(1869); 352 353 override fun getId(): Int { 354 return id 355 } 356 } 357 358 override fun shouldSuppress(entry: NotificationEntry): Boolean { 359 if (!isCooldownEnabled()) { 360 return false 361 } 362 val timeSinceAvalancheMs = systemClock.currentTimeMillis() - avalancheProvider.startTime 363 val timedOut = timeSinceAvalancheMs >= avalancheProvider.timeoutMs 364 if (timedOut) { 365 return false 366 } 367 val state = calculateState(entry) 368 if (state != State.SUPPRESS) { 369 return false 370 } 371 if (shouldShowEdu()) { 372 showEdu() 373 } 374 return true 375 } 376 377 /** Show avalanche education HUN from SystemUI. */ 378 private fun showEdu() { 379 val res = context.resources 380 val titleStr = 381 res.getString(com.android.systemui.res.R.string.adaptive_notification_edu_hun_title) 382 val textStr = 383 res.getString(com.android.systemui.res.R.string.adaptive_notification_edu_hun_text) 384 val actionStr = 385 res.getString(com.android.systemui.res.R.string.go_to_adaptive_notification_settings) 386 387 val intent = Intent(Settings.ACTION_MANAGE_ADAPTIVE_NOTIFICATIONS) 388 val pendingIntent = 389 PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) 390 391 // Replace "System UI" app name with "Android System" 392 val bundle = Bundle() 393 bundle.putString( 394 Notification.EXTRA_SUBSTITUTE_APP_NAME, 395 context.getString(com.android.internal.R.string.android_system_label), 396 ) 397 398 val builder = 399 Notification.Builder(context, NotificationChannels.ALERTS) 400 .setTicker(titleStr) 401 .setContentTitle(titleStr) 402 .setContentText(textStr) 403 .setStyle(Notification.BigTextStyle().bigText(textStr)) 404 .setSmallIcon(com.android.systemui.res.R.drawable.ic_settings) 405 .setCategory(Notification.CATEGORY_SYSTEM) 406 .setTimeoutAfter(/* one day in ms */ 24 * 60 * 60 * 1000L) 407 .setAutoCancel(true) 408 .addAction(android.R.drawable.button_onoff_indicator_off, actionStr, pendingIntent) 409 .setContentIntent(pendingIntent) 410 .addExtras(bundle) 411 412 notificationManager.notify(SystemMessage.NOTE_ADAPTIVE_NOTIFICATIONS, builder.build()) 413 hasSeenEdu = true 414 hasShownOnceForDebug = true 415 } 416 417 private fun calculateState(entry: NotificationEntry): State { 418 if ( 419 entry.ranking.isConversation && 420 entry.sbn.notification.getWhen() > avalancheProvider.startTime 421 ) { 422 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_NEW_CONVERSATION) 423 return State.ALLOW_CONVERSATION_AFTER_AVALANCHE 424 } 425 426 if (entry.channel?.isImportantConversation == true) { 427 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_PRIORITY_CONVERSATION) 428 return State.ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME 429 } 430 431 if (entry.sbn.notification.isStyle(Notification.CallStyle::class.java)) { 432 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CALL_STYLE) 433 return State.ALLOW_CALLSTYLE 434 } 435 436 if (entry.sbn.notification.category == CATEGORY_REMINDER) { 437 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_REMINDER) 438 return State.ALLOW_CATEGORY_REMINDER 439 } 440 441 if (entry.sbn.notification.category == CATEGORY_ALARM) { 442 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_ALARM) 443 return State.ALLOW_CATEGORY_ALARM 444 } 445 446 if (entry.sbn.notification.category == CATEGORY_CAR_EMERGENCY) { 447 uiEventLogger.log( 448 AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_EMERGENCY 449 ) 450 return State.ALLOW_CATEGORY_CAR_EMERGENCY 451 } 452 453 if (entry.sbn.notification.category == CATEGORY_CAR_WARNING) { 454 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_WARNING) 455 return State.ALLOW_CATEGORY_CAR_WARNING 456 } 457 458 if (entry.sbn.notification.category == CATEGORY_EVENT) { 459 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_EVENT) 460 return State.ALLOW_CATEGORY_EVENT 461 } 462 463 if (entry.sbn.notification.fullScreenIntent != null) { 464 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_FSI_WITH_PERMISSION) 465 return State.ALLOW_FSI_WITH_PERMISSION_ON 466 } 467 if (entry.sbn.notification.isColorized) { 468 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED) 469 return State.ALLOW_COLORIZED 470 } 471 if ( 472 packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) == 473 PERMISSION_GRANTED 474 ) { 475 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY) 476 return State.ALLOW_EMERGENCY 477 } 478 uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_SUPPRESSED) 479 return State.SUPPRESS 480 } 481 482 private fun isCooldownEnabled(): Boolean { 483 val isEnabledFromFlow = settingsInteractor.isCooldownEnabled.value 484 if (isCooldownFlowInSync) { 485 return isEnabledFromFlow 486 } 487 val isEnabled = 488 systemSettings.getInt(Settings.System.NOTIFICATION_COOLDOWN_ENABLED, /* def */ 1) == 1 489 if (isEnabled == isEnabledFromFlow) { 490 isCooldownFlowInSync = true 491 } 492 return isEnabled 493 } 494 } 495