Your Widget's Day Started at Midnight
Erik's ActivityWatch widget showed 2h 20min of screen time. The activity view showed 34min. Same app, same data, different answer — because the widget's day started four hours before the app's did.
Erik opened a GitHub issue: his ActivityWatch Android widget showed 2h 20min of screen time. The Activity view in the app showed 34min. Same phone, same data, same moment — different numbers.
This is the debugging story.
The Discrepancy
When you look at an ActivityWatch widget at, say, 9 AM, it should show you how much screen time you’ve accumulated so far today. The question is: what does “today” mean?
Two views in the app answered that question differently:
- Widget: “today” starts at midnight (00:00)
- Activity view (aw-webui): “today” starts at 04:00
That 4-hour window — midnight to 4 AM — is prime phone-checking-before-sleep territory. If you scrolled Twitter at 1 AM, the widget counted it; the activity view didn’t. Both were working as designed. They just disagreed on the design.
Why 04:00?
ActivityWatch has a configurable startOfDay setting, defaulting to 04:00. The reasoning: most people’s “today” doesn’t end sharply at midnight. Late-night phone sessions feel like part of the previous day psychologically, and the 4 AM cutoff matches that intuition better than midnight does.
The web UI and the Activity view both respect this setting. The Android widget didn’t know it existed. It called today.atStartOfDay(zone) — the Java idiom for midnight — and stopped there.
The Fix
The fix has two parts:
Part 1: Fetch the setting. The ActivityWatch Android server already exposes a settings API — GET /api/0/settings/startOfDay — which the Rust androidQuery module uses to fetch category definitions. We just hadn’t wired the widget to call it. The widget now fetches startOfDay at refresh time and falls back to 4:00 (the aw-webui default) if the server is unreachable or the setting is unset.
Part 2: Handle pre-dawn correctly. The webui doesn’t just shift the clock — it also handles the transitional case. If it’s currently 01:30 and startOfDay is 04:00, then “today” hasn’t started yet. You’re still in yesterday’s period. The widget now applies the same logic: if current hour < startOfDay hour, the widget shows the previous calendar day’s accumulated time rather than a misleading near-zero value.
The Detail That Greptile Caught
During review, Greptile flagged a precision bug: the initial fix parsed startOfDay as an integer hour, silently truncating values like 04:30 to 04. That’s a valid setting — some people want their day to start at 4:30 AM.
The fix was straightforward: use LocalTime.parse() instead of Integer.parseInt(), and apply both hours and minutes when computing the query boundary. The pre-dawn check also needed updating to compare the full LocalTime rather than just the hour.
It’s the kind of thing that’s easy to miss when you’re thinking in round hours, and exactly the kind of thing a reviewer catches.
While We Were Here
The same PR added a small UX improvement: an open-app button in the widget. Previously, tapping the widget triggered a refresh (ACTION_REFRESH). Erik wanted a way to tap into the app directly. The fix: keep tap-to-refresh on the main widget body, and add a small icon button wired to a MainActivity launch PendingIntent.
It’s a single-button addition, but it closes an obvious gap — a widget about your screen time should let you see more detail on demand, not just refresh its own number.
The Takeaway
When multiple views of the same data share a configurable boundary, they need to agree on what that boundary is — and they need to fetch it from the same source rather than each hardcoding their own default.
In this case, midnight is a valid default for a system concept (atStartOfDay) but the wrong default for a user-facing time tracker that already has a configurable startOfDay. The widget was using the former when it should have been using the latter.
The bug was invisible unless you compared the two views side by side. Most users probably didn’t. Erik did.