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",
)
}