The bug report was clean: started a stopwatch, put the phone down, came back after roughly two hours, and the stopwatch read about thirty minutes.

We had used the wrong clock. Android has three, they differ in exactly the situations a stopwatch cares about, and picking the wrong one produces something that works perfectly on a desk with the screen on and lies to you the moment the device goes to sleep.

Three clocks

System.currentTimeMillis() is wall-clock time — milliseconds since the epoch, the thing you show a user. It can jump. A network time sync corrects it. The user can set it by hand. A timezone change does not move it but changes how you render it. It is the only correct choice for displaying the time and a bad choice for measuring a duration, because a correction mid-measurement adds or removes time that did not pass.

SystemClock.uptimeMillis() counts milliseconds since boot, *excluding* time the system spent in deep sleep. It is monotonic and it is what most animation and timeout code should use. It is also, catastrophically, not a measure of elapsed real time — if the device sleeps for ninety minutes, uptimeMillis advances by roughly zero.

SystemClock.elapsedRealtime() counts milliseconds since boot *including* deep sleep. Monotonic, unaffected by time corrections, unaffected by the user setting the clock, and it keeps counting while the phone is asleep in a pocket.

We had used uptimeMillis. On our desks, with the screen on, uptimeMillis and elapsedRealtime advance identically, so every test passed. On a phone that went to sleep, they diverged by exactly the amount of sleep, and the missing ninety minutes were the ninety minutes the device had spent in deep sleep.

The rule that falls out of this: elapsedRealtime for anything that measures a real-world duration, currentTimeMillis only for displaying a date or time, and uptimeMillis for scheduling within a running UI. There is no fourth case.

Reboots break monotonic clocks too

elapsedRealtime is measured from boot, which means it resets to zero when the device restarts.

If the stopwatch stores a start value of elapsedRealtime and the phone reboots, the stored value is now larger than the current one, and the naive subtraction gives you a negative duration. A running stopwatch left over a reboot produced numbers that were not merely wrong, they were impossible.

The app now stores both elapsedRealtime and currentTimeMillis at start, and it can detect that a reboot occurred — the difference between the two has changed in a way it cannot otherwise. When it detects one, it does not attempt to reconstruct the elapsed time from the wall clock, because the wall clock may also have been corrected during the boot. It stops the stopwatch and says the measurement was interrupted by a restart.

That is a worse outcome than silently continuing and a better one than reporting a duration we cannot vouch for. The whole value of a stopwatch is that the number means something.

The timer has a different problem

A timer does not need to count. It needs to fire.

A running timer with the app in the foreground can render from elapsedRealtime and be exactly right. The hard part is the timer that is still running when you leave the app, lock the phone, and put it in your pocket — because at that point nothing of yours is executing, and the device will enter Doze if it sits still long enough.

In Doze, ordinary alarms are batched into maintenance windows. A timer set for twelve minutes can fire meaningfully late, which for a timer is the same as not firing.

The mechanism for "this really does need to happen at this moment" is an exact alarm, and Android has been progressively restricting those. Since Android 12 an app needs to hold the exact alarm permission, and on newer versions the permission that is granted by default is reserved for apps whose core purpose is alarms and clocks. An app that does not have it and schedules an exact alarm anyway gets an exception, not a late alarm.

So FlipClock checks whether it can schedule exact alarms, and if it cannot, it says so before you start a timer instead of after it fails to go off. There is a settings link. It is one more thing to explain and it is much better than a timer that quietly rounds itself off to the nearest maintenance window.

The countdown you see while the app is open is rendered from elapsedRealtime, and the alarm is what actually fires. Those are two independent mechanisms measuring the same instant, which is deliberate: the display being correct does not depend on the alarm, and the alarm does not depend on the app being alive.

Surviving process death

The other thing that kills a timer is your process being killed while it is in the background, which Android is entitled to do at any moment.

Nothing about the timer lives in memory. The end time is persisted at start, expressed in elapsedRealtime. The alarm is registered with the system. If the process is killed and later restarted, it reads the persisted end time and renders the correct remaining duration immediately — no lost state, no restart from zero.

This is the same principle as the stopwatch fix, generalised: store the anchor point, compute the display from it, and never treat accumulated in-memory state as the source of truth. A timer that counts down in a variable is a timer that resets when the system needs your memory.

When to use which mode

The engineering is in service of a small distinction, and it is worth stating plainly.

Clock is for ambient awareness. It answers "what time is it" without being asked, and it is what you want in your peripheral vision while working, reading, cooking, or waiting.

Timer is for a boundary you have decided in advance. A study block, a break between calls, laundry, the ten minutes before you have to leave. The value is that you can begin without negotiating with yourself about when to stop, because the decision is already made and something else is holding it.

Stopwatch is for a question rather than a boundary: how long does this actually take. Not "how long should it take" — that is a plan, and plans are usually wrong about this. Time the writing session, the walk to the station, the weekly chore you keep underestimating. The output is a number for the next plan, not a score.

The distinction between the timer and the stopwatch is small and it is the useful part. One imposes a limit. The other measures reality. People reach for the timer when they should be using the stopwatch, decide the task overran, and set the same wrong timer again next week.

What this update deliberately does not do

There is no session history, no tagging, no statistics, no streaks. FlipClock is a desk object that happens to run on a phone, and the moment it starts recording what you did with your time it becomes something you have to maintain.

If you want the history, write the number down. That takes four seconds and it means the app stays something you can put on a shelf and stop thinking about.