ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-06-25Advanced

The Day Rolled Over and Yesterday's Wallpaper Came Back — Designing Date Boundaries Instead of Trusting an Agent's Time Code

A record of redesigning a daily-rotating wallpaper feature that showed yesterday's pick after midnight for some users and switched twice in one night for travelers. I separate the device clock, time zone, and daylight saving time into three distinct failure sources, show how to define a stable day key, defend reward boundaries against a rewound clock, and test it all with an injectable clock — from a solo indie developer's point of view.

Android19Time ZonesDate HandlingArchitecture5Jetpack Compose4

Premium Article

I was fixing the feature that surfaces one "wallpaper of the day" when a user wrote in: "The date changed but I'm still seeing yesterday's wallpaper." A different user reported the opposite — "it switched twice overnight." The same feature was producing two contradictory symptoms at once.

I could not reproduce either on my Pixel, no matter how many times I tried. It only showed up when I advanced the device date by hand, or changed the time zone to somewhere overseas. When you maintain the same app for years as a solo developer, you get used to your own device always sitting at the same time in the same region, and you stop imagining a world where the clock drifts. What finally pushed me to look was an AdMob rewarded-ad log: a "once per day" bonus was granting many times over for one particular user. A clock problem was hitting both the display and the revenue at the same time.

The first version of this code came from asking the Antigravity agent to "write logic that picks today's wallpaper, rotating daily." It read naturally and sailed through review. But production clocks don't advance as politely as a development device does.

Where the "natural" code broke

The first implementation looked roughly like this — not the agent's exact output, but the same shape:

// First version (broke in production)
fun todaysWallpaperIndex(total: Int): Int {
    val now = System.currentTimeMillis()
    val daysSinceEpoch = now / (24 * 60 * 60 * 1000)  // ms -> days
    return (daysSinceEpoch % total).toInt()
}

It looks like a clean, region-independent calculation. But System.currentTimeMillis() is milliseconds since the UTC epoch. Dividing by 24*60*60*1000 pins the rollover to midnight UTC. For users in Japan that means the wallpaper changes at 9 a.m. every morning, which felt exactly like "the date changed but nothing happened."

The fix I reached for next caused the opposite failure:

// Second version (broke a different way)
fun todaysWallpaperIndex(total: Int): Int {
    val cal = java.util.Calendar.getInstance()  // device default time zone
    val year = cal.get(java.util.Calendar.YEAR)
    val day = cal.get(java.util.Calendar.DAY_OF_YEAR)
    return ((year * 366 + day) % total)
}

Now it read the device time zone, so in Japan it rolled over at midnight. But Calendar.getInstance() reads the device's current zone as-is. A user who flew across a time difference saw DAY_OF_YEAR jump forward or back, switching twice in one night or skipping a day. The year * 366 + day key is also sloppy and breaks on leap years and year boundaries.

Both failures come down to one thing: cramming three independent decisions into a single expression. "When does the day roll over," "whose clock do we measure with," and "what happens when the clock moves backward" are separate design questions. The agent writes a tidy line in front of it, but pulling those three apart is the job of the person who knows what the app actually is.

Three failure sources, kept apart

Time bugs almost always come from one of three layers. I write this split out on paper before going back to the code.

First, the device clock itself. Users can set the time by hand, and plenty of devices sit a few minutes off. Any boundary where "moving forward earns something" — like a daily reward — gets attacked here.

Second, the time zone. When "today" starts depends on the region. Using the user's region for the displayed "today" is natural, but that region changes when they travel.

Third, daylight saving time. On a transition day, a day becomes 23 or 25 hours long, and times appear that either don't exist or happen twice. A naive "run at midnight every day" quietly breaks twice a year.

These three need different defenses. The displayed "today" is a time-zone problem, reward fraud is a device-clock problem, scheduling drift is a DST problem. Assigning them this way removes the strain of asking one expression to carry everything.

Time sourceWhat it guaranteesWhere to use it
System.currentTimeMillis()UTC based, not monotonic (can move backward)Display date math, given an explicit zone
Device time zone settingThe user's felt "today," changes when they travelDisplay boundary, but never hard-pinned
SystemClock.elapsedRealtime()Monotonic since boot, cannot be hand-setElapsed-time checks; not for date decisions
Server timeDevice independent, needs a network callFinal ruling for rewards you must protect

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Separate the device clock, time zone, and daylight saving time as three independent failure sources and pin a rotating feature's notion of 'today' to a single day key rather than guessing
Close the path where a rewound device clock breaks daily rewards and AdMob reward boundaries, recording the last shown day monotonically to stop double grants, with concrete Kotlin
Build tests with an injected Clock that reproduce DST transitions and the midnight rollover, so you can decide where to lean on the agent and where to hold the invariant yourself
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

App Dev2026-06-25
Every Favorite Tap Was Redrawing Every Visible Wallpaper — Stopping Needless Recomposition in Compose, Measured
A record of fixing a wallpaper grid where toggling a single favorite recomposed every visible thumbnail. Covers turning on Composition Tracing to measure first, tracking the unstable parameter down, stabilizing the data model, handling lambdas and derived state, and comparing recomposition counts before and after from an indie developer's view.
App Dev2026-03-15
Antigravity × Android Production Development — Building Shippable Apps with Jetpack Compose + MVI
An advanced guide to building production-quality Android apps with Antigravity. Covers MVI architecture, Jetpack Compose performance optimization, error handling strategy, testing patterns, and CI/CD — all through AI-assisted development.
App Dev2026-03-10
Android Studio × Antigravity Android Dev Guide — Accelerate Kotlin/Jetpack Compose with AI
Master Android app development by combining Android Studio with Antigravity. From Jetpack Compose UI generation to testing and Google Play deployment.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →