From 50aebbd42b5e6e4f31e33b77e8570d77b5091663 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Thu, 23 Jul 2026 14:02:58 +0000 Subject: [PATCH] Stop notification listener when all logging is disabled --- README.md | 9 +++++ .../settings/LoggingRuleStore.kt | 9 ++++- ...NotificationListenerComponentController.kt | 37 +++++++++++++++++++ .../settings/NotificationListenerPolicy.kt | 6 +++ .../NotificationListenerPolicyTest.kt | 15 ++++++++ 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/se/ajpanton/notificationlog/settings/NotificationListenerComponentController.kt create mode 100644 app/src/main/java/se/ajpanton/notificationlog/settings/NotificationListenerPolicy.kt create mode 100644 app/src/test/java/se/ajpanton/notificationlog/settings/NotificationListenerPolicyTest.kt diff --git a/README.md b/README.md index 67aea16..2594ef6 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,15 @@ The app declares `QUERY_ALL_PACKAGES` solely to implement the settings pages' restricted package-visibility policy and provide the required declaration; a future distribution variant may need a narrower app-selection flow. +## Background behavior + +Android owns the notification-listener connection; this app has no polling, +scheduled job, alarm, or boot receiver. When one or more logging types are +enabled and notification access has been granted, Android binds the listener +again after device boot. When every logging type is disabled, the app disables +that listener component: it has no background service and does not start at +boot. Re-enabling any logging type re-enables and asks Android to rebind it. + ## Notification update policy Each platform notification key is tracked independently. Group summaries are diff --git a/app/src/main/java/se/ajpanton/notificationlog/settings/LoggingRuleStore.kt b/app/src/main/java/se/ajpanton/notificationlog/settings/LoggingRuleStore.kt index e616791..c6a3988 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/settings/LoggingRuleStore.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/settings/LoggingRuleStore.kt @@ -5,7 +5,8 @@ import androidx.core.content.edit /** Persists rule choices only; notification contents never enter SharedPreferences. */ class LoggingRuleStore(context: Context) { - private val preferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) + private val appContext = context.applicationContext + private val preferences = appContext.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) fun ruleFor(type: LoggingType): LoggingRule = LoggingRule( enabled = preferences.getBoolean(key(type, "enabled"), true), @@ -24,6 +25,7 @@ class LoggingRuleStore(context: Context) { preferences.edit { write(type, rule) } + updateListenerComponent() } fun copy(from: LoggingType, targets: Set) { @@ -33,6 +35,7 @@ class LoggingRuleStore(context: Context) { write(target, source.copy(selectedPackages = source.selectedPackages.toSet())) } } + updateListenerComponent() } private fun android.content.SharedPreferences.Editor.write(type: LoggingType, rule: LoggingRule) { @@ -46,6 +49,10 @@ class LoggingRuleStore(context: Context) { private fun key(type: LoggingType, suffix: String) = "${type.name.lowercase()}.$suffix" + private fun updateListenerComponent() { + NotificationListenerComponentController.update(appContext, LoggingType.entries.map(::ruleFor)) + } + private companion object { const val FILE_NAME = "logging-rules" } diff --git a/app/src/main/java/se/ajpanton/notificationlog/settings/NotificationListenerComponentController.kt b/app/src/main/java/se/ajpanton/notificationlog/settings/NotificationListenerComponentController.kt new file mode 100644 index 0000000..176a8ca --- /dev/null +++ b/app/src/main/java/se/ajpanton/notificationlog/settings/NotificationListenerComponentController.kt @@ -0,0 +1,37 @@ +package se.ajpanton.notificationlog.settings + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.service.notification.NotificationListenerService +import android.util.Log +import se.ajpanton.notificationlog.capture.NotificationCaptureService + +/** + * Notification listeners are re-bound by Android after boot when their component + * is enabled and the user granted notification access. Disabling the component + * is therefore both the no-work battery mode and the no-start-on-boot mode. + */ +internal object NotificationListenerComponentController { + fun update(context: Context, rules: Collection) { + val applicationContext = context.applicationContext + val component = ComponentName(applicationContext, NotificationCaptureService::class.java) + val packageManager = applicationContext.packageManager + val desired = if (NotificationListenerPolicy.shouldRun(rules)) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + if (packageManager.getComponentEnabledSetting(component) == desired) return + packageManager.setComponentEnabledSetting(component, desired, PackageManager.DONT_KILL_APP) + if (desired == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + try { + NotificationListenerService.requestRebind(component) + } catch (error: SecurityException) { + Log.w(TAG, "Notification access is not granted yet; Android will bind after the user grants it", error) + } + } + } + + private const val TAG = "NotificationListenerControl" +} diff --git a/app/src/main/java/se/ajpanton/notificationlog/settings/NotificationListenerPolicy.kt b/app/src/main/java/se/ajpanton/notificationlog/settings/NotificationListenerPolicy.kt new file mode 100644 index 0000000..bbbcd62 --- /dev/null +++ b/app/src/main/java/se/ajpanton/notificationlog/settings/NotificationListenerPolicy.kt @@ -0,0 +1,6 @@ +package se.ajpanton.notificationlog.settings + +/** The listener is useful only when at least one event type is enabled. */ +object NotificationListenerPolicy { + fun shouldRun(rules: Collection): Boolean = rules.any { it.enabled } +} diff --git a/app/src/test/java/se/ajpanton/notificationlog/settings/NotificationListenerPolicyTest.kt b/app/src/test/java/se/ajpanton/notificationlog/settings/NotificationListenerPolicyTest.kt new file mode 100644 index 0000000..227a2ac --- /dev/null +++ b/app/src/test/java/se/ajpanton/notificationlog/settings/NotificationListenerPolicyTest.kt @@ -0,0 +1,15 @@ +package se.ajpanton.notificationlog.settings + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class NotificationListenerPolicyTest { + @Test fun `listener runs when any logging type is enabled`() { + assertTrue(NotificationListenerPolicy.shouldRun(listOf(LoggingRule(enabled = false), LoggingRule(enabled = true)))) + } + + @Test fun `listener stays off when every logging type is disabled`() { + assertFalse(NotificationListenerPolicy.shouldRun(LoggingType.entries.map { LoggingRule(enabled = false) })) + } +}