Changelog
π Developer Logs
- Added a new
AppLoggerobject β a lightweight, file-backed logger that persists structured log lines (DEBUG/INFO/WARN/ERROR) to app storage. - Added an in-app Developer Logs viewer (
DeveloperLogSheet) with:- Filter tabs:
All,ERROR,WARN,INFO,DEBUG - Copy-all and clear-all actions
- Color-coded rows per log level
- Filter tabs:
- Instrumented key components with logging so real device behavior is traceable without
adb logcat:ScreenOffReceiverβ logs receipt ofACTION_SCREEN_OFF, wallpaper cycling decisions, cache hits/misses, and success/failure of applying the wallpaper.DailyVerseWorkerβ logsdoWork()start, success/failure, and (later) rescheduling.
- Added crash / kill detection: on each app start,
AppLoggercompares the previous session's state and reports either:- A previous session that ended in an uncaught crash, or
- The process having been killed by the system while backgrounded (with no clean shutdown).
- Fix: the "process was killed" log entry used to report the timestamp of reopening the app, not the time the process was actually last alive. It now uses the last recorded heartbeat (
onStop()) as the entry's timestamp instead of "now". - Fix: the "process was killed" message was always shown as a
WARN, implying it broke scheduled wallpaper updates β which is no longer true for interval-based scheduling (see AlarmManager section below). It's now:INFO(reassuring, "this is normal") when only interval-based scheduling is active, since that survives process death.WARNonly when "change wallpaper on screen off" is enabled, since that feature genuinely depends on the app process staying alive.
- Added a distinct Material 3 Expressive green highlight for the specific log lines that represent an actual wallpaper change (
Success: Wallpaper applied,β Wallpaper set successfully), so the single most important event in the log visually stands out from routine lifecycle/scheduling noise.
β° Exact-time wallpaper scheduling (major rework)
Problem: scheduled wallpaper changes were drifting later and later over time (e.g. showing up at 20:43 instead of 20:00), because the app relied on WorkManager's PeriodicWorkRequest / delayed OneTimeWorkRequest, which only guarantees "no earlier than" β Android's Doze mode and battery/App-Standby throttling are free to push real execution back, and that slack never shrinks back down.
- Replaced
PeriodicWorkRequest-based scheduling with a self-rescheduling chain of exactAlarmManageralarms (AlarmManager.setExactAndAllowWhileIdle), the one Android API explicitly exempted from Doze deferral. - Added
WallpaperAlarmReceiverβ a small, manifest-registeredBroadcastReceiverthat receives the exact alarm and hands the actual rendering work off toWorkManager(DailyVerseWorker), combining exact timing with WorkManager's more robust execution model. - Added
BootReceiverβ exact alarms don't survive a device reboot (unlike WorkManager jobs), so this re-arms them onBOOT_COMPLETED. DailyVerseWorkernow re-arms its own next alarm at the end of every run (success or failure) viarescheduleNext(), recomputing the delay fresh each time instead of relying on a fixed periodic interval.- Added a graceful fallback: if the exact-alarm permission isn't granted (or gets revoked), the app falls back to a delayed
WorkManagerjob with the same delay and logs a warning, instead of silently doing nothing. - Manifest changes: added
SCHEDULE_EXACT_ALARMandRECEIVE_BOOT_COMPLETEDpermissions, and registered both new receivers. - Updated the in-app diagnostic check (previously inspecting
WorkManager's job state) to instead check whether an exact alarm is currently armed for each schedule ("DailyBibleWallpaper", "WallpaperCycling").
π Local-time alignment fixes
Problem: interval-based scheduling ("every N hours") was computed from UTC epoch hours instead of the device's local time. On a UTC+2 device, "every 3 hours" landed on 17:00 / 20:00 instead of the expected 15:00 / 18:00 / 21:00 / 00:00 (offset drift by the device's UTC offset).
- Rewrote
computeSlotInitialDelayMs()to align to local wall-clock hour boundaries (viaCalendar) instead of UTC epoch-hour arithmetic. Fixes the 2h/3h/6h interval offset issue. - Fix: the 12-hour interval option silently ignored the user-selected anchor hour and fell through to the same (buggy, UTC-based) slot logic used for 2h/3h/6h. It now correctly honors the chosen hour, same as the 24h option, via
computeDailyCycleInitialDelayMs(hour, 12). - Fix (critical):
computeDailyCycleInitialDelayMs()only added the interval once when computing the next occurrence of the anchor hour. With a 12h interval and anchor hour 7 (07:00/19:00), if the current time was already past 19:00 (e.g. 19:48), the single addition still landed in the past β producing a negative delay. A negative/past trigger time madeAlarmManagerfire immediately, and since the worker rescheduled itself using the same buggy function, this became a tight refire loop (wallpaper changing almost every second). Fixed by looping the addition until the target time is strictly in the future. - Added a defense-in-depth safety net in
scheduleExactWallpaperAlarm(): any trigger time that isn't at least 60 seconds in the future is now clamped and logged as anERROR, so a similar bug can never again cause a silent refire loop β it will visibly show up in Developer Logs instead. LocalBibleProvider.getVerseForInterval(): the sub-24h verse "time slot" index was also based on raw UTC epoch hours, which could disagree with the (now local-time-based) alarm schedule β e.g. changing the anchor hour from 22:00 to 23:00 could still land in the same UTC slot and show the same verse. Switched the slot calculation to use local wall-clock hour + a local day counter, consistent with the scheduling fix above.- Note: image cycling (
WallpaperManager.cycleToNext()) was not affected by any of the above β it's a simple round-robin counter advanced once per triggered run, with no time-of-day math involved. It only inherits correctness from the alarm-scheduling fix (i.e. it now advances at the correct local time), but never had a UTC/local slot bug of its own.
π¨ Material 3 color/theming fixes
Problem: with Material You (dynamic color) enabled, several nested backgrounds that were supposed to look different rendered as the same color β most noticeably the settings-area background and the area behind the Pixel 6 lock-screen preview. The same issue showed up in the plain light theme too, even with Material You off.
- Root cause: these surfaces used the classic
background/surface/surfaceVariantroles, which are often very close in tone under dynamic color, and in the static (non-dynamic) light schemebackgroundandsurfacedefault to the exact same tone unless explicitly overridden. - Migrated the nested UI containers to Material 3's newer surface container ladder, which is specifically designed to keep each nesting level a deliberately distinct tone:
- Screen background (area behind the Pixel 6 preview) β stays
background - Settings-area wrapper β
surfaceβsurfaceContainerLow - Individual settings cards (
SettingsCard, incl. verse settings) βsurfaceVariantβsurfaceContainerHigh - Pixel 6 preview
Cardβ previously no explicit color (defaulted tosurface) β now explicitlysurfaceContainerHighest
- Screen background (area behind the Pixel 6 preview) β stays
- Follow-up fix: after the above change, the static dark theme (Material You off) looked different from before β because the newly-used
surfaceContainer*roles weren't explicitly set in the customDarkColorScheme, so they silently fell back to Material 3's baseline neutral defaults instead of this app's custom dark palette (BackgroundDark/SurfaceDark/SurfaceVariantDark). Fixed by explicitly pinningsurfaceContainerLowest/Low/(default)/High/HighestinTheme.kt, built from the same custom dark palette so the static dark theme's original look is preserved while the light/dynamic-color contrast fix still applies.
π Manifest
- Added
SCHEDULE_EXACT_ALARMpermission (for exact wallpaper-change alarms). - Added
RECEIVE_BOOT_COMPLETEDpermission (to re-arm alarms after reboot). - Registered
WallpaperAlarmReceiver(not exported β only triggered by the app's own alarms). - Registered
BootReceiver(exported, filtered toBOOT_COMPLETED).