Review follow-up on the midnight alarm.
The month widget could still show the wrong month after the rollover, and by way
of the exact workaround #228's reporter described. ShiftMonthAction stored an
absolute index on every tap, so paging forward and back — which looks like a
no-op and was how people forced a redraw — silently pinned the widget to the
month that was current at the time. It kept redrawing correctly and kept drawing
August into September, today's circle nowhere on the grid. Landing back on the
current month now clears the key instead of writing it, so the widget goes back
to following the date. Paging somewhere else and staying there is left alone;
that one is a choice, and the today button undoes it.
The alarm also had no way back once something dropped it without telling us — a
force-stop, a battery-restricted transition, an OEM freeze — short of the user
opening the app. onUpdate is the one wake-up the system still owns through
updatePeriodMillis, so both receivers re-arm from it. That doubles as a window
narrower: setAndAllowWhileIdle's delivery slack scales with how far out the alarm
was set, so re-arming every half hour keeps midnight's window at minutes rather
than hours.
Boot and package-replace now re-arm and stop there. The host sends
APPWIDGET_UPDATE after both, so the redraw was a second pair of wide provider
reads and RemoteViews serialisations in a cold process, exactly when the device
is busiest.
Smaller things: the unreachable lookahead fallback armed 5 seconds out, which
would have been a wake-loop rather than the late redraw the comment claimed —
an hour now. The action guard's comment was copied from ReminderScheduleReceiver
along with its claim that the broadcasts are protected; PROVIDER_CHANGED is not,
and the real reason the guard is worth having is that the receiver must stay
exported. Application start does its sync off the main thread now.
Tests moved onto frozen historical transitions — Berlin 2024 both ways, Havana
2018 — since the 2026 dates they used depend on DST rules that can still change
under a tzdata bump. The Berlin "fall back" case never repeated midnight, so it
now says what it actually checks (a 25-hour day must not overshoot) and a real
repeated-midnight zone, pre-2019 Sao Paulo, pins the known limitation instead of
implying it is handled. Dropped the two assertions that restated the code, and
added the one that was missing: that the receiver's action guard admits the
action the alarm is sent with, which is the single point where the whole thing
would fail silently.
Both home-screen widgets kept highlighting yesterday as "today" after the date
changed — and the agenda kept dimming events against yesterday — until the user
paged the month arrows or removed and re-added the widget.
The data layer was never the problem: the month cache guard already drops its
window as soon as the anchor date differs, which is exactly why an arrow tap
fixed it instantly. What was missing was anything to *trigger* a redraw at the
day boundary.
The manifest asked for DATE_CHANGED and the receiver's KDoc presented it as the
rollover mechanism, but DATE_CHANGED is not on the implicit-broadcast exemption
list, so a manifest-declared receiver has not been given it since Android 8 —
this has never worked on any device the app supports. That left only
updatePeriodMillis, which the system defers in doze and OxygenOS-style skins
throttle harder still; the reporter's is a 30-minute backstop that in practice
never ran.
So the app now holds its own wake-up. WidgetRolloverScheduler arms a single
alarm for just after the next local midnight and every firing re-arms the next,
the same shape as ReminderAlarmScheduler. It is deliberately inexact:
setAndAllowWhileIdle needs no permission and survives doze, and a rollover a few
minutes late is invisible on a sleeping screen — exact alarms stay reserved for
reminder snooze. The target is the *actual* start of the day rather than a
literal 00:00, so it stays right in Havana, where DST means midnight does not
happen, and it walks on past a whole date skipped by a date-line move.
The alarm is only armed while a widget is actually placed: onEnabled/onDisabled
on both Glance receivers re-sync it, and sync() cancels only when neither kind is
left, so removing the month widget never stops the agenda one rolling over.
WidgetUpdateReceiver re-arms on everything that can invalidate the alarm — boot
and package-replace wipe it, a clock or timezone change moves the boundary it
was aimed at — and CalendulaApp does the same on start, which is what arms
existing installs that upgrade into this without re-adding their widget. It also
gained an action allowlist, matching ReminderScheduleReceiver: it is exported and
the broadcasts it takes are protected.
DATE_CHANGED stays in the filter as a free extra for any OEM that does deliver
it, but the docs no longer claim anything depends on it.
Adding the two overrides makes the receivers as structurally alike as the widgets
they wrap, which is how #89 collapsed Glance's provider map, so the keep rule now
covers GlanceAppWidgetReceiver as well. Verified in the releaseTest mapping: all
four classes keep their real names.
Tests cover the arithmetic that decides when to wake — ordinary days, the
re-arming instant itself, both DST transitions, a zone whose midnight does not
exist, a half-hour offset, and the same instant seen from two zones.
Closes#228.
Calendula is published on Google Play, so the pipeline and the docs stop treating it as pending.
**README**
- Google Play badge next to F-Droid/Obtainium, linking the store page.
- Install table: the Play row goes from "Coming soon / —" to a real channel.
- The "coming soon" section becomes a live one with the store link; the closed-testing "testers wanted" call drops out. The note that Play signs with Google's key (so switching channels needs an uninstall) stays, and the trailing shared-signing-key line now flags Play as the exception.
**Play track**
- Release uploads default to `production` instead of `internal` — in the lane (`fastlane/Fastfile`) and in the workflow default (`.gitea/workflows/release.yaml`). Merging a bumped versionName to main is already the human gate, so the manual Console promotion only added delay.
- `docs/RELEASING.md` updated to match, including the escape hatches (`PLAY_TRACK=internal` to stage, `PLAY_RELEASE_STATUS=draft` to hold).
Note: if a `PLAY_TRACK` repo variable is set on Gitea it still wins over the new default — needs checking there.
No issue for this one.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/244
Surface the community translations that landed via Weblate but were never
listed in locales_config.xml. European Portuguese stays out: it has one
translated string.
Release 2.19.2 — `versionName` 2.19.2 / `versionCode` 21902. Merging this cuts the release.
A meeting you declined is struck through everywhere it appears and no longer schedules a reminder (#180). Tapping an event in month view opens the event instead of its day, in every month style (#187). An edit shows immediately when the same occurrence is re-opened (#196).
The month widget's arrows died after a couple of taps: the grid serialised ~740 views and one update ran to 450–540 KB, which overran the launcher's async binder buffer and made Android drop the whole widget host — killing updates for every widget on the home screen. The grid is down to 192 views, and `SizeMode.Exact` is kept so a resized widget reflows instead of clipping (#214). Widget taps now redraw the tapped widget by its own id rather than `updateAll`, whose provider lookup is empty in a cold-woken process (#18).
Jump-to-today in seamless weeks lands on the current week row (#191), and the day and week timelines got an end inset (#192).
Two follow-ups beyond the issues: the detail re-read keeps the loaded content on screen instead of blinking back to the skeleton, and a month chip tap with no recorded touch down (TalkBack, D-pad) no longer resolves to the previous tap's chip.
Release notes: `CHANGELOG.md` 2.19.2 section and `fastlane/metadata/android/en-US/changelogs/21902.txt` (471 chars).
Closes#18Closes#180Closes#187Closes#191Closes#192Closes#196Closes#214
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/216
Release branch for 2.19.1. Merging this cuts the release — versionName is bumped to 2.19.1 (versionCode 21901), which is what release.yaml picks up.
### What's in it
- **Drag to reschedule can be turned off** (#173). Settings → Views → *Drag to reschedule*, on by default. Turned off, no event block in the month, week or day view registers a drag gesture. The gate is `EventMoveScope.dragEnabled` rather than the scope itself, so the screen-reader "Move event" action — which only opens the edit form — stays available either way. The setting reads as off until the stored value has actually loaded, so a cold start can't hand out a drag before the preference is known.
### Release prep
- CHANGELOG 2.19.1 section written.
- Per-version "What's New" hand-written for all 12 store locales (`ar`, `de-DE`, `en-GB`, `en-US`, `es-ES`, `fr-FR`, `it-IT`, `pl-PL`, `pt-BR`, `pt-PT`, `ru-RU`, `zh-CN`).
- `lint test assembleDebug` and `check_translations.py` green locally.
- main is one commit ahead (Weblate #175, translations only) and merges clean — not merged into the branch, it comes along with this one.
Closes#173
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/176