From de984fb36c40fccaaf962fc62319731e5d0060ac Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Wed, 19 Aug 2026 20:57:17 +0000 Subject: [PATCH] Limit custom alerts to notification events --- README.md | 11 +--- .../alerts/AlertConfigurationStoreTest.kt | 2 +- .../alerts/AlertListenerPolicyDeviceTest.kt | 19 ------ .../module/AlertPolicySyncDeviceTest.kt | 10 --- app/src/main/AndroidManifest.xml | 8 --- .../notificationsmaster/AlertRuleDialog.kt | 7 +-- .../notificationsmaster/AppRulesFragment.kt | 10 +-- .../NotificationLogApplication.kt | 3 - .../alerts/AlertConfigurationStore.kt | 3 +- .../notificationsmaster/alerts/AlertModels.kt | 13 +--- .../alerts/AlertPlaybackRuntime.kt | 18 ------ .../alerts/AlertPolicyEvaluator.kt | 2 +- .../alerts/AlertRuleEvaluator.kt | 6 +- .../capture/NotificationCaptureService.kt | 9 +-- .../module/DirectAlertReceiver.kt | 31 ---------- .../module/DirectAlertSync.kt | 56 ----------------- .../module/DirectVibrationBridge.kt | 61 ------------------- .../module/NotificationsMasterModule.kt | 3 - .../module/SystemAlertPolicyCache.kt | 9 +-- .../main/res/layout/fragment_app_rules.xml | 12 ---- .../alerts/AlertConfigurationJsonTest.kt | 2 +- .../alerts/AlertRuleEvaluatorTest.kt | 36 ----------- test-helper/src/main/AndroidManifest.xml | 1 - .../helper/MainActivity.kt | 12 ---- 24 files changed, 19 insertions(+), 325 deletions(-) delete mode 100644 app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackRuntime.kt delete mode 100644 app/src/main/java/se/ajpanton/notificationsmaster/module/DirectAlertReceiver.kt delete mode 100644 app/src/main/java/se/ajpanton/notificationsmaster/module/DirectAlertSync.kt delete mode 100644 app/src/main/java/se/ajpanton/notificationsmaster/module/DirectVibrationBridge.kt diff --git a/README.md b/README.md index 1d1c7d4..327f58c 100644 --- a/README.md +++ b/README.md @@ -46,17 +46,12 @@ play to completion. Without optional system integration, a matching profile plays alongside the source app's normal alert. With a compatible LSPosed setup enabled, Notifications Master can suppress a matched standard notification alert while leaving the -notification itself visible. Experimental per-app control is also available for -direct vibrations that are explicitly marked by Android as notification usage. -It deliberately does not touch media, alarms, ringtones, touch feedback, or -unknown vibration usage. Direct app-originated sounds are not intercepted: they -do not have a safe, complete Android system boundary, so their original sound -continues to play. +notification itself visible. It only evaluates actual posted or updated +notifications; standalone sounds and vibrations remain under the source app's +control. To use the optional integration, enable Notifications Master as an LSPosed module and give it the **Android/system server** scope, then restart Android. -Enable direct-vibration control only for apps where you want that experimental -behaviour; normal notification rules work independently of this setting. Custom alerts respect Do Not Disturb. Android does not allow a normal app to force direct custom sound or vibration through Do Not Disturb. diff --git a/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStoreTest.kt b/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStoreTest.kt index cefcd4a..67c26eb 100644 --- a/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStoreTest.kt +++ b/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStoreTest.kt @@ -26,7 +26,7 @@ class AlertConfigurationStoreTest { val profile = AlertProfile("profile", "Sensitive profile", vibrationPattern = listOf(0, 120)) val configuration = AlertConfiguration( profiles = listOf(profile), - appSettings = listOf(AlertAppSettings("chat.app", directAlertControl = true)), + appSettings = listOf(AlertAppSettings("chat.app")), rules = listOf( AlertRule( "rule", "chat.app", 0, setOf(AlertSource.NOTIFICATION_POST), diff --git a/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertListenerPolicyDeviceTest.kt b/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertListenerPolicyDeviceTest.kt index 00dee82..a2c08e2 100644 --- a/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertListenerPolicyDeviceTest.kt +++ b/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AlertListenerPolicyDeviceTest.kt @@ -48,23 +48,4 @@ class AlertListenerPolicyDeviceTest { ) } - @Test - fun directAlertControlKeepsListenerEnabledWithoutRules() { - LoggingType.entries.forEach { LoggingRuleStore(context).save(it, LoggingRule(enabled = false)) } - - AlertConfigurationStore(context).save( - AlertConfiguration(appSettings = listOf(AlertAppSettings("example.app", directAlertControl = true))), - ) - - 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/androidTest/java/se/ajpanton/notificationsmaster/module/AlertPolicySyncDeviceTest.kt b/app/src/androidTest/java/se/ajpanton/notificationsmaster/module/AlertPolicySyncDeviceTest.kt index 4da5e1d..efe47b2 100644 --- a/app/src/androidTest/java/se/ajpanton/notificationsmaster/module/AlertPolicySyncDeviceTest.kt +++ b/app/src/androidTest/java/se/ajpanton/notificationsmaster/module/AlertPolicySyncDeviceTest.kt @@ -22,7 +22,6 @@ class AlertPolicySyncDeviceTest { val profile = AlertProfile("module-test", "Module test", vibrationPattern = listOf(0, 200)) val configuration = AlertConfiguration( profiles = listOf(profile), - appSettings = listOf(AlertAppSettings(HELPER_PACKAGE, directAlertControl = true)), rules = listOf( AlertRule( id = "module-helper-post", @@ -33,15 +32,6 @@ class AlertPolicySyncDeviceTest { outcome = AlertOutcome.PLAY_PROFILE, profileId = profile.id, ), - AlertRule( - id = "module-helper-direct-vibration", - packageName = HELPER_PACKAGE, - order = 1, - sources = setOf(AlertSource.DIRECT_NOTIFICATION_VIBRATION), - matcher = AlertTextMatcher(), - outcome = AlertOutcome.PLAY_PROFILE, - profileId = profile.id, - ), ), ) val store = AlertConfigurationStore(InstrumentationRegistry.getInstrumentation().targetContext) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 46f6c21..6ff07ed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,14 +53,6 @@ - - - - - diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt b/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt index 07b1e0b..3c7846e 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt @@ -62,10 +62,6 @@ object AlertRuleDialog { } val post = CheckBox(context).apply { text = "Appearing"; isChecked = existing == null || AlertSource.NOTIFICATION_POST in existing.sources } val update = CheckBox(context).apply { text = "Edits"; isChecked = existing == null || AlertSource.NOTIFICATION_UPDATE in existing.sources } - val directVibration = CheckBox(context).apply { - text = "Direct notification vibration (experimental)" - isChecked = AlertSource.DIRECT_NOTIFICATION_VIBRATION in (existing?.sources ?: emptySet()) - } val pattern = EditText(context).apply { hint = "Text to match (optional)"; setText(existing?.matcher?.value) } val regex = SwitchMaterial(context).apply { text = "Use regular expression"; isChecked = existing?.matcher?.mode == AlertTextMode.REGEX } val protected = SwitchMaterial(context).apply { text = "Play to completion"; isChecked = existing?.playToCompletion == true } @@ -73,7 +69,7 @@ object AlertRuleDialog { text = "Allow during DND (requires system support)" isChecked = existing?.allowDuringDnd == true } - listOf(outcome, profile, post, update, directVibration, pattern, regex, protected, dnd).forEach(form::addView) + listOf(outcome, profile, post, update, pattern, regex, protected, dnd).forEach(form::addView) form.addView(android.widget.TextView(context).apply { text = "Android blocks direct custom alerts during Do Not Disturb. This setting is retained for optional system support, but does not bypass Do Not Disturb in the normal app." textSize = 14f @@ -96,7 +92,6 @@ object AlertRuleDialog { val sources = buildSet { if (post.isChecked) add(AlertSource.NOTIFICATION_POST) if (update.isChecked) add(AlertSource.NOTIFICATION_UPDATE) - if (directVibration.isChecked) add(AlertSource.DIRECT_NOTIFICATION_VIBRATION) } if (sources.isEmpty()) { post.error = "Choose an event"; return@setOnClickListener } if (apps != null && selectedApps.isEmpty()) { app?.error = "Choose at least one app"; return@setOnClickListener } diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/AppRulesFragment.kt b/app/src/main/java/se/ajpanton/notificationsmaster/AppRulesFragment.kt index f52b267..604f838 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/AppRulesFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/AppRulesFragment.kt @@ -31,8 +31,6 @@ class AppRulesFragment : Fragment(R.layout.fragment_app_rules) { binding!!.addRule.setOnClickListener { edit(null) } binding!!.allowMultipleMatches.isChecked = store.load().appSettings.firstOrNull { it.packageName == packageName }?.allowMultipleMatches == true binding!!.allowMultipleMatches.setOnCheckedChangeListener { _, enabled -> setAllowMultipleMatches(enabled) } - binding!!.directAlertControl.isChecked = store.load().appSettings.firstOrNull { it.packageName == packageName }?.directAlertControl == true - binding!!.directAlertControl.setOnCheckedChangeListener { _, enabled -> setDirectAlertControl(enabled) } ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { override fun onMove(recyclerView: RecyclerView, holder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { adapter.move(holder.bindingAdapterPosition, target.bindingAdapterPosition) @@ -75,15 +73,9 @@ class AppRulesFragment : Fragment(R.layout.fragment_app_rules) { saveAppSettings(configuration, setting) } - private fun setDirectAlertControl(enabled: Boolean) { - val configuration = store.load() - val old = configuration.appSettings.firstOrNull { it.packageName == packageName } - saveAppSettings(configuration, (old ?: AlertAppSettings(packageName)).copy(directAlertControl = enabled)) - } - private fun saveAppSettings(configuration: se.ajpanton.notificationsmaster.alerts.AlertConfiguration, setting: AlertAppSettings) { store.save(configuration.copy(appSettings = configuration.appSettings.filterNot { it.packageName == packageName } + - if (setting.allowMultipleMatches || setting.directAlertControl) listOf(setting) else emptyList())) + if (setting.allowMultipleMatches) listOf(setting) else emptyList())) } private class RuleAdapter(private val onClick: (AlertRule) -> Unit) : RecyclerView.Adapter() { diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/NotificationLogApplication.kt b/app/src/main/java/se/ajpanton/notificationsmaster/NotificationLogApplication.kt index 70dcce7..dd342a2 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/NotificationLogApplication.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/NotificationLogApplication.kt @@ -2,15 +2,12 @@ package se.ajpanton.notificationsmaster import android.app.Application import android.content.pm.PackageManager -import se.ajpanton.notificationsmaster.alerts.AlertPlaybackRuntime import se.ajpanton.notificationsmaster.data.EncryptedNotificationLogStore import se.ajpanton.notificationsmaster.settings.AppFilterSettingsStore import se.ajpanton.notificationsmaster.settings.NotificationListenerComponentController import se.ajpanton.notificationsmaster.settings.PerAppEventSettingsStore class NotificationLogApplication : Application() { - val alertPlayback by lazy { AlertPlaybackRuntime(this) } - override fun onCreate() { super.onCreate() synchronizeListenerComponent() 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 aad8d5e..50e8f70 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStore.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertConfigurationStore.kt @@ -94,7 +94,6 @@ internal object AlertConfigurationJson { private fun AlertAppSettings.toJson() = JSONObject() .put("packageName", packageName).put("allowMultipleMatches", allowMultipleMatches) - .put("directAlertControl", directAlertControl) private fun AlertRule.toJson() = JSONObject() .put("id", id).put("packageName", packageName).put("order", order) @@ -113,7 +112,7 @@ internal object AlertConfigurationJson { ) private fun JSONObject.toAppSettings() = AlertAppSettings( - getString("packageName"), getBoolean("allowMultipleMatches"), getBoolean("directAlertControl"), + getString("packageName"), getBoolean("allowMultipleMatches"), ) private fun JSONObject.toRule() = AlertRule( 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 20d3dc0..619bbfb 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertModels.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertModels.kt @@ -5,17 +5,11 @@ import com.google.re2j.Pattern enum class AlertSource { NOTIFICATION_POST, NOTIFICATION_UPDATE, - DIRECT_NOTIFICATION_SOUND, - DIRECT_NOTIFICATION_VIBRATION, ; - val isDirect get() = this == DIRECT_NOTIFICATION_SOUND || this == DIRECT_NOTIFICATION_VIBRATION - fun label() = when (this) { NOTIFICATION_POST -> "Appearing" NOTIFICATION_UPDATE -> "Edits" - DIRECT_NOTIFICATION_SOUND -> "Direct notification sound" - DIRECT_NOTIFICATION_VIBRATION -> "Direct notification vibration" } } @@ -113,7 +107,6 @@ data class AlertRuleGroupingKey( data class AlertAppSettings( val packageName: String, val allowMultipleMatches: Boolean = false, - val directAlertControl: Boolean = false, ) data class AlertQueueSettings( @@ -143,8 +136,7 @@ data class AlertConfiguration( } val hasEnabledRules get() = enabled && rules.any { it.enabled } - val hasDirectAlertControl get() = enabled && appSettings.any { it.directAlertControl } - val needsListener get() = hasEnabledRules || hasDirectAlertControl + val needsListener get() = hasEnabledRules } data class AlertEvent( @@ -181,8 +173,7 @@ fun AlertDecision.snapshot(): AlertSnapshot? = profile?.let { data class AlertEvaluation( val decisions: List, - val silenceUnmatchedDirectAlert: Boolean, ) fun AlertEvaluation.suppressesOriginal() = - silenceUnmatchedDirectAlert || decisions.any { it.outcome != AlertOutcome.PASS_THROUGH } + decisions.any { it.outcome != AlertOutcome.PASS_THROUGH } diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackRuntime.kt b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackRuntime.kt deleted file mode 100644 index d464999..0000000 --- a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackRuntime.kt +++ /dev/null @@ -1,18 +0,0 @@ -package se.ajpanton.notificationsmaster.alerts - -import android.content.Context - -/** Process-wide playback queue shared by the notification listener and direct-alert handoff. */ -class AlertPlaybackRuntime(context: Context) { - private var queueSettings = AlertQueueSettings() - private val controller = AlertPlaybackController(AndroidAlertEffectPlayer(context)) { queueSettings } - - @Synchronized - fun enqueue(alert: QueuedAlert, settings: AlertQueueSettings) { - queueSettings = settings - controller.enqueue(alert) - } - - @Synchronized - fun stop() = controller.stop() -} diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPolicyEvaluator.kt b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPolicyEvaluator.kt index 1196472..a743e9a 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPolicyEvaluator.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPolicyEvaluator.kt @@ -12,7 +12,7 @@ object AlertPolicyEvaluator { isRoutineUpdate && configuration.ignoreRoutineUpdates ) { - return AlertEvaluation(emptyList(), false) + return AlertEvaluation(emptyList()) } val app = configuration.appSettings.firstOrNull { it.packageName == event.packageName } ?: AlertAppSettings(event.packageName) diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertRuleEvaluator.kt b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertRuleEvaluator.kt index 0fe44ae..56966c3 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertRuleEvaluator.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertRuleEvaluator.kt @@ -7,9 +7,7 @@ object AlertRuleEvaluator { profiles: Map, event: AlertEvent, ): AlertEvaluation { - if (app.packageName != event.packageName || event.source.isDirect && !app.directAlertControl) { - return AlertEvaluation(emptyList(), false) - } + if (app.packageName != event.packageName) return AlertEvaluation(emptyList()) val matches = rules.asSequence() .filter { it.enabled && it.packageName == event.packageName && event.source in it.sources } .sortedWith(compareBy { it.order }.thenBy { it.id }) @@ -17,7 +15,7 @@ object AlertRuleEvaluator { .mapNotNull { rule -> rule.decision(profiles[rule.profileId]) } .let { if (app.allowMultipleMatches) it else it.take(1) } .toList() - return AlertEvaluation(matches, event.source.isDirect && matches.isEmpty()) + return AlertEvaluation(matches) } private fun AlertRule.decision(profile: AlertProfile?): AlertDecision? = when (outcome) { 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 77db4b7..520d6d9 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationCaptureService.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/capture/NotificationCaptureService.kt @@ -9,9 +9,10 @@ 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.NotificationLogApplication +import se.ajpanton.notificationsmaster.alerts.AndroidAlertEffectPlayer import se.ajpanton.notificationsmaster.module.AlertPolicySync import se.ajpanton.notificationsmaster.data.EncryptedNotificationLogStore import se.ajpanton.notificationsmaster.data.EncryptedImageStore @@ -36,7 +37,7 @@ class NotificationCaptureService : NotificationListenerService() { private lateinit var perAppEventSettings: PerAppEventSettingsStore private lateinit var imageStore: EncryptedImageStore private lateinit var alertConfigurationStore: AlertConfigurationStore - private lateinit var alertPlayback: se.ajpanton.notificationsmaster.alerts.AlertPlaybackRuntime + private lateinit var alertPlayback: AlertPlaybackController private var alertQueueSettings = AlertQueueSettings() override fun onCreate() { @@ -48,7 +49,7 @@ class NotificationCaptureService : NotificationListenerService() { perAppEventSettings = PerAppEventSettingsStore(this) imageStore = EncryptedImageStore(this) alertConfigurationStore = AlertConfigurationStore(this) - alertPlayback = (application as NotificationLogApplication).alertPlayback + alertPlayback = AlertPlaybackController(AndroidAlertEffectPlayer(this)) { alertQueueSettings } writeExecutor = Executors.newSingleThreadExecutor { runnable -> Thread(runnable, "notification-log-writer") } @@ -189,7 +190,7 @@ class NotificationCaptureService : NotificationListenerService() { configuration, AlertEvent(snapshot.packageName, source, snapshot.alertTitle, snapshot.alertBody, snapshot.alertSender), isRoutineUpdate = source == AlertSource.NOTIFICATION_UPDATE && snapshot.isRoutine, - ).forEach { alertPlayback.enqueue(it, alertQueueSettings) } + ).forEach(alertPlayback::enqueue) } private fun appName(packageName: String): String = try { diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/DirectAlertReceiver.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/DirectAlertReceiver.kt deleted file mode 100644 index 0ef6a8f..0000000 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/DirectAlertReceiver.kt +++ /dev/null @@ -1,31 +0,0 @@ -package se.ajpanton.notificationsmaster.module - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.util.Log -import se.ajpanton.notificationsmaster.NotificationLogApplication -import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore -import se.ajpanton.notificationsmaster.alerts.QueuedAlert - -/** Signature-protected handoff that can start the app when its listener process was evicted. */ -class DirectAlertReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action != DirectAlertSync.ACTION) return - val request = DirectAlertSync.snapshot(intent) ?: run { - Log.w(TAG, "Rejected malformed direct alert request") - return - } - val (ruleId, snapshot) = request - val settings = AlertConfigurationStore(context).load().queueSettings - (context.applicationContext as NotificationLogApplication).alertPlayback.enqueue( - QueuedAlert(ruleId, snapshot), - settings, - ) - Log.i(TAG, "Playing matched direct notification alert") - } - - private companion object { - const val TAG = "NotificationCapture" - } -} diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/DirectAlertSync.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/DirectAlertSync.kt deleted file mode 100644 index f2402ad..0000000 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/DirectAlertSync.kt +++ /dev/null @@ -1,56 +0,0 @@ -package se.ajpanton.notificationsmaster.module - -import android.annotation.SuppressLint -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.os.UserHandle -import android.os.Binder -import se.ajpanton.notificationsmaster.alerts.AlertDecision -import se.ajpanton.notificationsmaster.alerts.AlertSnapshot -import se.ajpanton.notificationsmaster.alerts.snapshot - -/** One-way requests from system_server to the app's private direct-alert receiver. */ -internal object DirectAlertSync { - const val ACTION = "se.ajpanton.notificationsmaster.PLAY_DIRECT_ALERT" - private const val PACKAGE = "se.ajpanton.notificationsmaster" - private const val RECEIVER = "$PACKAGE.module.DirectAlertReceiver" - private const val EXTRA_RULE_ID = "ruleId" - private const val EXTRA_SOUND_URI = "soundUri" - private const val EXTRA_VIBRATION = "vibration" - private const val EXTRA_PROTECTED = "protected" - private const val EXTRA_ALLOW_DND = "allowDnd" - - @SuppressLint("MissingPermission") // This method runs only in system_server via the LSPosed module. - fun send(context: Context, decision: AlertDecision, userId: Int): Boolean { - val snapshot = decision.snapshot() ?: return false - val identity = Binder.clearCallingIdentity() - try { - context.sendBroadcastAsUser(Intent(ACTION).setComponent(ComponentName(PACKAGE, RECEIVER)) - .putExtra(EXTRA_RULE_ID, decision.ruleId) - .putExtra(EXTRA_SOUND_URI, snapshot.soundUri) - .putExtra(EXTRA_VIBRATION, snapshot.vibrationPattern.toLongArray()) - .putExtra(EXTRA_PROTECTED, snapshot.playToCompletion) - .putExtra(EXTRA_ALLOW_DND, snapshot.allowDuringDnd), userHandle(userId)) - } finally { - Binder.restoreCallingIdentity(identity) - } - return true - } - - fun snapshot(intent: Intent): Pair? = runCatching { - val soundUri = intent.getStringExtra(EXTRA_SOUND_URI) - val vibration = intent.getLongArrayExtra(EXTRA_VIBRATION)?.toList().orEmpty() - val snapshot = AlertSnapshot( - soundUri, - vibration, - intent.getBooleanExtra(EXTRA_PROTECTED, false), - intent.getBooleanExtra(EXTRA_ALLOW_DND, false), - ) - intent.getStringExtra(EXTRA_RULE_ID)?.takeIf(String::isNotBlank)?.let { it to snapshot } - }.getOrNull() - - private fun userHandle(userId: Int): UserHandle = UserHandle::class.java - .getDeclaredConstructor(Int::class.javaPrimitiveType) - .newInstance(userId) -} diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/DirectVibrationBridge.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/DirectVibrationBridge.kt deleted file mode 100644 index d40511b..0000000 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/DirectVibrationBridge.kt +++ /dev/null @@ -1,61 +0,0 @@ -package se.ajpanton.notificationsmaster.module - -import android.os.Build -import android.os.CombinedVibration -import android.os.IBinder -import android.os.VibrationAttributes -import android.util.Log -import io.github.libxposed.api.XposedInterface -import java.lang.reflect.Method -import se.ajpanton.notificationsmaster.alerts.AlertSource - -/** Intercepts only direct vibrations explicitly labelled as notification effects. */ -internal class DirectVibrationBridge(private val framework: XposedInterface) { - fun install(classLoader: ClassLoader) { - val method = findVibrateMethod(classLoader) ?: run { - Log.w(TAG, "Direct vibration method shape was not found; leaving Android unchanged") - return - } - method.isAccessible = true - framework.hook(method).intercept { chain -> - val uid = chain.args.getOrNull(UID_INDEX) as? Int - val packageName = chain.args.getOrNull(PACKAGE_INDEX) as? String - val attributes = chain.args.getOrNull(ATTRIBUTES_INDEX) as? VibrationAttributes - val event = packageName?.takeIf { attributes?.usage == VibrationAttributes.USAGE_NOTIFICATION } - ?.let { se.ajpanton.notificationsmaster.alerts.AlertEvent(it, AlertSource.DIRECT_NOTIFICATION_VIBRATION) } - if (event != null && SystemAlertPolicyCache.shouldSuppress(event)) { - SystemAlertPolicyCache.sendDirectAlerts(event, (uid ?: 0).coerceAtLeast(0) / USER_UID_RANGE) - Log.i(TAG, "Suppressed matched direct notification vibration from $packageName") - null - } else { - chain.proceed() - } - } - Log.i(TAG, "Installed API ${Build.VERSION.SDK_INT} direct notification vibration bridge") - } - - private fun findVibrateMethod(classLoader: ClassLoader): Method? = runCatching { - Class.forName(VIBRATOR_MANAGER_SERVICE, false, classLoader).declaredMethods.singleOrNull { method -> - method.name == "vibrate" && method.returnType == Void.TYPE && - method.parameterTypes.contentEquals(VIBRATE_PARAMETERS) - } - }.getOrNull() - - private companion object { - const val TAG = "NotificationsMaster" - const val VIBRATOR_MANAGER_SERVICE = "com.android.server.vibrator.VibratorManagerService" - const val PACKAGE_INDEX = 2 - const val UID_INDEX = 0 - const val ATTRIBUTES_INDEX = 4 - const val USER_UID_RANGE = 100_000 - val VIBRATE_PARAMETERS = arrayOf( - Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType, - String::class.java, - CombinedVibration::class.java, - VibrationAttributes::class.java, - String::class.java, - IBinder::class.java, - ) - } -} diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/NotificationsMasterModule.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/NotificationsMasterModule.kt index 81449e8..ea27761 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/NotificationsMasterModule.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/NotificationsMasterModule.kt @@ -17,9 +17,6 @@ class NotificationsMasterModule : XposedModule() { installBridge("notification attention") { NotificationAttentionBridge(this, ::diagnoseAttentionHelperEvent).install(param.classLoader) } - installBridge("direct notification vibration") { - DirectVibrationBridge(this).install(param.classLoader) - } installBridge("notification enqueue") { SystemNotificationBridge(this, ::diagnoseHelperEvent).install(param.classLoader) } diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/SystemAlertPolicyCache.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/SystemAlertPolicyCache.kt index 00c9a67..736582a 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/SystemAlertPolicyCache.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/SystemAlertPolicyCache.kt @@ -63,14 +63,7 @@ internal object SystemAlertPolicyCache { AlertPolicyEvaluator.evaluate(configuration, event, isRoutineUpdate = false) fun shouldSuppress(event: AlertEvent): Boolean = - (event.source.isDirect || playbackReady) && evaluate(event).suppressesOriginal() - - fun sendDirectAlerts(event: AlertEvent, userId: Int) { - val context = systemContext() ?: return - val decisions = evaluate(event).decisions - val sent = decisions.count { DirectAlertSync.send(context, it, userId) } - Log.i(TAG, "Sent $sent direct alert request(s) for ${event.packageName}") - } + playbackReady && evaluate(event).suppressesOriginal() private fun systemContext(): Context? = runCatching { val activityThread = Class.forName("android.app.ActivityThread") diff --git a/app/src/main/res/layout/fragment_app_rules.xml b/app/src/main/res/layout/fragment_app_rules.xml index 03002fa..9ffb2eb 100644 --- a/app/src/main/res/layout/fragment_app_rules.xml +++ b/app/src/main/res/layout/fragment_app_rules.xml @@ -25,18 +25,6 @@ android:layout_marginTop="8dp" android:text="Allow multiple matching rules" /> - - - - - diff --git a/test-helper/src/main/java/se/ajpanton/notificationsmaster/helper/MainActivity.kt b/test-helper/src/main/java/se/ajpanton/notificationsmaster/helper/MainActivity.kt index e293468..43185bd 100644 --- a/test-helper/src/main/java/se/ajpanton/notificationsmaster/helper/MainActivity.kt +++ b/test-helper/src/main/java/se/ajpanton/notificationsmaster/helper/MainActivity.kt @@ -9,9 +9,6 @@ import android.graphics.Bitmap import android.media.AudioAttributes import android.media.RingtoneManager import android.os.Bundle -import android.os.VibrationAttributes -import android.os.VibrationEffect -import android.os.VibratorManager import android.widget.Button import android.widget.LinearLayout import android.widget.ScrollView @@ -72,7 +69,6 @@ class MainActivity : AppCompatActivity() { "chronometer" -> postChronometer() "timeout" -> postTimeout() "group" -> postGroup() - "direct_vibration" -> directNotificationVibration() "cancel_text" -> manager.cancel(TEXT_ID) "cancel_all" -> manager.cancelAll() } @@ -125,13 +121,6 @@ class MainActivity : AppCompatActivity() { manager.notify(GROUP_SUMMARY, base().setContentTitle("Grouped summary").setGroup(GROUP_KEY).setGroupSummary(true).build()) } - private fun directNotificationVibration() { - getSystemService(VibratorManager::class.java).defaultVibrator.vibrate( - VibrationEffect.createWaveform(longArrayOf(0, 200), -1), - VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_NOTIFICATION).build(), - ) - } - private companion object { const val CHANNEL = "test" const val EXTRA_ACTION = "helper_action" @@ -156,7 +145,6 @@ class MainActivity : AppCompatActivity() { "Post very long text" to "very_long_text", "Post inbox" to "inbox", "Post progress" to "progress", "Update progress" to "progress_update", "Post chronometer" to "chronometer", "Post timeout" to "timeout", "Post group" to "group", - "Direct notification vibration" to "direct_vibration", "Cancel text" to "cancel_text", "Cancel all" to "cancel_all", ) }