Skip to content

AndroidConnectionStatusProvider treats a never-populated cache as fresh after boot #6027

Description

@runningcode

AndroidConnectionStatusProvider uses 0 as the "cache has never been populated" sentinel:

private volatile long lastCacheUpdateTime = 0;
private static final long CACHE_TTL_MS = 2 * 60 * 1000L; // 2 minutes

private boolean isCacheValid() {
  return (timeProvider.getCurrentTimeMillis() - lastCacheUpdateTime) < CACHE_TTL_MS;
}

The injected provider is AndroidCurrentDateProvider, which returns SystemClock.uptimeMillis() — and uptime starts at 0 at boot. So for the first two minutes of every boot, uptimeMillis() - 0 < CACHE_TTL_MS is true and the cache reads as fresh while it is still empty.

Impact

getConnectionStatus() and getConnectionType() both skip updateCache(null) when isCacheValid() returns true, then fall through to getConnectionStatusFromCache(). With cachedNetworkCapabilities == null that takes the legacy fallback path rather than the NetworkCapabilities path the device is actually configured for, so the first connectivity reads of a boot session can report the wrong status.

The window lands squarely on cold starts shortly after boot.

unregisterNetworkCallback() resets lastCacheUpdateTime = 0, so the same window reopens after every unregister, not only at process start.

Note on the audit

#5530 section D lists "AndroidConnectionStatusProvider cache TTL (uptime)" under Checked and confirmed correct. The wall-vs-monotonic pairing is indeed correct — both sides are uptime. What the audit missed is that the sentinel value is not valid on a monotonic clock: unlike epoch millis, where 0 is 1970 and safely far in the past, 0 on any boot-relative clock is a real and very recent instant.

This generalises beyond this class: any long field using 0 for "unset" is wrong once it is compared against a monotonic clock. Worth grepping for as part of the project.

Fix

Represent "never populated" outside the numeric range — a nullable sentinel, or a deadline value that is expired by construction. Note that migrating the clock from uptimeMillis() to elapsedRealtimeNanos() does not fix it; elapsedRealtimeNanos() also starts at 0 at boot.

Repro

Boot a device, launch an app with the SDK within two minutes, and observe that the first getConnectionStatus() does not query ConnectivityManager.

Unit-testable by constructing the provider with a clock reporting a small tick and asserting that the first read populates the cache.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions