Notifications in Android #288
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Add notifications that pop up whenever a reminder is due, along with a "DONE" quick action.
Plan
The Android app posts a notification when a reminder command (R29) becomes due, with a Done action that marks it as done exactly as the banner in the app does. This needs one new function in the core: when the next reminder becomes due. Everything else is on the Android side. The biggest change there is moving the stream and the repository out of the
StreamViewModelinto an object owned by the application, so a background worker can share them.Decisions (clarified with the user)
sincechanges) and again once a day for as long as it stays due. It is withdrawn once the reminder is no longer due.1. Core: the next due moment
crates/streamd-core/src/commands/reminder/due.rsnext_due<S: Borrow<LocalizedShard>>(commands: &IndexMap<String, CommandDefinition>, shards: &[S], now, tz) -> Option<DateTime<Utc>>returns the earliest moment afternowat which a reminder that is not due atnowbecomes due:Trigger::Days(n):start_of_day(max(last, start) + n, tz)(localize::start_of_day, DST-safe). It isNoneif the reminder is already due (this includes the case of no last match and nostart).Trigger::Since(recurrence): the end ofrecurrence.span(start, now, tz), i.e. the next occurrence afternow. It isNoneif the reminder is due or the recurrence ends.expected_momentsandfinding. Do not duplicate the rules ofdue_after_days/due_since.startand with a futurestart;sincedaily, timed (BYHOUR=0,12) and across a DST change inEurope/Berlin; an already-due reminder contributes nothing; the earliest of several wins.crates/streamd-core/src/api.rs:next_reminder_due(project, stream, now) -> Option<DateTime<Utc>>, with a test on aMemoryStore.2. Bindings
crates/streamd-ffiProject.next_reminder_due(stream, now) -> Option<Timestamp>inproject.rs, with a test.DueReminder(records.rs) getstext, the description or else the key (reminder::DueReminder.text), so the notification can use it as its title.summarystays.3. Android: one stream per application
Today the
RepositoryConnectionand theStreamSessionbelong to theStreamViewModel. A worker would otherwise need a secondRepositoryConnectionon the same clone, with its own lock, which risks racing JGit.StreamdApplication : Application(android:namein the manifest) holds a new classStreamd(Streamd.kt).Streamdowns theRepositoryConnection, theStreamSessionflow (made anew whenever theConnectionchanges, as now) and thePinStore, in an applicationCoroutineScope. It also hassuspend fun ready(): StreamSession?, which starts the connection and waits for a loaded stream; the workers use it.StreamViewModeltakes everything from(application as StreamdApplication).streamdand keeps only what belongs to the screens. Its behaviour does not change. Because the session is shared, the app shows a done marked from a notification without reloading.4. Android: notifications
New package
notify/. Nothing outside it namesNotificationManageror WorkManager, apart from the manifest andStreamdApplication.androidx.work:work-runtime-ktxinlibs.versions.tomlandapp/build.gradle.kts. Writeandroid/deps.jsonagain with the update script and commit it.POST_NOTIFICATIONS(minSdk is 33, so it is a runtime permission). No boot receiver is needed, since WorkManager keeps its jobs across a reboot.ReminderWorker(CoroutineWorker), unique workreminders:streamd.ready(); if no repository is connected, do nothing.ConnectivityManager),repository.pull()and thensession.reload(). Ignore a failure.session.reminders(now)gives what is due;notifications.show(due, now)(below).session.nextDue(now)(bindings) and, if anything is still due, the next day's re-alert time. This is a uniqueOneTimeWorkRequestwithExistingWorkPolicy.REPLACEand aninitialDelay.PeriodicWorkRequestof 3 hours (unique,KEEP), enqueued inStreamdApplication.onCreate.StreamStatebecomes anotherReady(a save, a pull, a done, a changed configuration),Streamdruns a cheap check without a pull. It withdraws what is no longer due, posts what became due and reschedules. This keeps notifications in step with edits made in the app.ReminderNotifications:reminders("Reminders", default importance).keyand a tag ofkey.textand the body is thesummary. Tapping it opensMainActivityon the Stream tab, where the banner is.canMarkDone.ReminderAlerts.ktwith unit tests. It is given the due reminders, the record of what was alerted (key → since, local date of the last alert, kept in a small DataStorealerts),nowand the zone. It returns what to post with sound, what to update silently (setOnlyAlertOnce), what to cancel and the new record.sincechanged.since), not by every check.PendingIntentto aBroadcastReceiver(ReminderDoneReceiver) that enqueues an expeditedDoneWorkerwith thekey. A receiver must not do the work itself. TheDoneWorker:streamd.ready()andsession.reminderDone(key, now).MarkedDone.Done(id):repository.written(id), which commits and pushes according to the settings, the same pathStreamViewModel.markDonetakes. Cancel the notification and drop its record.MarkedDone.Failed(reason): replace the notification with one saying the reason, keeping Done.POST_NOTIFICATIONSonce, the first time the stream isReadywith at least one reminder command (CommandKindof the summaries). UserememberLauncherForActivityResultinApp.kt, and remember in the DataStore that it was asked. If it was denied, nothing more is asked and the workers still run; posting is skipped whenNotificationManagerCompat.areNotificationsEnabled()is false.5. Tests
cargo testandnix flake check.ReminderAlerts: a new key alerts, the samesincethe same day updates silently, the next day before 08:00 stays silent, the next day after 08:00 alerts again, a changedsincealerts, a key no longer due is cancelled and dropped.StreamViewModelrefactor must leave the screenshot tests unchanged. The screens do not change, since only the content composables are drawn.nix run .#emulator): a timed remindersince = "FREQ=MINUTELY;INTERVAL=5"notifies, Done writes, commits and pushes and the notification goes away, the app shows the done without a reload, a denied permission posts nothing, and it still works after a reboot.6. Documentation
REQUIREMENTS.md:next_duereturns).README.md: the notifications of the app.AGENTS.md:Streamd/StreamdApplicationand that the view model takes the stream from it.notify/package, and that nothing outside it names WorkManager orNotificationManager.next_dueindue.rsandnext_reminder_duein the facade list.deps.jsonwas written again for.Commits (suggested)
feat(reminder): the moment the next reminder becomes duefeat(ffi): nextReminderDue and the text of a due reminderrefactor(android): the stream and the repository belong to the applicationfeat(android): notifications of due reminders with a Done actiondocs: notifications of reminders on AndroidImplementation report
Implemented in PR #290 (branch
288_android-reminder-notifications), as five commits that follow the plan.Findings
next_due. It is built on the existingfinding/expected_moments: a reminder that is due now contributes nothing, adaysrule comes due atstart_of_day(max(last, start) + N), and asincerule comes due at the end ofRecurrence::span. The DST test (Berlin, 2026-10-25) confirmed the wall-clock behaviour.Remindergainedtextandsince. The default values keep the existing tests and screenshot fixtures unchanged.Streamd) was a clean cut. The view model'sinitnow only callsstreamd.start(), which runs once per process, followed by the pull-on-open. The screenshot tests did not change.deps.jsonbuilds from the flake, which only sees files Git knows. The new Kotlin files have to be staged (git add) first, or the script fails with "unresolved reference".REPLACEon its own unique work, so WorkManager cancels the running worker. This happens after the check has finished, so nothing is lost, but the run shows as cancelled.288fe85; that sentence has been dropped.