From 24e68331fd62b57343c8ceb67a27b2bad74220a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 00:44:18 +0000 Subject: [PATCH] Warn on the Memos list when a reminder permission is missing The Memos tab now shows a tappable banner above the filter row whenever a system grant the chosen notification style depends on is missing. Which grants count is style-aware: notifications and exact alarms matter to every style; full-screen alarms and Do Not Disturb access only to the Alarm style, since without the full-screen grant (Android 14+) the Alarm style falls back to a heads-up whose sound lands on the notification stream and usually arrives silently. Tapping the banner opens Settings > Reminders & alerts, which now supports being opened directly on a sub-screen. The style-to-permission rule lives in a pure ReminderPermissionGrants model in the notification package, pinned by ReminderPermissionsTest. The Full-screen alarms row in Settings also now says alarms can arrive silently without it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VRyMjuZLuk1Eq1KnSukjDL --- .../dash/notification/ReminderPermissions.kt | 79 ++++++++++++++++ .../dash/permission/PermissionHelper.kt | 9 ++ .../ui/components/core/PermissionBanner.kt | 84 +++++++++++++++++ .../mapgie/dash/ui/navigation/DashNavGraph.kt | 12 ++- .../screens/reminders/RemindersListScreen.kt | 34 +++++++ .../reminders/RemindersListViewModel.kt | 4 + .../ui/screens/settings/SettingsScreen.kt | 18 +++- .../notification/ReminderPermissionsTest.kt | 94 +++++++++++++++++++ .../unreleased/memos-permission-banner.json | 9 ++ 9 files changed, 340 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/mapgie/dash/notification/ReminderPermissions.kt create mode 100644 app/src/main/java/com/mapgie/dash/ui/components/core/PermissionBanner.kt create mode 100644 app/src/test/java/com/mapgie/dash/notification/ReminderPermissionsTest.kt create mode 100644 changelog/unreleased/memos-permission-banner.json diff --git a/app/src/main/java/com/mapgie/dash/notification/ReminderPermissions.kt b/app/src/main/java/com/mapgie/dash/notification/ReminderPermissions.kt new file mode 100644 index 0000000..21a2412 --- /dev/null +++ b/app/src/main/java/com/mapgie/dash/notification/ReminderPermissions.kt @@ -0,0 +1,79 @@ +package com.mapgie.dash.notification + +/** + * The system permissions a reminder needs to reach the user, and which of them + * the chosen delivery mode actually depends on. + * + * Kept Android-free (no Context) so the rule "what counts as missing for this + * style" is pinned by a plain JVM test. `PermissionHelper.reminderGrants` takes + * the live snapshot; the Memos list turns the result into its banner. + * + * Ordered by how badly the gap hurts: no notifications means nothing is ever + * shown, no exact alarms means it fires late, no full-screen means the Alarm + * style cannot ring (see below), no Do Not Disturb access means it is muted + * only while Do Not Disturb is on. + */ +enum class ReminderPermission(val label: String) { + NOTIFICATIONS("Notifications"), + EXACT_ALARMS("Exact alarms"), + + /** + * Android 14+ lets the user revoke full-screen intents per app. Without it + * the Alarm style cannot launch its ring screen on a locked phone, so the + * alert falls back to a plain heads-up whose sound lands on the notification + * stream (LESSONS #52). On most phones that is silence: the alarm arrives, + * but it does not ring. The other styles never use a full-screen intent. + */ + FULL_SCREEN("Full-screen alarms"), + + /** Only the Alarm style promises to sound through Do Not Disturb. */ + DND_ACCESS("Do Not Disturb access"), +} + +/** A snapshot of the four grants. Platforms below a permission's API level report it as granted. */ +data class ReminderPermissionGrants( + val notifications: Boolean = true, + val exactAlarms: Boolean = true, + val fullScreen: Boolean = true, + val dndAccess: Boolean = true, +) { + /** + * The permissions the given delivery mode needs and does not have, most + * damaging first. Notifications and exact alarms matter to every style; the + * full-screen and Do Not Disturb grants only to the Alarm style, so a user on + * Notification or Silent is never nagged about them. + */ + fun missingFor(deliveryMode: String): List { + val alarm = DeliveryMode.ringsOnAlarmStream(deliveryMode) + return buildList { + if (!notifications) add(ReminderPermission.NOTIFICATIONS) + if (!exactAlarms) add(ReminderPermission.EXACT_ALARMS) + if (alarm && !fullScreen) add(ReminderPermission.FULL_SCREEN) + if (alarm && !dndAccess) add(ReminderPermission.DND_ACCESS) + } + } + + /** + * One line for the list-screen banner, or null when nothing the current style + * needs is missing. [plural] is the user's word for the feature ("memos", + * "reminders", "alarms"), lower case. A single gap is named with its effect; + * several are counted, since the Settings page they open lists each one. + */ + fun warningFor(deliveryMode: String, plural: String): String? { + val missing = missingFor(deliveryMode) + return when (missing.size) { + 0 -> null + 1 -> when (missing.single()) { + ReminderPermission.NOTIFICATIONS -> + "Notifications are off, so $plural cannot alert you. Tap to allow." + ReminderPermission.EXACT_ALARMS -> + "Exact alarms are off, so $plural may ring late. Tap to allow." + ReminderPermission.FULL_SCREEN -> + "Full-screen alarms are off, so $plural may ring silently. Tap to allow." + ReminderPermission.DND_ACCESS -> + "Do Not Disturb access is off, so $plural stay quiet during Do Not Disturb. Tap to allow." + } + else -> "${missing.size} permissions are off, so $plural may not ring. Tap to review." + } + } +} diff --git a/app/src/main/java/com/mapgie/dash/permission/PermissionHelper.kt b/app/src/main/java/com/mapgie/dash/permission/PermissionHelper.kt index a5f1097..878a955 100644 --- a/app/src/main/java/com/mapgie/dash/permission/PermissionHelper.kt +++ b/app/src/main/java/com/mapgie/dash/permission/PermissionHelper.kt @@ -7,6 +7,7 @@ import android.net.Uri import android.os.Build import android.provider.Settings import androidx.core.app.NotificationManagerCompat +import com.mapgie.dash.notification.ReminderPermissionGrants /** * Settings deep links for the permissions AlarmScheduler and NotificationHelper depend on. @@ -40,6 +41,14 @@ object PermissionHelper { .canUseFullScreenIntent() } + /** The four grants in one read, for [ReminderPermissionGrants.missingFor] and the list banners. */ + fun reminderGrants(context: Context): ReminderPermissionGrants = ReminderPermissionGrants( + notifications = areNotificationsEnabled(context), + exactAlarms = canScheduleExactAlarms(context), + fullScreen = canUseFullScreenIntent(context), + dndAccess = isDndAccessGranted(context), + ) + /** Opens this app's "Full-screen notifications" toggle (API 34+). */ fun fullScreenIntentSettingsIntent(context: Context): Intent = Intent( diff --git a/app/src/main/java/com/mapgie/dash/ui/components/core/PermissionBanner.kt b/app/src/main/java/com/mapgie/dash/ui/components/core/PermissionBanner.kt new file mode 100644 index 0000000..c3b0829 --- /dev/null +++ b/app/src/main/java/com/mapgie/dash/ui/components/core/PermissionBanner.kt @@ -0,0 +1,84 @@ +package com.mapgie.dash.ui.components.core + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.mapgie.dash.ui.theme.Dimens +import com.mapgie.dash.ui.theme.LucideIcons +import com.mapgie.dash.ui.theme.StatusTone +import com.mapgie.dash.ui.theme.badgeContainerColor +import com.mapgie.dash.ui.theme.textColor + +/** + * A tappable strip above a list warning that a system permission the screen's + * feature depends on is missing: alert glyph, one line of [text] naming the gap + * and its effect, and a chevron. Wears the shared ATTENTION (amber) tone, so it + * matches the "due soon" badges rather than the error red reserved for failures. + * The glyph and the wording carry the state; the tint is secondary. + * + * Tapping goes to wherever the gap can be fixed (the caller decides). Announced + * politely when it appears, since it usually shows on return from system settings. + */ +@Composable +fun PermissionBanner( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val container = StatusTone.ATTENTION.badgeContainerColor() ?: MaterialTheme.colorScheme.surfaceContainerHigh + val content = StatusTone.ATTENTION.textColor() + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + modifier = modifier + .fillMaxWidth() + .padding(horizontal = Dimens.cardInset) + .clip(MaterialTheme.shapes.medium) + .background(container) + .semantics { + role = Role.Button + liveRegion = LiveRegionMode.Polite + } + .clickable(onClick = onClick) + .heightIn(min = 44.dp) + .padding(horizontal = 14.dp, vertical = 10.dp), + ) { + Icon( + imageVector = LucideIcons.CircleAlert, + contentDescription = null, + tint = content, + modifier = Modifier.size(18.dp), + ) + Text( + text = text, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = content, + modifier = Modifier.weight(1f), + ) + Icon( + imageVector = LucideIcons.ChevronRight, + contentDescription = null, + tint = content, + modifier = Modifier.size(18.dp), + ) + } +} diff --git a/app/src/main/java/com/mapgie/dash/ui/navigation/DashNavGraph.kt b/app/src/main/java/com/mapgie/dash/ui/navigation/DashNavGraph.kt index 85deb73..6a8c0bd 100644 --- a/app/src/main/java/com/mapgie/dash/ui/navigation/DashNavGraph.kt +++ b/app/src/main/java/com/mapgie/dash/ui/navigation/DashNavGraph.kt @@ -42,6 +42,7 @@ import com.mapgie.dash.ui.screens.reminder.ReminderViewScreen import com.mapgie.dash.ui.screens.reminder.reminderViewRoute import com.mapgie.dash.ui.screens.reminders.RemindersListScreen import com.mapgie.dash.ui.screens.settings.SettingsScreen +import com.mapgie.dash.ui.screens.settings.SettingsSubScreen import com.mapgie.dash.ui.screens.tasks.TaskListScreen import com.mapgie.dash.ui.theme.LocalTypeAccents import com.mapgie.dash.ui.theme.LucideIcons @@ -108,6 +109,9 @@ fun DashNavGraph( var fabExpanded by remember { mutableStateOf(false) } var pendingAddIntent by remember { mutableStateOf(null) } + // A sub-screen the Settings tab should open on arrival (e.g. the Memos + // permission banner sends the user to Reminders & alerts). + var pendingSettingsSubScreen by remember { mutableStateOf(null) } val navUiState by navViewModel.uiState.collectAsStateWithLifecycle() // The Memos/Reminders slot is always present, so the five-slot bar never @@ -250,12 +254,18 @@ fun DashNavGraph( composable(Screen.Reminders.route) { RemindersListScreen( pendingAddIntent = pendingAddIntent, - onPendingAddIntentConsumed = { pendingAddIntent = null } + onPendingAddIntentConsumed = { pendingAddIntent = null }, + onOpenReminderSettings = { + pendingSettingsSubScreen = SettingsSubScreen.REMINDERS + navigateTo(Screen.Settings.route) + }, ) } composable(Screen.Settings.route) { SettingsScreen( onNavigateToLicenses = { navController.navigate("licenses") }, + pendingSubScreen = pendingSettingsSubScreen, + onPendingSubScreenConsumed = { pendingSettingsSubScreen = null }, ) } composable("licenses") { diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/reminders/RemindersListScreen.kt b/app/src/main/java/com/mapgie/dash/ui/screens/reminders/RemindersListScreen.kt index 0446906..06dd56b 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/reminders/RemindersListScreen.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/reminders/RemindersListScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -40,23 +41,29 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import com.mapgie.dash.data.model.AddMenuOption import com.mapgie.dash.data.model.NEW_DRAFT_KEY import com.mapgie.dash.data.model.ReminderAppearance import com.mapgie.dash.data.model.ReminderDto import com.mapgie.dash.data.model.ReminderSortKey import com.mapgie.dash.data.model.Swatch +import com.mapgie.dash.permission.PermissionHelper import com.mapgie.dash.ui.components.AddReminderSheet import com.mapgie.dash.ui.components.ReminderCard import com.mapgie.dash.ui.components.core.HeaderIconButton import com.mapgie.dash.ui.components.core.LocalReminderLabel import com.mapgie.dash.ui.components.core.PageHeader +import com.mapgie.dash.ui.components.core.PermissionBanner import com.mapgie.dash.ui.components.core.SearchRow import com.mapgie.dash.ui.components.core.SectionLabel import com.mapgie.dash.ui.components.core.SortControls @@ -76,18 +83,37 @@ import kotlinx.coroutines.launch * * The owner ("mine / all") header action the design shows is not built: memos * carry no owner, so there is nothing to filter by. + * + * Above the filter row, a permission banner appears whenever a system grant the + * chosen notification style depends on is missing (full-screen alarms for the + * Alarm style, say), since a memo that fires silently is worse than none. + * Tapping it opens Settings › Reminders & alerts via [onOpenReminderSettings]. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun RemindersListScreen( pendingAddIntent: AddMenuOption?, onPendingAddIntentConsumed: () -> Unit, + onOpenReminderSettings: () -> Unit, viewModel: RemindersListViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() val snackbarHost = remember { SnackbarHostState() } val scope = rememberCoroutineScope() + // The system grants reminders depend on, re-read on every resume so a change + // made in system settings shows the moment the user comes back. + val context = LocalContext.current + var grants by remember { mutableStateOf(PermissionHelper.reminderGrants(context)) } + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) grants = PermissionHelper.reminderGrants(context) + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + // Swipe-to-done leaves a brief Undo, so a stray swipe is one tap to reverse. fun markReminderDoneWithUndo(reminder: ReminderDto) { viewModel.setReminderDone(reminder.id, true) @@ -199,6 +225,14 @@ fun RemindersListScreen( return@Scaffold } + grants.warningFor(uiState.deliveryMode, plural)?.let { warning -> + PermissionBanner( + text = warning, + onClick = onOpenReminderSettings, + modifier = Modifier.padding(top = 4.dp, bottom = 2.dp), + ) + } + // Filter chips, then the sort pill pinned to the right. Row( modifier = Modifier diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/reminders/RemindersListViewModel.kt b/app/src/main/java/com/mapgie/dash/ui/screens/reminders/RemindersListViewModel.kt index 96f97cf..2011550 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/reminders/RemindersListViewModel.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/reminders/RemindersListViewModel.kt @@ -24,6 +24,7 @@ import com.mapgie.dash.data.preferences.SettingsRepository import com.mapgie.dash.data.repository.ChoreRepository import com.mapgie.dash.data.repository.ReminderRepository import com.mapgie.dash.data.repository.TaskRepository +import com.mapgie.dash.notification.DeliveryMode import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -61,6 +62,8 @@ data class ReminderUiState( /** Settings › Categories styling and Settings › Colours axes, so a linked memo can wear its chore's or task's look. */ val catalog: CategoryCatalog = CategoryCatalog(), val colourAxes: ChoreColourAxes = ChoreColourAxes(), + /** Settings › Reminders & alerts style; decides which missing permissions the list warns about. */ + val deliveryMode: String = DeliveryMode.NOTIFICATION, ) { val active: List get() = sorted(reminders.filter { it.archivedAt == null && !it.isDone }) @@ -147,6 +150,7 @@ class RemindersListViewModel @Inject constructor( reminderLabel = settings.reminderLabel, sort = settings.reminderSort, colourAxes = settings.colourAxes, + deliveryMode = settings.deliveryMode, ) } } diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsScreen.kt index 7eb408b..5bf4e42 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/settings/SettingsScreen.kt @@ -97,7 +97,7 @@ import java.io.BufferedReader import java.io.InputStreamReader import kotlin.math.roundToInt -private enum class SettingsSubScreen { +enum class SettingsSubScreen { NONE, CONNECTION, APPEARANCE, COLOURS, CATEGORIES, DISPLAY, QUICK_ADD, REMINDERS, WIDGET, ABOUT, HELP } @@ -106,13 +106,27 @@ private const val CHANGELOG_URL = "https://github.com/mapgie/choreDash-Android/b /** Content inset for every settings page (18dp per the 3a/4a mock-ups). */ private val PageInset = 18.dp +/** + * The Settings tab. [pendingSubScreen] opens a sub-screen directly (the Memos + * permission banner lands on Reminders & alerts this way); it is consumed once + * applied so the tab's own back navigation works as usual afterwards. + */ @Composable fun SettingsScreen( onNavigateToLicenses: () -> Unit, + pendingSubScreen: SettingsSubScreen? = null, + onPendingSubScreenConsumed: () -> Unit = {}, viewModel: SettingsViewModel = hiltViewModel() ) { var subScreen by rememberSaveable { mutableStateOf(SettingsSubScreen.NONE) } + LaunchedEffect(pendingSubScreen) { + if (pendingSubScreen != null) { + subScreen = pendingSubScreen + onPendingSubScreenConsumed() + } + } + BackHandler(enabled = subScreen != SettingsSubScreen.NONE) { subScreen = SettingsSubScreen.NONE } @@ -988,7 +1002,7 @@ private fun RemindersSubScreen( SettingsHairline() PermissionRow( title = "Full-screen alarms", - subtitle = "Lets the Alarm style turn the screen on and ring", + subtitle = "Lets the Alarm style turn the screen on and ring. Without it, alarms can arrive silently", granted = fullScreenAllowed, icon = LucideIcons.Lamp, onClick = { context.startActivity(PermissionHelper.fullScreenIntentSettingsIntent(context)) } diff --git a/app/src/test/java/com/mapgie/dash/notification/ReminderPermissionsTest.kt b/app/src/test/java/com/mapgie/dash/notification/ReminderPermissionsTest.kt new file mode 100644 index 0000000..2d420ae --- /dev/null +++ b/app/src/test/java/com/mapgie/dash/notification/ReminderPermissionsTest.kt @@ -0,0 +1,94 @@ +package com.mapgie.dash.notification + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Which missing permissions the Memos list warns about, per delivery style. The + * Alarm style is the only one that rings through a full-screen intent, so it is + * the only one told when that grant is gone; every style needs notifications and + * exact alarms. Pure logic, no Android involved. + */ +class ReminderPermissionsTest { + + private val all = ReminderPermissionGrants() + + @Test + fun `nothing missing shows no warning in any style`() { + for (mode in listOf(DeliveryMode.ALARM, DeliveryMode.NOTIFICATION, DeliveryMode.SILENT)) { + assertTrue(all.missingFor(mode).isEmpty()) + assertNull(all.warningFor(mode, "memos")) + } + } + + @Test + fun `alarm style without full-screen warns that memos may ring silently`() { + val grants = all.copy(fullScreen = false) + assertEquals(listOf(ReminderPermission.FULL_SCREEN), grants.missingFor(DeliveryMode.ALARM)) + assertEquals( + "Full-screen alarms are off, so memos may ring silently. Tap to allow.", + grants.warningFor(DeliveryMode.ALARM, "memos"), + ) + } + + @Test + fun `notification and silent styles ignore full-screen and do not disturb access`() { + val grants = all.copy(fullScreen = false, dndAccess = false) + assertTrue(grants.missingFor(DeliveryMode.NOTIFICATION).isEmpty()) + assertTrue(grants.missingFor(DeliveryMode.SILENT).isEmpty()) + assertNull(grants.warningFor(DeliveryMode.NOTIFICATION, "memos")) + } + + @Test + fun `every style needs notifications and exact alarms`() { + val grants = all.copy(notifications = false, exactAlarms = false) + val expected = listOf(ReminderPermission.NOTIFICATIONS, ReminderPermission.EXACT_ALARMS) + assertEquals(expected, grants.missingFor(DeliveryMode.SILENT)) + assertEquals(expected, grants.missingFor(DeliveryMode.NOTIFICATION)) + assertEquals(expected, grants.missingFor(DeliveryMode.ALARM)) + } + + @Test + fun `missing permissions are listed most damaging first`() { + val none = ReminderPermissionGrants( + notifications = false, exactAlarms = false, fullScreen = false, dndAccess = false, + ) + assertEquals( + listOf( + ReminderPermission.NOTIFICATIONS, + ReminderPermission.EXACT_ALARMS, + ReminderPermission.FULL_SCREEN, + ReminderPermission.DND_ACCESS, + ), + none.missingFor(DeliveryMode.ALARM), + ) + } + + @Test + fun `one missing permission is named, several are counted`() { + assertEquals( + "Notifications are off, so reminders cannot alert you. Tap to allow.", + all.copy(notifications = false).warningFor(DeliveryMode.NOTIFICATION, "reminders"), + ) + assertEquals( + "Exact alarms are off, so alarms may ring late. Tap to allow.", + all.copy(exactAlarms = false).warningFor(DeliveryMode.SILENT, "alarms"), + ) + assertEquals( + "Do Not Disturb access is off, so memos stay quiet during Do Not Disturb. Tap to allow.", + all.copy(dndAccess = false).warningFor(DeliveryMode.ALARM, "memos"), + ) + assertEquals( + "2 permissions are off, so memos may not ring. Tap to review.", + all.copy(fullScreen = false, dndAccess = false).warningFor(DeliveryMode.ALARM, "memos"), + ) + } + + @Test + fun `an unknown stored mode is treated as a plain notification`() { + assertTrue(all.copy(fullScreen = false).missingFor("LOUD").isEmpty()) + assertEquals(listOf(ReminderPermission.NOTIFICATIONS), all.copy(notifications = false).missingFor("")) + } +} diff --git a/changelog/unreleased/memos-permission-banner.json b/changelog/unreleased/memos-permission-banner.json new file mode 100644 index 0000000..c986219 --- /dev/null +++ b/changelog/unreleased/memos-permission-banner.json @@ -0,0 +1,9 @@ +{ + "bump": "minor", + "added": [ + "The Memos list now shows a banner when a permission the chosen notification style needs is missing (for example full-screen alarms, without which the Alarm style can arrive silently). Tapping it opens Settings > Reminders & alerts." + ], + "changed": [ + "The Full-screen alarms row in Reminders & alerts now says that without it alarms can arrive silently." + ] +}