feat(reminders): reminder notifications — EVENT_REMINDER receiver, onboarding step, settings toggle (v1.4)
Calendula now posts event reminders itself (the Etar model): the provider schedules the alarms and broadcasts EVENT_REMINDER, but a calendar app must turn them into visible notifications — essential for users whose only calendar app this is. A manifest-registered, exported receiver (data scheme content://com.android.calendar) wakes us at reminder time; no foreground service, no own alarm scheduling. Delivery path (data/reminders/): EventReminderReceiver (Hilt, goAsync) → ReminderAlertStore queries CalendarAlerts for STATE_SCHEDULED rows with ALARM_TIME <= now → ReminderNotifier posts one notification per alert on a dedicated high-importance channel, then best-effort marks rows FIRED (needs WRITE_CALENDAR; without it a re-broadcast silently replaces — tag per alert + setOnlyAlertOnce). Swiped notifications never return: FIRED rows are never re-queried, so no dismiss-intent machinery. Research (AOSP CalendarAlarmManager): the provider creates alert rows only for METHOD_ALERT reminders, so the email-reminder filter happens upstream. Tapping opens the event's detail screen: MainActivity is singleTop now, parses eventId/begin/end extras (onCreate + onNewIntent) into Compose state, and CalendarHost consumes the key exactly like an event tap. Onboarding gained a one-time second step after the calendar grant (shared OnboardingScaffold extracted from PermissionScreen): explains delivery, warns that a second calendar app with notifications on duplicates reminders, requests POST_NOTIFICATIONS (dialog on API 33+ only; minSdk 29). "Not now" turns the feature off; reminders default ON. Settings mirrors the toggle in a new Notifications section with the duplicate hint, and re-requests the permission when enabling. Strings DE+EN. Deliberately deferred (roadmap): snooze/dismiss actions, BOOT_COMPLETED / exact-alarm scheduling, battery-exemption prompts. Tests: reminderTimeText (all-day UTC-midnight reading, exclusive end day, midnight-crossing ranges), reminders/onboarding pref round-trips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -29,9 +30,18 @@ import kotlinx.datetime.LocalDate
|
||||
* Holds the active top-level view (spec M1) and swaps between the calendar
|
||||
* screens. Each screen owns its own ViewModel and date anchor; the view-switcher
|
||||
* pill in their top bars writes back here via [onSelectView].
|
||||
*
|
||||
* [requestedDetailKey] is an externally requested occurrence (a tapped
|
||||
* reminder notification routed through MainActivity): it opens the detail
|
||||
* overlay exactly like an event tap and is cleared via [onDetailKeyConsumed]
|
||||
* so a later recomposition can't re-open it.
|
||||
*/
|
||||
@Composable
|
||||
fun CalendarHost(modifier: Modifier = Modifier) {
|
||||
fun CalendarHost(
|
||||
modifier: Modifier = Modifier,
|
||||
requestedDetailKey: LongArray? = null,
|
||||
onDetailKeyConsumed: () -> Unit = {},
|
||||
) {
|
||||
var view by rememberSaveable { mutableStateOf(CalendarView.Week) }
|
||||
val onSelectView: (CalendarView) -> Unit = { view = it }
|
||||
|
||||
@@ -61,6 +71,15 @@ fun CalendarHost(modifier: Modifier = Modifier) {
|
||||
detailKey = key
|
||||
}
|
||||
|
||||
// A tapped reminder notification asks for a specific occurrence.
|
||||
LaunchedEffect(requestedDetailKey) {
|
||||
if (requestedDetailKey != null) {
|
||||
heldKey = requestedDetailKey
|
||||
detailKey = requestedDetailKey
|
||||
onDetailKeyConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
// Settings (M4) is hoisted here so it overlays whichever calendar view is
|
||||
// active and survives view switches. (The calendar filter now lives inline
|
||||
// in the navigation drawer, so no overlay state is needed for it.)
|
||||
|
||||
Reference in New Issue
Block a user