Skip to content

feat(android): add a daily wallpaper schedule (3.7.0) - #88

Merged
codenameakshay merged 8 commits into
mainfrom
feat/daily-schedule
Nov 5, 2026
Merged

codenameakshay merged 8 commits into
mainfrom
feat/daily-schedule

Conversation

@codenameakshay

@codenameakshay codenameakshay commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Why

Prism's Day and night needs one wall by day and one by night on Android. 3.6.0 cannot switch at two times of day: timeOfDay fires once a day on the hour, and every trigger moves a playlist cursor instead of reading the clock. This adds a daily schedule next to rotation, built to the spec in Hash-Studios/Prism#811 ("Android: what the plugin needs"). Hash-Studios/Prism#819 uses it.

What changed

  • New startDailySchedule(DailyScheduleRequest), stopDailySchedule(), applyCurrentScheduleEntry() and getDailyScheduleStatus(). A request has 2 to 100 {minuteOfDay, source} entries, a target, and the same URL and file sources as rotation. Every source is cached at start; one failure rejects the request and leaves the running mode alone.
  • An entry owns the local time from its minute up to the next entry, wrapping past midnight. One inexact setAndAllowWhileIdle alarm is armed for the next switch (no SCHEDULE_EXACT_ALARM). Each delivery applies the entry that owns the current time, not a cursor, then arms the next one. DailyScheduleMath finds the next switch by walking whole minutes with the zone's real offsets, so DST gaps, repeated hours and zone changes follow one rule.
  • BOOT_COMPLETED, TIME_SET, TIMEZONE_CHANGED and MY_PACKAGE_REPLACED (new in the manifest) apply the current entry and re-arm. Receivers apply at once inside the alarm's idle allowance. If another wallpaper change holds the lock for more than 3 seconds, or the apply throws, a WorkManager job applies it later.
  • One schedule at a time: a daily schedule stops rotation, and a successful rotation start stops the daily schedule. Status reports the mode (none, rotation, dailySchedule), the current and last applied entry, the next switch time and the last error.
  • Version 3.7.0, README, CHANGELOG, and a Daily schedule section in the example app. feat(android): show different OpenGL live content on Home and Lock #89 (per-screen live wallpapers) also targets the unreleased 3.7.0, so this branch merges main and ships both in one 3.7.0 with one CHANGELOG entry.

Scope

  • Android only. iOS returns unsupported and status mode none.
  • Not published and not tagged. That waits for your OK.
  • Open: process death between the two mode commits of a handoff can leave both modes marked running (see Review).

Tradeoffs

  • Minute walk instead of java.time: minSdk is 24, and java.time would need core library desugaring in every app. The walk is at most 3 days of minutes, once per switch.
  • Inexact alarm: Doze can deliver it some minutes late. Apps should show "around 07:00".
  • Receivers apply directly instead of only queueing a job. A plain job can wait for a Doze maintenance window, so the 07:00 switch could land only when the phone wakes.

Review

Codex (gpt-6.1-sol, high) reviewed 3.6.0..HEAD before the proof. Boat refused sandbox creation (403), so it ran locally. It reproduced each defect with a test first, then fixed it (42 tests):

  • Receivers could run past their broadcast deadline while they waited for the lock.
  • A slow start could cross a switch boundary and miss a whole interval.
  • A failed commit left wrong in-memory preferences, and a failed replacement could delete the rollback cache or leave the wrong wall up.
  • A failed rotation handoff deleted the daily cache too early.
  • A missing cache looked healthy in status and kept an unusable alarm. It now shows lastError at once, apply returns failed, and nextSwitchEpochMs is 0.
  • Corrupt preferences or a stale index could cause bad scheduling.

Codex had moved receivers to queue-only WorkManager recovery. I put back the direct apply with a 3 second lock wait and kept the job as the fallback (cc6552c, 3 tests).

One finding needs your call. A process death between the rotation commit and the daily commit can restart both schedules. Options:

  1. Persist the last completed handoff and recover it on start.
  2. Stop both schedules when both are marked running. The app then restores its one mode on the next open. Prism does this already through AutoRotateMode. I would pick this one.

Verification

  • Kotlin: :async_wallpaper:testDebugUnitTest, 257 tests, 0 failures. DailyScheduleMathTest checks midnight, the New York spring gap and fall back, London DST days minute by minute, Kolkata and Kathmandu offsets, and a zone change for the same instant.
  • Dart: flutter test 82 passed (19 in test/daily_schedule_test.dart), flutter analyze no issues, example flutter test 11 passed.
  • Example app: flutter build apk --debug built app-debug.apk.
  • Device proof was not captured. The Simlock Android emulator rejects every app install and has no adb shell. Details and Android test renders are in feat(day-night): run Day and night on Android with the async_wallpaper daily schedule Hash-Studios/Prism#819, which runs this branch through a local path override.

Release order

  1. Merge this PR.
  2. Publish 3.7.0 to pub.dev and tag it (needs your OK).
  3. Run fvm flutter pub get in feat(day-night): run Day and night on Android with the async_wallpaper daily schedule Hash-Studios/Prism#819, which already asks for ^3.7.0, then merge it.

🤖 Generated with Claude Code

codenameakshay and others added 4 commits November 5, 2026 05:14
startDailySchedule caches every entry, applies the entry that owns the
current local time, stops rotation and arms one inexact alarm for the
next switch. Boot, clock, time zone and package-replace broadcasts apply
the current entry and arm the alarm again. Starting rotation stops the
schedule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kotlin tests cover the switch maths across midnight, DST days and time
zones, the request validator and the store. Dart tests cover the API.
The example app gains a Daily schedule section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Receivers could exceed their deadline, failed handoffs could destroy
prior schedules, and clock-boundary starts could miss an interval.

Preserve rollback sources and expose cache and persistence failures.
Keep public stop cleanup unconditional and handoff cleanup reversible.
Add 42 regression tests.

Cross-mode process-death recovery remains open pending a policy for
ambiguous persisted modes.
A queued job can wait for a Doze maintenance window, so the morning
switch could land only when the phone wakes. Receivers now apply at
once when the coordinator lock frees within 3 seconds, and queue the
durable job only when it does not or the apply throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codenameakshay and others added 4 commits November 5, 2026 12:15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ship the daily schedule in the unreleased 3.7.0 next to per-screen live
wallpapers. Pigeon bindings are regenerated from the merged input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A returned FAILED outcome counted as handled, so only a busy lock or an
exception reached the durable job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge picked up a lockfile that a newer local Flutter SDK rewrote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codenameakshay
codenameakshay merged commit eb728f1 into main Nov 5, 2026
6 checks passed
@codenameakshay codenameakshay mentioned this pull request Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant