Stop notification listener when all logging is disabled
This commit is contained in:
@@ -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
|
restricted package-visibility policy and provide the required declaration; a
|
||||||
future distribution variant may need a narrower app-selection flow.
|
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
|
## Notification update policy
|
||||||
|
|
||||||
Each platform notification key is tracked independently. Group summaries are
|
Each platform notification key is tracked independently. Group summaries are
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import androidx.core.content.edit
|
|||||||
|
|
||||||
/** Persists rule choices only; notification contents never enter SharedPreferences. */
|
/** Persists rule choices only; notification contents never enter SharedPreferences. */
|
||||||
class LoggingRuleStore(context: Context) {
|
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(
|
fun ruleFor(type: LoggingType): LoggingRule = LoggingRule(
|
||||||
enabled = preferences.getBoolean(key(type, "enabled"), true),
|
enabled = preferences.getBoolean(key(type, "enabled"), true),
|
||||||
@@ -24,6 +25,7 @@ class LoggingRuleStore(context: Context) {
|
|||||||
preferences.edit {
|
preferences.edit {
|
||||||
write(type, rule)
|
write(type, rule)
|
||||||
}
|
}
|
||||||
|
updateListenerComponent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copy(from: LoggingType, targets: Set<LoggingType>) {
|
fun copy(from: LoggingType, targets: Set<LoggingType>) {
|
||||||
@@ -33,6 +35,7 @@ class LoggingRuleStore(context: Context) {
|
|||||||
write(target, source.copy(selectedPackages = source.selectedPackages.toSet()))
|
write(target, source.copy(selectedPackages = source.selectedPackages.toSet()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateListenerComponent()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun android.content.SharedPreferences.Editor.write(type: LoggingType, rule: LoggingRule) {
|
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 key(type: LoggingType, suffix: String) = "${type.name.lowercase()}.$suffix"
|
||||||
|
|
||||||
|
private fun updateListenerComponent() {
|
||||||
|
NotificationListenerComponentController.update(appContext, LoggingType.entries.map(::ruleFor))
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val FILE_NAME = "logging-rules"
|
const val FILE_NAME = "logging-rules"
|
||||||
}
|
}
|
||||||
|
|||||||
+37
@@ -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<LoggingRule>) {
|
||||||
|
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"
|
||||||
|
}
|
||||||
@@ -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<LoggingRule>): Boolean = rules.any { it.enabled }
|
||||||
|
}
|
||||||
+15
@@ -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) }))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user