androidinterview.com

Android System Design Interview Questions

How do you get accurate time in Android?

Tier: Less commonDifficulty: Medium

Design time handling for an Android feature that cannot blindly trust the date and time shown on the phone. First distinguish measuring how long something lasts from knowing the current time agreed with a server.

The problem

A retry delay needs elapsed time. A deadline shared across devices needs an external reference and a policy for uncertainty when offline. The user changing device settings must not accidentally extend a running timer or make a stale timestamp appear current.

For example, an app starts a thirty second retry delay, then the user moves the displayed clock back an hour. The delay should still end after thirty seconds of elapsed time. A separate offer ending at a server defined deadline needs a trusted time reference, and the server must remain authoritative about accepting it.

Explain the clock choice for each job, what survives a restart and what the app does when it cannot refresh its time reference. The required precision should be agreed before choosing a synchronization approach.

Starting the design

You do not get trustworthy wall clock time from the device, because the user can set it to anything. So you ask a source you trust once, and from then on you count forward with a clock the user cannot touch. For anything that is only a duration, you never need a date at all, you need SystemClock.elapsedRealtime().

The clocks Android gives you do different jobs, and picking the wrong one is the actual trap in this question.

  • System.currentTimeMillis(), wall clock time. This is the value the user changes in Settings. The device normally sets it from the carrier over NITZ or from a network time server, and it can simply be wrong.
  • SystemClock.elapsedRealtime(), milliseconds since boot, including deep sleep. This is the one for a session length, a timeout or a retry backoff, because nothing the user does moves it.
  • SystemClock.uptimeMillis(), milliseconds since boot, excluding deep sleep. Right for animation and UI timing, wrong for anything that has to keep counting while the device sleeps.
  • System.nanoTime(), the JVM monotonic clock. It is what people reach for out of habit from server code. On Android it is not guaranteed to advance during deep sleep, so elapsedRealtime() is the answer here instead.
  • SystemClock.currentNetworkTimeClock(), added in API 31. It hands you a java.time.Clock built on the network time the platform already syncs. It throws DateTimeException when no sync has happened yet, so you still need a fallback behind it.

The offset technique

For a trusted date, take the time from your backend once and remember how far it sits from the monotonic clock. Everything after that is arithmetic, with the device clock out of the picture.

// At sync time, from the server's response.
val offset = serverNowMillis - SystemClock.elapsedRealtime()

// Every read after that. Nothing here can be changed in Settings.
fun trustedNow(): Long = offset + SystemClock.elapsedRealtime()

The one thing to remember is that elapsedRealtime() restarts at zero on boot, so a persisted offset is meaningless after a reboot. Resync on boot, and resync on a schedule to keep drift small.

Where the trusted time comes from

Your own server's response is the cheap version and it is usually enough, because you need it to be honest rather than accurate to the millisecond. Reading a Date header off a request you were making anyway costs nothing extra. Raw SNTP from an app is more work than people expect, since there is no platform API for it, which is why most apps do not bother. On API 31 and up, currentNetworkTimeClock() gives you the platform's own synced time without a round trip.

How I'd build this on Android

I'd inject a time source into repositories and expiry checks. The ViewModel uses it to calculate the remaining duration, and the UI decides how often to redraw the countdown. A repository fetches server time. The composable should not make a new time request every second.

interface TrustedClock { fun nowMillis(): Long? }

class SessionTrustedClock(
    private val serverAtReceipt: Long,
    private val receivedElapsed: Long,
    private val elapsed: () -> Long = SystemClock::elapsedRealtime,
) : TrustedClock {
    override fun nowMillis(): Long? {
        val delta = elapsed() - receivedElapsed
        return if (delta >= 0) serverAtReceipt + delta else null
    }
}

This estimate stays in memory for the current process and boot. I would not restore it after reboot just because delta happens to be positive. A full implementation also records how old the measurement is, how uncertain it is and when to refresh it.

A server timestamp is already slightly old when the response arrives. A cached HTTP Date header may be older still. Measuring request and response time helps estimate the delay, but using the midpoint assumes something about the journey in each direction. It does not give perfectly accurate time.

The backend checks payment, token and offer expiry even if the app shows a countdown. If the app cannot estimate the time, it shows that a fresh check is needed. currentNetworkTimeClock() does not give the device authority to accept an expired payment.

I'd check these cases.

  • The user changing the clock.
  • Deep sleep.
  • Reboot.
  • A delayed response.
  • Opening the app offline.

See SystemClock.

Tradeoffs I'd call out

  • A round trip against freshness. Fetching server time costs a request, so you cache the offset and resync on a schedule rather than per read.
  • No network, no trusted time. A cold start offline has nothing to anchor to. Decide what the feature does then, block, or run on device time and mark the result as untrusted.
  • Client checks are a courtesy. Expiry of a token or a promotion has to be validated on the server. A client side check on System.currentTimeMillis() is defeated by changing the clock, so the client check is there to give a good message, not to enforce anything.

Read more SystemClock (opens in a new tab)