diff --git a/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertListenerPolicyDeviceTest.kt b/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertListenerPolicyDeviceTest.kt new file mode 100644 index 0000000..9b39c22 --- /dev/null +++ b/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertListenerPolicyDeviceTest.kt @@ -0,0 +1,50 @@ +package se.ajpanton.notificationsmaster.alerts + +import android.content.ComponentName +import android.content.pm.PackageManager +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Test +import se.ajpanton.notificationsmaster.capture.NotificationCaptureService +import se.ajpanton.notificationsmaster.settings.LoggingRule +import se.ajpanton.notificationsmaster.settings.LoggingRuleStore +import se.ajpanton.notificationsmaster.settings.LoggingType + +class AlertListenerPolicyDeviceTest { + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val component = ComponentName(context, NotificationCaptureService::class.java) + + @After + fun restoreDefaults() { + LoggingType.entries.forEach { LoggingRuleStore(context).save(it, LoggingRule()) } + AlertConfigurationStore(context).save(AlertConfiguration()) + } + + @Test + fun alertRuleKeepsListenerEnabledWithoutLogging() { + LoggingType.entries.forEach { LoggingRuleStore(context).save(it, LoggingRule(enabled = false)) } + val profile = AlertProfile("profile", "Profile", vibrationPattern = listOf(0, 10)) + val configuration = AlertConfiguration( + profiles = listOf(profile), + rules = listOf( + AlertRule( + "rule", "example.app", 0, setOf(AlertSource.NOTIFICATION_POST), + outcome = AlertOutcome.PLAY_PROFILE, profileId = profile.id, + ), + ), + ) + + AlertConfigurationStore(context).save(configuration) + assertEquals( + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + context.packageManager.getComponentEnabledSetting(component), + ) + + AlertConfigurationStore(context).save(AlertConfiguration()) + assertEquals( + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + context.packageManager.getComponentEnabledSetting(component), + ) + } +} diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/NotificationLogApplication.kt b/app/src/main/java/se/ajpanton/notificationsmaster/NotificationLogApplication.kt index 9df00a0..dd342a2 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/NotificationLogApplication.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/NotificationLogApplication.kt @@ -4,8 +4,6 @@ import android.app.Application import android.content.pm.PackageManager import se.ajpanton.notificationsmaster.data.EncryptedNotificationLogStore import se.ajpanton.notificationsmaster.settings.AppFilterSettingsStore -import se.ajpanton.notificationsmaster.settings.LoggingRuleStore -import se.ajpanton.notificationsmaster.settings.LoggingType import se.ajpanton.notificationsmaster.settings.NotificationListenerComponentController import se.ajpanton.notificationsmaster.settings.PerAppEventSettingsStore @@ -22,12 +20,7 @@ class NotificationLogApplication : Application() { * Reconcile it before the UI starts so fresh default rules can bind again. */ private fun synchronizeListenerComponent() { - val rules = LoggingRuleStore(this) - NotificationListenerComponentController.update( - this, - LoggingType.entries.associateWith(rules::ruleFor), - PerAppEventSettingsStore(this).hasEnabledEventOverride(), - ) + NotificationListenerComponentController.synchronize(this) } private fun removeStaleAppStorage() { diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStore.kt b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStore.kt index 7ab47db..c570276 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStore.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStore.kt @@ -7,6 +7,7 @@ import org.json.JSONObject import se.ajpanton.notificationsmaster.data.AesGcmCipher import se.ajpanton.notificationsmaster.data.EncryptedPayload import se.ajpanton.notificationsmaster.data.LogEncryptionKeyProvider +import se.ajpanton.notificationsmaster.settings.NotificationListenerComponentController import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.DataInputStream @@ -16,7 +17,8 @@ import java.io.FileNotFoundException /** Atomic, encrypted storage for alert settings. An unsupported format fails closed. */ class AlertConfigurationStore(context: Context) { - private val file = AtomicFile(File(context.applicationContext.filesDir, FILE_NAME)) + private val context = context.applicationContext + private val file = AtomicFile(File(this.context.filesDir, FILE_NAME)) private val cipher = AesGcmCipher(LogEncryptionKeyProvider().getOrCreate()) @Synchronized @@ -44,6 +46,7 @@ class AlertConfigurationStore(context: Context) { stream.write(payload.cipherText) stream.flush() file.finishWrite(output) + NotificationListenerComponentController.synchronize(context) } catch (error: Exception) { file.failWrite(output) throw error @@ -59,6 +62,8 @@ class AlertConfigurationStore(context: Context) { internal object AlertConfigurationJson { fun encode(configuration: AlertConfiguration): ByteArray = JSONObject() .put("version", 1) + .put("enabled", configuration.enabled) + .put("ignoreRoutineUpdates", configuration.ignoreRoutineUpdates) .put("profiles", JSONArray().apply { configuration.profiles.forEach { put(it.toJson()) } }) .put("apps", JSONArray().apply { configuration.appSettings.forEach { put(it.toJson()) } }) .put("rules", JSONArray().apply { configuration.rules.forEach { put(it.toJson()) } }) @@ -70,6 +75,8 @@ internal object AlertConfigurationJson { val root = JSONObject(bytes.decodeToString()) require(root.getInt("version") == 1) { "Unsupported alert-configuration format." } return AlertConfiguration( + enabled = root.getBoolean("enabled"), + ignoreRoutineUpdates = root.getBoolean("ignoreRoutineUpdates"), profiles = root.getJSONArray("profiles").map { (it as JSONObject).toProfile() }, appSettings = root.getJSONArray("apps").map { (it as JSONObject).toAppSettings() }, rules = root.getJSONArray("rules").map { (it as JSONObject).toRule() }, diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertModels.kt b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertModels.kt index a6be411..e0d3e3c 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertModels.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertModels.kt @@ -119,6 +119,8 @@ data class AlertQueueSettings( } data class AlertConfiguration( + val enabled: Boolean = true, + val ignoreRoutineUpdates: Boolean = true, val profiles: List = emptyList(), val appSettings: List = emptyList(), val rules: List = emptyList(), @@ -132,6 +134,8 @@ data class AlertConfiguration( rule.profileId in profiles.map { it.id } }) } + + val hasEnabledRules get() = enabled && rules.any { it.enabled } } data class AlertEvent( diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcher.kt b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcher.kt new file mode 100644 index 0000000..7757589 --- /dev/null +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcher.kt @@ -0,0 +1,32 @@ +package se.ajpanton.notificationsmaster.alerts + +/** + * Converts a captured notification event into immutable playback work. + * Rootless operation can play matched profiles but intentionally cannot + * suppress the originating app's native effect. + */ +object AlertNotificationDispatcher { + fun dispatch( + configuration: AlertConfiguration, + event: AlertEvent, + isRoutineUpdate: Boolean, + ): List { + if (!configuration.enabled || + event.source == AlertSource.NOTIFICATION_UPDATE && + isRoutineUpdate && + configuration.ignoreRoutineUpdates + ) { + return emptyList() + } + val app = configuration.appSettings.firstOrNull { it.packageName == event.packageName } + ?: AlertAppSettings(event.packageName) + return AlertRuleEvaluator.evaluate( + app, + configuration.rules, + configuration.profiles.associateBy { it.id }, + event, + ).decisions.mapNotNull { decision -> + decision.snapshot()?.let { QueuedAlert(decision.ruleId, it) } + } + } +} diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationCaptureService.kt b/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationCaptureService.kt index a1518ae..700bb43 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationCaptureService.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationCaptureService.kt @@ -5,6 +5,13 @@ import android.content.pm.PackageManager import android.service.notification.NotificationListenerService import android.service.notification.StatusBarNotification import android.util.Log +import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore +import se.ajpanton.notificationsmaster.alerts.AlertEvent +import se.ajpanton.notificationsmaster.alerts.AlertNotificationDispatcher +import se.ajpanton.notificationsmaster.alerts.AlertPlaybackController +import se.ajpanton.notificationsmaster.alerts.AlertQueueSettings +import se.ajpanton.notificationsmaster.alerts.AlertSource +import se.ajpanton.notificationsmaster.alerts.AndroidAlertEffectPlayer import se.ajpanton.notificationsmaster.data.EncryptedNotificationLogStore import se.ajpanton.notificationsmaster.data.EncryptedImageStore import se.ajpanton.notificationsmaster.model.NotificationAction @@ -27,6 +34,9 @@ class NotificationCaptureService : NotificationListenerService() { private lateinit var appFilterStore: AppFilterSettingsStore private lateinit var perAppEventSettings: PerAppEventSettingsStore private lateinit var imageStore: EncryptedImageStore + private lateinit var alertConfigurationStore: AlertConfigurationStore + private lateinit var alertPlayback: AlertPlaybackController + private var alertQueueSettings = AlertQueueSettings() override fun onCreate() { super.onCreate() @@ -36,6 +46,8 @@ class NotificationCaptureService : NotificationListenerService() { appFilterStore = AppFilterSettingsStore(this) perAppEventSettings = PerAppEventSettingsStore(this) imageStore = EncryptedImageStore(this) + alertConfigurationStore = AlertConfigurationStore(this) + alertPlayback = AlertPlaybackController(AndroidAlertEffectPlayer(this)) { alertQueueSettings } writeExecutor = Executors.newSingleThreadExecutor { runnable -> Thread(runnable, "notification-log-writer") } @@ -62,23 +74,29 @@ class NotificationCaptureService : NotificationListenerService() { SeenApps.markSeen(snapshot.packageName) val previous = activeNotifications.put(snapshot.key, snapshot) when { - previous == null -> record( - snapshot, - NotificationAction.APPEARED, - LoggingType.APPEARING, - includeContents = true, - imageBytes = { NotificationContents.extractImageBytes(sbn.notification, this) }, - ) - NotificationChangeClassifier.isMeaningfulEdit(previous, snapshot) && - !NotificationChangeClassifier.shouldIgnoreEdit(previous, snapshot, ruleStore.ruleFor(LoggingType.EDITS).ignoreRoutineUpdates) -> + previous == null -> { record( snapshot, - NotificationAction.EDITED, - LoggingType.EDITS, + NotificationAction.APPEARED, + LoggingType.APPEARING, includeContents = true, - previousSnapshot = previous, imageBytes = { NotificationContents.extractImageBytes(sbn.notification, this) }, ) + dispatchAlert(snapshot, AlertSource.NOTIFICATION_POST) + } + NotificationChangeClassifier.isMeaningfulEdit(previous, snapshot) -> { + if (!NotificationChangeClassifier.shouldIgnoreEdit(previous, snapshot, ruleStore.ruleFor(LoggingType.EDITS).ignoreRoutineUpdates)) { + record( + snapshot, + NotificationAction.EDITED, + LoggingType.EDITS, + includeContents = true, + previousSnapshot = previous, + imageBytes = { NotificationContents.extractImageBytes(sbn.notification, this) }, + ) + } + dispatchAlert(snapshot, AlertSource.NOTIFICATION_UPDATE) + } } } @@ -93,6 +111,7 @@ class NotificationCaptureService : NotificationListenerService() { } override fun onDestroy() { + alertPlayback.stop() writeExecutor.shutdown() super.onDestroy() } @@ -160,6 +179,16 @@ class NotificationCaptureService : NotificationListenerService() { return eventEnabled && NotificationRuleEvaluator.allows(appFilterStore.load(), packageName) } + private fun dispatchAlert(snapshot: NotificationSnapshot, source: AlertSource) { + val configuration = alertConfigurationStore.load() + alertQueueSettings = configuration.queueSettings + AlertNotificationDispatcher.dispatch( + configuration, + AlertEvent(snapshot.packageName, source, snapshot.alertTitle, snapshot.alertBody, snapshot.alertSender), + isRoutineUpdate = source == AlertSource.NOTIFICATION_UPDATE && snapshot.isRoutine, + ).forEach(alertPlayback::enqueue) + } + private fun appName(packageName: String): String = try { val applicationInfo = packageManager.getApplicationInfo(packageName, 0) packageManager.getApplicationLabel(applicationInfo).toString() diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationSnapshot.kt b/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationSnapshot.kt index ecc56e1..f661208 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationSnapshot.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationSnapshot.kt @@ -19,9 +19,18 @@ data class NotificationSnapshot( val hasImage: Boolean, val isGroupSummary: Boolean, val isRoutine: Boolean, + val alertTitle: String? = null, + val alertBody: String? = null, + val alertSender: String? = null, ) object NotificationContents { + data class AlertTextContents( + val title: String?, + val body: String?, + val sender: String?, + ) + fun extract(notification: Notification): String? { val extras = notification.extras ?: Bundle.EMPTY messagingContents(extras)?.let { return it } @@ -69,6 +78,7 @@ object NotificationContents { fun snapshot(sbn: StatusBarNotification): NotificationSnapshot { val notification = sbn.notification val extras = notification.extras + val alertContents = alertContents(notification) return NotificationSnapshot( key = sbn.key, packageName = sbn.packageName, @@ -83,9 +93,24 @@ object NotificationContents { it.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 && it.getInt(Notification.EXTRA_PROGRESS, -1) >= 0 } == true, + alertTitle = alertContents.title, + alertBody = alertContents.body, + alertSender = alertContents.sender, ) } + fun alertContents(notification: Notification): AlertTextContents { + val extras = notification.extras ?: Bundle.EMPTY + val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString() + val body = extract(notification)?.let { contents -> + title?.let { contents.removePrefix(it + "\n") }?.ifBlank { null } ?: contents + } + val sender = Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_MESSAGES, Bundle::class.java), + ).lastOrNull()?.senderPerson?.name?.toString() + return AlertTextContents(title, body, sender) + } + /** Called only after the event has passed all capture filters. */ fun extractImageBytes(notification: Notification, context: Context): ByteArray? { val extras = notification.extras ?: return null diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/settings/LoggingRuleStore.kt b/app/src/main/java/se/ajpanton/notificationsmaster/settings/LoggingRuleStore.kt index 4fc286e..daf49ce 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/settings/LoggingRuleStore.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/settings/LoggingRuleStore.kt @@ -28,11 +28,7 @@ class LoggingRuleStore(context: Context) { private fun key(type: LoggingType, suffix: String) = "${type.name.lowercase()}.$suffix" private fun updateListenerComponent() { - NotificationListenerComponentController.update( - appContext, - LoggingType.entries.associateWith(::ruleFor), - PerAppEventSettingsStore(appContext).hasEnabledEventOverride(), - ) + NotificationListenerComponentController.synchronize(appContext) } private companion object { diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/settings/NotificationListenerComponentController.kt b/app/src/main/java/se/ajpanton/notificationsmaster/settings/NotificationListenerComponentController.kt index 9bedb96..1d760e3 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/settings/NotificationListenerComponentController.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/settings/NotificationListenerComponentController.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.pm.PackageManager import android.service.notification.NotificationListenerService import android.util.Log +import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore import se.ajpanton.notificationsmaster.capture.NotificationCaptureService /** @@ -13,11 +14,27 @@ import se.ajpanton.notificationsmaster.capture.NotificationCaptureService * is therefore both the no-work battery mode and the no-start-on-boot mode. */ internal object NotificationListenerComponentController { - fun update(context: Context, rules: Map, hasEnabledEventOverride: Boolean = false) { + fun synchronize(context: Context) { + val applicationContext = context.applicationContext + val rules = LoggingRuleStore(applicationContext) + update( + applicationContext, + LoggingType.entries.associateWith(rules::ruleFor), + PerAppEventSettingsStore(applicationContext).hasEnabledEventOverride(), + AlertConfigurationStore(applicationContext).load().hasEnabledRules, + ) + } + + fun update( + context: Context, + rules: Map, + hasEnabledEventOverride: Boolean = false, + hasEnabledAlertRule: Boolean = false, + ) { val applicationContext = context.applicationContext val component = ComponentName(applicationContext, NotificationCaptureService::class.java) val packageManager = applicationContext.packageManager - val desired = if (NotificationListenerPolicy.shouldRun(rules, hasEnabledEventOverride)) { + val desired = if (NotificationListenerPolicy.shouldRun(rules, hasEnabledEventOverride, hasEnabledAlertRule)) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED } else { PackageManager.COMPONENT_ENABLED_STATE_DISABLED diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/settings/NotificationListenerPolicy.kt b/app/src/main/java/se/ajpanton/notificationsmaster/settings/NotificationListenerPolicy.kt index 1d85777..0e44324 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/settings/NotificationListenerPolicy.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/settings/NotificationListenerPolicy.kt @@ -5,5 +5,8 @@ object NotificationListenerPolicy { fun shouldRun( rules: Map, hasEnabledEventOverride: Boolean = false, - ): Boolean = LoggingType.eventTypes.any { rules.getValue(it).enabled } || hasEnabledEventOverride + hasEnabledAlertRule: Boolean = false, + ): Boolean = LoggingType.eventTypes.any { rules.getValue(it).enabled } || + hasEnabledEventOverride || + hasEnabledAlertRule } diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/settings/PerAppEventSettingsStore.kt b/app/src/main/java/se/ajpanton/notificationsmaster/settings/PerAppEventSettingsStore.kt index 479b312..5f95919 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/settings/PerAppEventSettingsStore.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/settings/PerAppEventSettingsStore.kt @@ -52,8 +52,7 @@ class PerAppEventSettingsStore(context: Context) { } private fun updateListenerComponent() { - val rules = LoggingType.entries.associateWith { LoggingRuleStore(appContext).ruleFor(it) } - NotificationListenerComponentController.update(appContext, rules, hasEnabledEventOverride()) + NotificationListenerComponentController.synchronize(appContext) } private fun key(packageName: String) = "$KEY_PREFIX$packageName" diff --git a/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcherTest.kt b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcherTest.kt new file mode 100644 index 0000000..07ac6f9 --- /dev/null +++ b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcherTest.kt @@ -0,0 +1,59 @@ +package se.ajpanton.notificationsmaster.alerts + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class AlertNotificationDispatcherTest { + private val profile = AlertProfile("profile", "Profile", "content://sound") + private val event = AlertEvent("chat.app", AlertSource.NOTIFICATION_POST, body = "hello") + private val rule = AlertRule( + "rule", "chat.app", 0, setOf(AlertSource.NOTIFICATION_POST), + outcome = AlertOutcome.PLAY_PROFILE, profileId = profile.id, + ) + + @Test + fun disabledAlertingProducesNoPlayback() { + assertTrue(AlertNotificationDispatcher.dispatch(configuration(enabled = false), event, false).isEmpty()) + } + + @Test + fun matchedPostProducesSnapshotPlayback() { + val queued = AlertNotificationDispatcher.dispatch(configuration(), event, false) + + assertEquals(listOf("rule"), queued.map { it.ruleId }) + assertEquals(profile.soundUri, queued.single().snapshot.soundUri) + } + + @Test + fun routineUpdateIsIgnoredByDefault() { + val updateRule = rule.copy(sources = setOf(AlertSource.NOTIFICATION_UPDATE)) + val configuration = AlertConfiguration(profiles = listOf(profile), rules = listOf(updateRule)) + + assertTrue( + AlertNotificationDispatcher.dispatch( + configuration, + event.copy(source = AlertSource.NOTIFICATION_UPDATE), + isRoutineUpdate = true, + ).isEmpty(), + ) + } + + @Test + fun appCanQueueEveryMatchingRule() { + val second = rule.copy(id = "second", order = 1) + val configuration = AlertConfiguration( + profiles = listOf(profile), + appSettings = listOf(AlertAppSettings("chat.app", allowMultipleMatches = true)), + rules = listOf(rule, second), + ) + + assertEquals(listOf("rule", "second"), AlertNotificationDispatcher.dispatch(configuration, event, false).map { it.ruleId }) + } + + private fun configuration(enabled: Boolean = true) = AlertConfiguration( + enabled = enabled, + profiles = listOf(profile), + rules = listOf(rule), + ) +} diff --git a/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertRuleEditorTest.kt b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertRuleEditorTest.kt index 7551161..df1cdd5 100644 --- a/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertRuleEditorTest.kt +++ b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertRuleEditorTest.kt @@ -59,7 +59,7 @@ class AlertRuleEditorTest { id, packageName, order, definition.sources, definition.matcher, definition.outcome, definition.profileId, ) - private fun configuration(vararg rules: AlertRule) = AlertConfiguration(listOf(profile), rules = rules.toList()) + private fun configuration(vararg rules: AlertRule) = AlertConfiguration(profiles = listOf(profile), rules = rules.toList()) private fun iterOf(vararg values: String) = values.iterator() } diff --git a/app/src/test/java/se/ajpanton/notificationsmaster/settings/NotificationListenerPolicyTest.kt b/app/src/test/java/se/ajpanton/notificationsmaster/settings/NotificationListenerPolicyTest.kt index b06afff..eb0559d 100644 --- a/app/src/test/java/se/ajpanton/notificationsmaster/settings/NotificationListenerPolicyTest.kt +++ b/app/src/test/java/se/ajpanton/notificationsmaster/settings/NotificationListenerPolicyTest.kt @@ -26,6 +26,11 @@ class NotificationListenerPolicyTest { ) } + @Test + fun listenerRunsForEnabledAlertRuleEvenWhenLoggingIsOff() { + assertTrue(NotificationListenerPolicy.shouldRun(rules(), hasEnabledAlertRule = true)) + } + private fun rules(vararg enabled: Pair) = LoggingType.entries.associateWith { type -> LoggingRule(enabled = enabled.firstOrNull { it.first == type }?.second ?: false) }