1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.util;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.net.ConnectivityManager;
26 import android.net.Network;
27 import android.net.NetworkInfo;
28 import android.net.SntpClient;
29 import android.os.Build;
30 import android.os.SystemClock;
31 import android.provider.Settings;
32 import android.text.TextUtils;
33 
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.annotations.VisibleForTesting;
36 
37 import java.io.PrintWriter;
38 import java.net.InetSocketAddress;
39 import java.net.URI;
40 import java.net.URISyntaxException;
41 import java.time.Duration;
42 import java.time.Instant;
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.List;
46 import java.util.Objects;
47 
48 /**
49  * A singleton that connects with a remote NTP server as its trusted time source. This class
50  * is thread-safe. The {@link #forceRefresh()} method is synchronous, i.e. it may occupy the
51  * current thread while performing an NTP request. All other threads calling {@link #forceRefresh()}
52  * will block during that request.
53  *
54  * @hide
55  */
56 public abstract class NtpTrustedTime implements TrustedTime {
57 
58     private static final String URI_SCHEME_NTP = "ntp";
59     @VisibleForTesting
60     public static final String NTP_SETTING_SERVER_NAME_DELIMITER = "|";
61     private static final String NTP_SETTING_SERVER_NAME_DELIMITER_REGEXP = "\\|";
62 
63     /**
64      * NTP server configuration.
65      *
66      * @hide
67      */
68     public static final class NtpConfig {
69 
70         @NonNull private final List<URI> mServerUris;
71         @NonNull private final Duration mTimeout;
72 
73         /**
74          * Creates an instance with the supplied properties. There must be at least one NTP server
75          * URI and the timeout must be non-zero / non-negative.
76          *
77          * <p>If the arguments are invalid then an {@link IllegalArgumentException} will be thrown.
78          * See {@link #parseNtpUriStrict(String)} and {@link #parseNtpServerSetting(String)} to
79          * create valid URIs.
80          */
NtpConfig(@onNull List<URI> serverUris, @NonNull Duration timeout)81         public NtpConfig(@NonNull List<URI> serverUris, @NonNull Duration timeout)
82                 throws IllegalArgumentException {
83 
84             Objects.requireNonNull(serverUris);
85             if (serverUris.isEmpty()) {
86                 throw new IllegalArgumentException("Server URIs is empty");
87             }
88 
89             List<URI> validatedServerUris = new ArrayList<>();
90             for (URI serverUri : serverUris) {
91                 try {
92                     URI validatedServerUri = validateNtpServerUri(
93                             Objects.requireNonNull(serverUri));
94                     validatedServerUris.add(validatedServerUri);
95                 } catch (URISyntaxException e) {
96                     throw new IllegalArgumentException("Bad server URI", e);
97                 }
98             }
99             mServerUris = Collections.unmodifiableList(validatedServerUris);
100 
101             if (timeout.isNegative() || timeout.isZero()) {
102                 throw new IllegalArgumentException("timeout < 0");
103             }
104             mTimeout = timeout;
105         }
106 
107         /** Returns a non-empty, immutable list of NTP server URIs. */
108         @NonNull
getServerUris()109         public List<URI> getServerUris() {
110             return mServerUris;
111         }
112 
113         @NonNull
getTimeout()114         public Duration getTimeout() {
115             return mTimeout;
116         }
117 
118         @Override
toString()119         public String toString() {
120             return "NtpConnectionInfo{"
121                     + "mServerUris=" + mServerUris
122                     + ", mTimeout=" + mTimeout
123                     + '}';
124         }
125     }
126 
127     /**
128      * The result of a successful NTP query.
129      *
130      * @hide
131      */
132     // Non-final for mocking frameworks
133     public static class TimeResult {
134         private final long mUnixEpochTimeMillis;
135         private final long mElapsedRealtimeMillis;
136         private final int mUncertaintyMillis;
137         @NonNull private final InetSocketAddress mNtpServerSocketAddress;
138 
TimeResult( long unixEpochTimeMillis, long elapsedRealtimeMillis, int uncertaintyMillis, @NonNull InetSocketAddress ntpServerSocketAddress)139         public TimeResult(
140                 long unixEpochTimeMillis, long elapsedRealtimeMillis, int uncertaintyMillis,
141                 @NonNull InetSocketAddress ntpServerSocketAddress) {
142             mUnixEpochTimeMillis = unixEpochTimeMillis;
143             mElapsedRealtimeMillis = elapsedRealtimeMillis;
144             mUncertaintyMillis = uncertaintyMillis;
145             mNtpServerSocketAddress = Objects.requireNonNull(ntpServerSocketAddress);
146         }
147 
getTimeMillis()148         public long getTimeMillis() {
149             return mUnixEpochTimeMillis;
150         }
151 
getElapsedRealtimeMillis()152         public long getElapsedRealtimeMillis() {
153             return mElapsedRealtimeMillis;
154         }
155 
getUncertaintyMillis()156         public int getUncertaintyMillis() {
157             return mUncertaintyMillis;
158         }
159 
160         /**
161          * Calculates and returns the current Unix epoch time accounting for the age of this result.
162          */
currentTimeMillis()163         public long currentTimeMillis() {
164             return mUnixEpochTimeMillis + getAgeMillis();
165         }
166 
167         /** Calculates and returns the age of this result. */
getAgeMillis()168         public long getAgeMillis() {
169             return getAgeMillis(SystemClock.elapsedRealtime());
170         }
171 
172         /**
173          * Calculates and returns the age of this result relative to currentElapsedRealtimeMillis.
174          *
175          * @param currentElapsedRealtimeMillis - reference elapsed real time
176          */
getAgeMillis(long currentElapsedRealtimeMillis)177         public long getAgeMillis(long currentElapsedRealtimeMillis) {
178             return currentElapsedRealtimeMillis - mElapsedRealtimeMillis;
179         }
180 
181         @Override
equals(Object o)182         public boolean equals(Object o) {
183             if (this == o) {
184                 return true;
185             }
186             if (!(o instanceof TimeResult)) {
187                 return false;
188             }
189             TimeResult that = (TimeResult) o;
190             return mUnixEpochTimeMillis == that.mUnixEpochTimeMillis
191                     && mElapsedRealtimeMillis == that.mElapsedRealtimeMillis
192                     && mUncertaintyMillis == that.mUncertaintyMillis
193                     && mNtpServerSocketAddress.equals(
194                     that.mNtpServerSocketAddress);
195         }
196 
197         @Override
hashCode()198         public int hashCode() {
199             return Objects.hash(mUnixEpochTimeMillis, mElapsedRealtimeMillis, mUncertaintyMillis,
200                     mNtpServerSocketAddress);
201         }
202 
203         @Override
toString()204         public String toString() {
205             return "TimeResult{"
206                     + "unixEpochTime=" + Instant.ofEpochMilli(mUnixEpochTimeMillis)
207                     + ", elapsedRealtime=" + Duration.ofMillis(mElapsedRealtimeMillis)
208                     + ", mUncertaintyMillis=" + mUncertaintyMillis
209                     + ", mNtpServerSocketAddress=" + mNtpServerSocketAddress
210                     + '}';
211         }
212     }
213 
214     private static final String TAG = "NtpTrustedTime";
215     private static final boolean LOGD = false;
216 
217     private static NtpTrustedTime sSingleton;
218 
219     /** A lock to prevent multiple refreshes taking place at the same time. */
220     private final Object mRefreshLock = new Object();
221 
222     /** A lock to ensure safe read/writes to configuration. */
223     private final Object mConfigLock = new Object();
224 
225     /** An in-memory config override for use during tests. */
226     @GuardedBy("mConfigLock")
227     @Nullable
228     private NtpConfig mNtpConfigForTests;
229 
230     /**
231      * The latest time result.
232      *
233      * <p>Written when holding {@link #mRefreshLock} but declared volatile and can be read outside
234      * synchronized blocks to avoid blocking dump() during {@link #forceRefresh}.
235      */
236     @Nullable
237     private volatile TimeResult mTimeResult;
238 
239     /**
240      * The last successful NTP server URI, i.e. the one used to obtain {@link #mTimeResult} when it
241      * is non-null.
242      *
243      * <p>Written when holding {@link #mRefreshLock} but declared volatile and can be read outside
244      * synchronized blocks to avoid blocking dump() during {@link #forceRefresh}.
245      */
246     @Nullable
247     private volatile URI mLastSuccessfulNtpServerUri;
248 
NtpTrustedTime()249     protected NtpTrustedTime() {
250     }
251 
252     @UnsupportedAppUsage
getInstance(Context context)253     public static synchronized NtpTrustedTime getInstance(Context context) {
254         if (sSingleton == null) {
255             Context appContext = context.getApplicationContext();
256             sSingleton = new NtpTrustedTimeImpl(appContext);
257         }
258         return sSingleton;
259     }
260 
261     /**
262      * Overrides the NTP server config for tests. Passing {@code null} to a parameter clears the
263      * test value, i.e. so the normal value will be used next time.
264      */
setServerConfigForTests(@onNull NtpConfig ntpConfig)265     public void setServerConfigForTests(@NonNull NtpConfig ntpConfig) {
266         synchronized (mConfigLock) {
267             mNtpConfigForTests = ntpConfig;
268         }
269     }
270 
271     /** Forces a refresh using the default network. */
272     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
forceRefresh()273     public boolean forceRefresh() {
274         synchronized (mRefreshLock) {
275             Network network = getDefaultNetwork();
276             if (network == null) {
277                 if (LOGD) Log.d(TAG, "forceRefresh: no network available");
278                 return false;
279             }
280 
281             return forceRefreshLocked(network);
282         }
283     }
284 
285     /** Forces a refresh using the specified network. */
forceRefresh(@onNull Network network)286     public boolean forceRefresh(@NonNull Network network) {
287         Objects.requireNonNull(network);
288 
289         synchronized (mRefreshLock) {
290             // Prevent concurrent refreshes.
291             return forceRefreshLocked(network);
292         }
293     }
294 
295     @GuardedBy("mRefreshLock")
forceRefreshLocked(@onNull Network network)296     private boolean forceRefreshLocked(@NonNull Network network) {
297         Objects.requireNonNull(network);
298 
299         if (!isNetworkConnected(network)) {
300             if (LOGD) Log.d(TAG, "forceRefreshLocked: network=" + network + " is not connected");
301             return false;
302         }
303 
304         NtpConfig ntpConfig = getNtpConfig();
305         if (ntpConfig == null) {
306             // missing server config, so no NTP time available
307             if (LOGD) Log.d(TAG, "forceRefreshLocked: invalid server config");
308             return false;
309         }
310 
311         if (LOGD) {
312             Log.d(TAG, "forceRefreshLocked: NTP request network=" + network
313                     + " ntpConfig=" + ntpConfig);
314         }
315 
316         List<URI> unorderedServerUris = ntpConfig.getServerUris();
317 
318         // Android supports multiple NTP server URIs for situations where servers might be
319         // unreachable for some devices due to network topology, e.g. we understand that devices
320         // travelling to China often have difficulty accessing "time.android.com". Android
321         // partners may want to configure alternative URIs for devices sold globally, or those
322         // that are likely to travel to part of the world without access to the full internet.
323         //
324         // The server URI list is expected to contain one element in the general case, with two
325         // or three as the anticipated maximum. The list is never empty. Server URIs are
326         // considered to be in a rough priority order of servers to try initially (no
327         // randomization), but besides that there is assumed to be no preference.
328         //
329         // The server selection algorithm below tries to stick with a successfully accessed NTP
330         // server's URI where possible:
331         //
332         // The algorithm based on the assumption that a cluster of NTP servers sharing the same
333         // host name, particularly commercially run ones, are likely to agree more closely on
334         // the time than servers from different URIs, so it's best to be sticky. Switching
335         // between URIs could result in flip-flopping between reference clocks or involve
336         // talking to server clusters with different approaches to leap second handling.
337         //
338         // Stickiness may also be useful if some server URIs early in the list are permanently
339         // black-holing requests, or if the responses are not routed back. In those cases it's
340         // best not to try those URIs more than we have to, as might happen if the algorithm
341         // always started at the beginning of the list.
342         //
343         // Generally, we have to assume that any of the configured servers are going to be "good
344         // enough" as an external reference clock when reachable, so the stickiness is a very
345         // lightly applied bias. There's no tracking of failure rates or back-off on a per-URI
346         // basis; higher level code is expected to handle rate limiting of NTP requests in the
347         // event of failure to contact any server.
348 
349         List<URI> orderedServerUris = new ArrayList<>();
350         for (URI serverUri : unorderedServerUris) {
351             if (serverUri.equals(mLastSuccessfulNtpServerUri)) {
352                 orderedServerUris.add(0, serverUri);
353             } else {
354                 orderedServerUris.add(serverUri);
355             }
356         }
357 
358         for (URI serverUri : orderedServerUris) {
359             TimeResult timeResult = queryNtpServer(network, serverUri, ntpConfig.getTimeout());
360             // Only overwrite previous state if the request was successful.
361             if (timeResult != null) {
362                 mLastSuccessfulNtpServerUri = serverUri;
363                 mTimeResult = timeResult;
364                 return true;
365             }
366         }
367         return false;
368     }
369 
getNtpConfig()370     private NtpConfig getNtpConfig() {
371         synchronized (mConfigLock) {
372             if (mNtpConfigForTests != null) {
373                 return mNtpConfigForTests;
374             }
375             return getNtpConfigInternal();
376         }
377     }
378 
379     /**
380      * Returns the {@link NtpConfig} to use during an NTP query. This method can return {@code null}
381      * if there is no config, or the config found is invalid.
382      *
383      * <p>This method has been made public for easy replacement during tests.
384      */
385     @GuardedBy("mConfigLock")
386     @VisibleForTesting
387     @Nullable
getNtpConfigInternal()388     public abstract NtpConfig getNtpConfigInternal();
389 
390     /**
391      * Returns the default {@link Network} to use during an NTP query when no network is specified.
392      * This method can return {@code null} if the device hasn't fully initialized or there is no
393      * active network.
394      *
395      * <p>This method has been made public for easy replacement during tests.
396      */
397     @VisibleForTesting
398     @Nullable
getDefaultNetwork()399     public abstract Network getDefaultNetwork();
400 
401     /**
402      * Returns {@code true} if there is likely to be connectivity on the supplied network.
403      *
404      * <p>This method has been made public for easy replacement during tests.
405      */
406     @VisibleForTesting
isNetworkConnected(@onNull Network network)407     public abstract boolean isNetworkConnected(@NonNull Network network);
408 
409     /**
410      * Queries the specified NTP server. This is a blocking call. Returns {@code null} if the query
411      * fails.
412      *
413      * <p>This method has been made public for easy replacement during tests.
414      */
415     @VisibleForTesting
416     @Nullable
queryNtpServer( @onNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout)417     public abstract TimeResult queryNtpServer(
418             @NonNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout);
419 
420     /**
421      * Only kept for UnsupportedAppUsage.
422      *
423      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
424      */
425     @Deprecated
426     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
hasCache()427     public boolean hasCache() {
428         return mTimeResult != null;
429     }
430 
431     /**
432      * Only kept for UnsupportedAppUsage.
433      *
434      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
435      */
436     @Deprecated
437     @Override
getCacheAge()438     public long getCacheAge() {
439         TimeResult timeResult = mTimeResult;
440         if (timeResult != null) {
441             return SystemClock.elapsedRealtime() - timeResult.getElapsedRealtimeMillis();
442         } else {
443             return Long.MAX_VALUE;
444         }
445     }
446 
447     /**
448      * Only kept for UnsupportedAppUsage.
449      *
450      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
451      */
452     @Deprecated
453     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
currentTimeMillis()454     public long currentTimeMillis() {
455         TimeResult timeResult = mTimeResult;
456         if (timeResult == null) {
457             throw new IllegalStateException("Missing authoritative time source");
458         }
459         if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit");
460 
461         // current time is age after the last ntp cache; callers who
462         // want fresh values will hit forceRefresh() first.
463         return timeResult.currentTimeMillis();
464     }
465 
466     /**
467      * Only kept for UnsupportedAppUsage.
468      *
469      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
470      */
471     @Deprecated
472     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getCachedNtpTime()473     public long getCachedNtpTime() {
474         if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit");
475         TimeResult timeResult = mTimeResult;
476         return timeResult == null ? 0 : timeResult.getTimeMillis();
477     }
478 
479     /**
480      * Only kept for UnsupportedAppUsage.
481      *
482      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
483      */
484     @Deprecated
485     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getCachedNtpTimeReference()486     public long getCachedNtpTimeReference() {
487         TimeResult timeResult = mTimeResult;
488         return timeResult == null ? 0 : timeResult.getElapsedRealtimeMillis();
489     }
490 
491     /**
492      * Returns an object containing the latest NTP information available. Can return {@code null} if
493      * no information is available.
494      */
495     @Nullable
getCachedTimeResult()496     public TimeResult getCachedTimeResult() {
497         return mTimeResult;
498     }
499 
500     /** Sets the last received NTP time. Intended for use during tests. */
setCachedTimeResult(TimeResult timeResult)501     public void setCachedTimeResult(TimeResult timeResult) {
502         synchronized (mRefreshLock) {
503             mTimeResult = timeResult;
504         }
505     }
506 
507     /** Clears the last received NTP time. Intended for use during tests. */
clearCachedTimeResult()508     public void clearCachedTimeResult() {
509         synchronized (mRefreshLock) {
510             mTimeResult = null;
511         }
512     }
513 
514     /**
515      * Parses and returns an NTP server config URI, or throws an exception if the URI doesn't
516      * conform to expectations.
517      *
518      * <p>NTP server config URIs are in the form "ntp://{hostname}[:port]". This is not a registered
519      * IANA URI scheme.
520      */
521     @NonNull
parseNtpUriStrict(@onNull String ntpServerUriString)522     public static URI parseNtpUriStrict(@NonNull String ntpServerUriString)
523             throws URISyntaxException {
524         // java.net.URI is used in preference to android.net.Uri, since android.net.Uri is very
525         // forgiving of obvious errors. URI catches issues sooner.
526         URI unvalidatedUri = new URI(ntpServerUriString);
527         return validateNtpServerUri(unvalidatedUri);
528     }
529 
530     /**
531      * Parses a setting string and returns a list of URIs that will be accepted by {@link
532      * NtpConfig}, or {@code null} if the string is invalid.
533      *
534      * <p>The setting string is expected to be one or more server values separated by a pipe ("|")
535      * character.
536      *
537      * <p>NTP server config URIs are in the form "ntp://{hostname}[:port]". This is not a registered
538      * IANA URI scheme.
539      *
540      * <p>Unlike {@link #parseNtpUriStrict(String)} this method will not throw an exception. It
541      * checks each value for a leading "ntp:" and will call through to {@link
542      * #parseNtpUriStrict(String)} to attempt to parse it, returning {@code null} if it fails.
543      * To support legacy settings values, it will also accept string values that only consists of a
544      * server name, which will be coerced into a URI in the form "ntp://{server name}".
545      */
546     @VisibleForTesting
547     @Nullable
parseNtpServerSetting(@ullable String ntpServerSetting)548     public static List<URI> parseNtpServerSetting(@Nullable String ntpServerSetting) {
549         if (TextUtils.isEmpty(ntpServerSetting)) {
550             return null;
551         } else {
552             String[] values = ntpServerSetting.split(NTP_SETTING_SERVER_NAME_DELIMITER_REGEXP);
553             if (values.length == 0) {
554                 return null;
555             }
556 
557             List<URI> uris = new ArrayList<>();
558             for (String value : values) {
559                 if (value.startsWith(URI_SCHEME_NTP + ":")) {
560                     try {
561                         uris.add(parseNtpUriStrict(value));
562                     } catch (URISyntaxException e) {
563                         Log.w(TAG, "Rejected NTP uri setting=" + ntpServerSetting, e);
564                         return null;
565                     }
566                 } else {
567                     // This is the legacy settings path. Assumes that the string is just a host name
568                     // and creates a URI in the form ntp://<host name>
569                     try {
570                         URI uri = new URI(URI_SCHEME_NTP, /*host=*/value,
571                                 /*path=*/null, /*fragment=*/null);
572                         // Paranoia: validate just in case the host name somehow results in a bad
573                         // URI.
574                         URI validatedUri = validateNtpServerUri(uri);
575                         uris.add(validatedUri);
576                     } catch (URISyntaxException e) {
577                         Log.w(TAG, "Rejected NTP legacy setting=" + ntpServerSetting, e);
578                         return null;
579                     }
580                 }
581             }
582             return uris;
583         }
584     }
585 
586     /**
587      * Checks that the supplied URI can be used to identify an NTP server.
588      * This method currently ignores Uri components that are not used, only checking the parts that
589      * must be present. Returns the supplied {@code uri} if validation is successful.
590      */
591     @NonNull
validateNtpServerUri(@onNull URI uri)592     private static URI validateNtpServerUri(@NonNull URI uri) throws URISyntaxException {
593         if (!uri.isAbsolute()) {
594             throw new URISyntaxException(uri.toString(), "Relative URI not supported");
595         }
596         if (!URI_SCHEME_NTP.equals(uri.getScheme())) {
597             throw new URISyntaxException(uri.toString(), "Unrecognized scheme");
598         }
599         String host = uri.getHost();
600         if (TextUtils.isEmpty(host)) {
601             throw new URISyntaxException(uri.toString(), "Missing host");
602         }
603         return uri;
604     }
605 
606     /** Prints debug information. */
dump(PrintWriter pw)607     public void dump(PrintWriter pw) {
608         synchronized (mConfigLock) {
609             pw.println("getNtpConfig()=" + getNtpConfig());
610             pw.println("mNtpConfigForTests=" + mNtpConfigForTests);
611         }
612 
613         pw.println("mLastSuccessfulNtpServerUri=" + mLastSuccessfulNtpServerUri);
614 
615         TimeResult timeResult = mTimeResult;
616         pw.println("mTimeResult=" + timeResult);
617         if (timeResult != null) {
618             pw.println("mTimeResult.getAgeMillis()="
619                     + Duration.ofMillis(timeResult.getAgeMillis()));
620         }
621     }
622 
623     /**
624      * The real implementation of {@link NtpTrustedTime}. Contains the parts that are more difficult
625      * to test.
626      */
627     private static final class NtpTrustedTimeImpl extends NtpTrustedTime {
628 
629         @GuardedBy("this")
630         private ConnectivityManager mConnectivityManager;
631 
632         @NonNull
633         private final Context mContext;
634 
NtpTrustedTimeImpl(@onNull Context context)635         private NtpTrustedTimeImpl(@NonNull Context context) {
636             mContext = Objects.requireNonNull(context);
637         }
638 
639         @Override
640         @VisibleForTesting
641         @Nullable
getNtpConfigInternal()642         public NtpConfig getNtpConfigInternal() {
643             final ContentResolver resolver = mContext.getContentResolver();
644             final Resources res = mContext.getResources();
645 
646             // The Settings value has priority over static config. Check settings first.
647             final String serverGlobalSetting =
648                     Settings.Global.getString(resolver, Settings.Global.NTP_SERVER);
649             final List<URI> settingsServerUris = parseNtpServerSetting(serverGlobalSetting);
650 
651             List<URI> ntpServerUris;
652             if (settingsServerUris != null) {
653                 ntpServerUris = settingsServerUris;
654             } else {
655                 String[] configValues =
656                         res.getStringArray(com.android.internal.R.array.config_ntpServers);
657                 try {
658                     List<URI> configServerUris = new ArrayList<>();
659                     for (String configValue : configValues) {
660                         configServerUris.add(parseNtpUriStrict(configValue));
661                     }
662                     ntpServerUris = configServerUris;
663                 } catch (URISyntaxException e) {
664                     ntpServerUris = null;
665                 }
666             }
667 
668             final int defaultTimeoutMillis =
669                     res.getInteger(com.android.internal.R.integer.config_ntpTimeout);
670             final Duration timeout = Duration.ofMillis(Settings.Global.getInt(
671                     resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis));
672             return ntpServerUris == null ? null : new NtpConfig(ntpServerUris, timeout);
673         }
674 
675         @Override
getDefaultNetwork()676         public Network getDefaultNetwork() {
677             ConnectivityManager connectivityManager = getConnectivityManager();
678             if (connectivityManager == null) {
679                 return null;
680             }
681             return connectivityManager.getActiveNetwork();
682         }
683 
684         @Override
isNetworkConnected(@onNull Network network)685         public boolean isNetworkConnected(@NonNull Network network) {
686             ConnectivityManager connectivityManager = getConnectivityManager();
687             if (connectivityManager == null) {
688                 return false;
689             }
690             final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
691 
692             // This connectivity check is to avoid performing a DNS lookup for the time server on a
693             // unconnected network. There are races to obtain time in Android when connectivity
694             // changes, which means that forceRefresh() can be called by various components before
695             // the network is actually available. This led in the past to DNS lookup failures being
696             // cached (~2 seconds) thereby preventing the device successfully making an NTP request
697             // when connectivity had actually been established.
698             // A side effect of check is that tests that run a fake NTP server on the device itself
699             // will only be able to use it if the active network is connected, even though loopback
700             // addresses are actually reachable.
701             if (ni == null || !ni.isConnected()) {
702                 if (LOGD) Log.d(TAG, "getNetwork: no connectivity");
703                 return false;
704             }
705             return true;
706         }
707 
getConnectivityManager()708         private synchronized ConnectivityManager getConnectivityManager() {
709             if (mConnectivityManager == null) {
710                 mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
711             }
712             if (mConnectivityManager == null) {
713                 if (LOGD) Log.d(TAG, "getConnectivityManager: no ConnectivityManager");
714             }
715             return mConnectivityManager;
716         }
717 
718         @Override
719         @Nullable
queryNtpServer( @onNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout)720         public TimeResult queryNtpServer(
721                 @NonNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout) {
722 
723             final SntpClient client = new SntpClient();
724             final String serverName = ntpServerUri.getHost();
725             final int port = ntpServerUri.getPort() == -1
726                     ? SntpClient.STANDARD_NTP_PORT : ntpServerUri.getPort();
727             final int timeoutMillis = saturatedCast(timeout.toMillis());
728             if (client.requestTime(serverName, port, timeoutMillis, network)) {
729                 int ntpUncertaintyMillis = saturatedCast(client.getRoundTripTime() / 2);
730                 InetSocketAddress ntpServerSocketAddress = client.getServerSocketAddress();
731                 return new TimeResult(
732                         client.getNtpTime(), client.getNtpTimeReference(), ntpUncertaintyMillis,
733                         ntpServerSocketAddress);
734             } else {
735                 return null;
736             }
737         }
738 
739         /**
740          * Casts a {@code long} to an {@code int}, clamping the value within the int range.
741          */
saturatedCast(long longValue)742         private static int saturatedCast(long longValue) {
743             if (longValue > Integer.MAX_VALUE) {
744                 return Integer.MAX_VALUE;
745             }
746             if (longValue < Integer.MIN_VALUE) {
747                 return Integer.MIN_VALUE;
748             }
749             return (int) longValue;
750         }
751     }
752 }
753