Respect Do Not Disturb for custom alerts

This commit is contained in:
ajp_anton
2026-08-18 16:16:47 +00:00
parent c9d189ac97
commit bd727eaec0
5 changed files with 66 additions and 2 deletions
@@ -1,5 +1,6 @@
package se.ajpanton.notificationsmaster.alerts
import android.app.NotificationManager
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -20,4 +21,20 @@ class AndroidAlertEffectPlayerTest {
assertTrue(completion.await(2, TimeUnit.SECONDS))
player.stop()
}
@Test
fun dndSkipsPlaybackAndCompletesImmediately() {
val completion = CountDownLatch(1)
val player = AndroidAlertEffectPlayer(
InstrumentationRegistry.getInstrumentation().targetContext,
) { NotificationManager.INTERRUPTION_FILTER_NONE }
player.play(
QueuedAlert("test", AlertSnapshot(null, listOf(0, 1_000), false, false)),
completion::countDown,
)
assertTrue(completion.await(100, TimeUnit.MILLISECONDS))
player.stop()
}
}
@@ -55,8 +55,15 @@ object AlertRuleDialog {
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 }
val dnd = SwitchMaterial(context).apply { text = "Allow during DND when supported"; isChecked = existing?.allowDuringDnd == true }
val dnd = SwitchMaterial(context).apply {
text = "Allow during DND (requires system support)"
isChecked = existing?.allowDuringDnd == true
}
listOf(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
})
val dialog = MaterialAlertDialogBuilder(context).setTitle(if (existing == null) "Add rule" else "Edit rule")
.setView(form).setNegativeButton("Cancel", null).setPositiveButton("Save", null)
.apply { if (onDelete != null) setNeutralButton("Delete", null) }.show()
@@ -1,6 +1,7 @@
package se.ajpanton.notificationsmaster.alerts
import android.content.Context
import android.app.NotificationManager
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.net.Uri
@@ -12,7 +13,12 @@ import android.os.VibratorManager
import android.util.Log
/** Plays resolved notification-class effects and releases all platform resources on stop. */
class AndroidAlertEffectPlayer(context: Context) : AlertEffectPlayer {
class AndroidAlertEffectPlayer(
context: Context,
private val interruptionFilter: () -> Int = {
context.getSystemService(NotificationManager::class.java).currentInterruptionFilter
},
) : AlertEffectPlayer {
private val context = context.applicationContext
private val handler = Handler(Looper.getMainLooper())
private val vibrator = context.getSystemService(VibratorManager::class.java).defaultVibrator
@@ -27,6 +33,12 @@ class AndroidAlertEffectPlayer(context: Context) : AlertEffectPlayer {
val token = generation
completion = onFinished
val snapshot = alert.snapshot
if (!DndPolicy.allowsCustomAlert(interruptionFilter())) {
Log.i(TAG, "Skipping custom notification alert during Do Not Disturb")
completion = null
onFinished()
return
}
val vibrationDuration = snapshot.vibrationPattern.sum()
remainingEffects = (if (snapshot.soundUri == null) 0 else 1) + (if (vibrationDuration > 0) 1 else 0)
snapshot.soundUri?.let { startSound(Uri.parse(it), token) }
@@ -0,0 +1,9 @@
package se.ajpanton.notificationsmaster.alerts
import android.app.NotificationManager
/** Direct custom effects must not bypass the user's Do Not Disturb choice. */
internal object DndPolicy {
fun allowsCustomAlert(interruptionFilter: Int) =
interruptionFilter == NotificationManager.INTERRUPTION_FILTER_ALL
}
@@ -0,0 +1,19 @@
package se.ajpanton.notificationsmaster.alerts
import android.app.NotificationManager
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class DndPolicyTest {
@Test
fun allowsCustomAlertsOnlyWhenInterruptionsAreNormal() {
assertTrue(DndPolicy.allowsCustomAlert(NotificationManager.INTERRUPTION_FILTER_ALL))
listOf(
NotificationManager.INTERRUPTION_FILTER_UNKNOWN,
NotificationManager.INTERRUPTION_FILTER_PRIORITY,
NotificationManager.INTERRUPTION_FILTER_NONE,
NotificationManager.INTERRUPTION_FILTER_ALARMS,
).forEach { assertFalse(DndPolicy.allowsCustomAlert(it)) }
}
}