diff --git a/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AndroidAlertEffectPlayerTest.kt b/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AndroidAlertEffectPlayerTest.kt index 182f0bd..7ddc1a8 100644 --- a/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AndroidAlertEffectPlayerTest.kt +++ b/app/src/androidTest/java/se/ajpanton/notificationsmaster/alerts/AndroidAlertEffectPlayerTest.kt @@ -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() + } } diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt b/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt index f4c0e11..a8c005f 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt @@ -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() diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AndroidAlertEffectPlayer.kt b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AndroidAlertEffectPlayer.kt index baf806c..4a214a3 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AndroidAlertEffectPlayer.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AndroidAlertEffectPlayer.kt @@ -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) } diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/DndPolicy.kt b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/DndPolicy.kt new file mode 100644 index 0000000..afc06cf --- /dev/null +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/DndPolicy.kt @@ -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 +} diff --git a/app/src/test/java/se/ajpanton/notificationsmaster/alerts/DndPolicyTest.kt b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/DndPolicyTest.kt new file mode 100644 index 0000000..57bc793 --- /dev/null +++ b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/DndPolicyTest.kt @@ -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)) } + } +}