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.
AndroidConnectionStatusProvideruses0as the "cache has never been populated" sentinel:The injected provider is
AndroidCurrentDateProvider, which returnsSystemClock.uptimeMillis()— and uptime starts at 0 at boot. So for the first two minutes of every boot,uptimeMillis() - 0 < CACHE_TTL_MSis true and the cache reads as fresh while it is still empty.Impact
getConnectionStatus()andgetConnectionType()both skipupdateCache(null)whenisCacheValid()returns true, then fall through togetConnectionStatusFromCache(). WithcachedNetworkCapabilities == nullthat 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()resetslastCacheUpdateTime = 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
0is 1970 and safely far in the past,0on any boot-relative clock is a real and very recent instant.This generalises beyond this class: any
longfield using0for "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()toelapsedRealtimeNanos()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 queryConnectivityManager.Unit-testable by constructing the provider with a clock reporting a small tick and asserting that the first read populates the cache.