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     /** An in-memory config override for use during tests. */
220     @GuardedBy("this")
221     @Nullable
222     private NtpConfig mNtpConfigForTests;
223 
224     @GuardedBy("this")
225     @Nullable
226     private URI mLastSuccessfulNtpServerUri;
227 
228     // Declared volatile and accessed outside synchronized blocks to avoid blocking reads during
229     // forceRefresh().
230     private volatile TimeResult mTimeResult;
231 
NtpTrustedTime()232     protected NtpTrustedTime() {
233     }
234 
235     @UnsupportedAppUsage
getInstance(Context context)236     public static synchronized NtpTrustedTime getInstance(Context context) {
237         if (sSingleton == null) {
238             Context appContext = context.getApplicationContext();
239             sSingleton = new NtpTrustedTimeImpl(appContext);
240         }
241         return sSingleton;
242     }
243 
244     /**
245      * Overrides the NTP server config for tests. Passing {@code null} to a parameter clears the
246      * test value, i.e. so the normal value will be used next time.
247      */
setServerConfigForTests(@onNull NtpConfig ntpConfig)248     public void setServerConfigForTests(@NonNull NtpConfig ntpConfig) {
249         synchronized (this) {
250             mNtpConfigForTests = ntpConfig;
251         }
252     }
253 
254     /** Forces a refresh using the default network. */
255     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
forceRefresh()256     public boolean forceRefresh() {
257         synchronized (this) {
258             Network network = getDefaultNetwork();
259             if (network == null) {
260                 if (LOGD) Log.d(TAG, "forceRefresh: no network available");
261                 return false;
262             }
263 
264             return forceRefreshLocked(network);
265         }
266     }
267 
268     /** Forces a refresh using the specified network. */
forceRefresh(@onNull Network network)269     public boolean forceRefresh(@NonNull Network network) {
270         Objects.requireNonNull(network);
271 
272         synchronized (this) {
273             return forceRefreshLocked(network);
274         }
275     }
276 
277     @GuardedBy("this")
forceRefreshLocked(@onNull Network network)278     private boolean forceRefreshLocked(@NonNull Network network) {
279         Objects.requireNonNull(network);
280 
281         if (!isNetworkConnected(network)) {
282             if (LOGD) Log.d(TAG, "forceRefreshLocked: network=" + network + " is not connected");
283             return false;
284         }
285 
286         NtpConfig ntpConfig = getNtpConfig();
287         if (ntpConfig == null) {
288             // missing server config, so no NTP time available
289             if (LOGD) Log.d(TAG, "forceRefreshLocked: invalid server config");
290             return false;
291         }
292 
293         if (LOGD) {
294             Log.d(TAG, "forceRefreshLocked: NTP request network=" + network
295                     + " ntpConfig=" + ntpConfig);
296         }
297 
298         List<URI> unorderedServerUris = ntpConfig.getServerUris();
299 
300         // Android supports multiple NTP server URIs for situations where servers might be
301         // unreachable for some devices due to network topology, e.g. we understand that devices
302         // travelling to China often have difficulty accessing "time.android.com". Android
303         // partners may want to configure alternative URIs for devices sold globally, or those
304         // that are likely to travel to part of the world without access to the full internet.
305         //
306         // The server URI list is expected to contain one element in the general case, with two
307         // or three as the anticipated maximum. The list is never empty. Server URIs are
308         // considered to be in a rough priority order of servers to try initially (no
309         // randomization), but besides that there is assumed to be no preference.
310         //
311         // The server selection algorithm below tries to stick with a successfully accessed NTP
312         // server's URI where possible:
313         //
314         // The algorithm based on the assumption that a cluster of NTP servers sharing the same
315         // host name, particularly commercially run ones, are likely to agree more closely on
316         // the time than servers from different URIs, so it's best to be sticky. Switching
317         // between URIs could result in flip-flopping between reference clocks or involve
318         // talking to server clusters with different approaches to leap second handling.
319         //
320         // Stickiness may also be useful if some server URIs early in the list are permanently
321         // black-holing requests, or if the responses are not routed back. In those cases it's
322         // best not to try those URIs more than we have to, as might happen if the algorithm
323         // always started at the beginning of the list.
324         //
325         // Generally, we have to assume that any of the configured servers are going to be "good
326         // enough" as an external reference clock when reachable, so the stickiness is a very
327         // lightly applied bias. There's no tracking of failure rates or back-off on a per-URI
328         // basis; higher level code is expected to handle rate limiting of NTP requests in the
329         // event of failure to contact any server.
330 
331         List<URI> orderedServerUris = new ArrayList<>();
332         for (URI serverUri : unorderedServerUris) {
333             if (serverUri.equals(mLastSuccessfulNtpServerUri)) {
334                 orderedServerUris.add(0, serverUri);
335             } else {
336                 orderedServerUris.add(serverUri);
337             }
338         }
339 
340         for (URI serverUri : orderedServerUris) {
341             TimeResult timeResult = queryNtpServer(network, serverUri, ntpConfig.getTimeout());
342             // Only overwrite previous state if the request was successful.
343             if (timeResult != null) {
344                 mLastSuccessfulNtpServerUri = serverUri;
345                 mTimeResult = timeResult;
346                 return true;
347             }
348         }
349         return false;
350     }
351 
352     @GuardedBy("this")
getNtpConfig()353     private NtpConfig getNtpConfig() {
354         if (mNtpConfigForTests != null) {
355             return mNtpConfigForTests;
356         }
357         return getNtpConfigInternal();
358     }
359 
360     /**
361      * Returns the {@link NtpConfig} to use during an NTP query. This method can return {@code null}
362      * if there is no config, or the config found is invalid.
363      *
364      * <p>This method has been made public for easy replacement during tests.
365      */
366     @VisibleForTesting
367     @Nullable
getNtpConfigInternal()368     public abstract NtpConfig getNtpConfigInternal();
369 
370     /**
371      * Returns the default {@link Network} to use during an NTP query when no network is specified.
372      * This method can return {@code null} if the device hasn't fully initialized or there is no
373      * active network.
374      *
375      * <p>This method has been made public for easy replacement during tests.
376      */
377     @VisibleForTesting
378     @Nullable
getDefaultNetwork()379     public abstract Network getDefaultNetwork();
380 
381     /**
382      * Returns {@code true} if there is likely to be connectivity on the supplied network.
383      *
384      * <p>This method has been made public for easy replacement during tests.
385      */
386     @VisibleForTesting
isNetworkConnected(@onNull Network network)387     public abstract boolean isNetworkConnected(@NonNull Network network);
388 
389     /**
390      * Queries the specified NTP server. This is a blocking call. Returns {@code null} if the query
391      * fails.
392      *
393      * <p>This method has been made public for easy replacement during tests.
394      */
395     @VisibleForTesting
396     @Nullable
queryNtpServer( @onNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout)397     public abstract TimeResult queryNtpServer(
398             @NonNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout);
399 
400     /**
401      * Only kept for UnsupportedAppUsage.
402      *
403      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
404      */
405     @Deprecated
406     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
hasCache()407     public boolean hasCache() {
408         return mTimeResult != null;
409     }
410 
411     /**
412      * Only kept for UnsupportedAppUsage.
413      *
414      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
415      */
416     @Deprecated
417     @Override
getCacheAge()418     public long getCacheAge() {
419         TimeResult timeResult = mTimeResult;
420         if (timeResult != null) {
421             return SystemClock.elapsedRealtime() - timeResult.getElapsedRealtimeMillis();
422         } else {
423             return Long.MAX_VALUE;
424         }
425     }
426 
427     /**
428      * Only kept for UnsupportedAppUsage.
429      *
430      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
431      */
432     @Deprecated
433     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
currentTimeMillis()434     public long currentTimeMillis() {
435         TimeResult timeResult = mTimeResult;
436         if (timeResult == null) {
437             throw new IllegalStateException("Missing authoritative time source");
438         }
439         if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit");
440 
441         // current time is age after the last ntp cache; callers who
442         // want fresh values will hit forceRefresh() first.
443         return timeResult.currentTimeMillis();
444     }
445 
446     /**
447      * Only kept for UnsupportedAppUsage.
448      *
449      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
450      */
451     @Deprecated
452     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getCachedNtpTime()453     public long getCachedNtpTime() {
454         if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit");
455         TimeResult timeResult = mTimeResult;
456         return timeResult == null ? 0 : timeResult.getTimeMillis();
457     }
458 
459     /**
460      * Only kept for UnsupportedAppUsage.
461      *
462      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
463      */
464     @Deprecated
465     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getCachedNtpTimeReference()466     public long getCachedNtpTimeReference() {
467         TimeResult timeResult = mTimeResult;
468         return timeResult == null ? 0 : timeResult.getElapsedRealtimeMillis();
469     }
470 
471     /**
472      * Returns an object containing the latest NTP information available. Can return {@code null} if
473      * no information is available.
474      */
475     @Nullable
getCachedTimeResult()476     public TimeResult getCachedTimeResult() {
477         return mTimeResult;
478     }
479 
480     /** Sets the last received NTP time. Intended for use during tests. */
setCachedTimeResult(TimeResult timeResult)481     public void setCachedTimeResult(TimeResult timeResult) {
482         synchronized (this) {
483             mTimeResult = timeResult;
484         }
485     }
486 
487     /** Clears the last received NTP time. Intended for use during tests. */
clearCachedTimeResult()488     public void clearCachedTimeResult() {
489         synchronized (this) {
490             mTimeResult = null;
491         }
492     }
493 
494     /**
495      * Parses and returns an NTP server config URI, or throws an exception if the URI doesn't
496      * conform to expectations.
497      *
498      * <p>NTP server config URIs are in the form "ntp://{hostname}[:port]". This is not a registered
499      * IANA URI scheme.
500      */
501     @NonNull
parseNtpUriStrict(@onNull String ntpServerUriString)502     public static URI parseNtpUriStrict(@NonNull String ntpServerUriString)
503             throws URISyntaxException {
504         // java.net.URI is used in preference to android.net.Uri, since android.net.Uri is very
505         // forgiving of obvious errors. URI catches issues sooner.
506         URI unvalidatedUri = new URI(ntpServerUriString);
507         return validateNtpServerUri(unvalidatedUri);
508     }
509 
510     /**
511      * Parses a setting string and returns a list of URIs that will be accepted by {@link
512      * NtpConfig}, or {@code null} if the string is invalid.
513      *
514      * <p>The setting string is expected to be one or more server values separated by a pipe ("|")
515      * character.
516      *
517      * <p>NTP server config URIs are in the form "ntp://{hostname}[:port]". This is not a registered
518      * IANA URI scheme.
519      *
520      * <p>Unlike {@link #parseNtpUriStrict(String)} this method will not throw an exception. It
521      * checks each value for a leading "ntp:" and will call through to {@link
522      * #parseNtpUriStrict(String)} to attempt to parse it, returning {@code null} if it fails.
523      * To support legacy settings values, it will also accept string values that only consists of a
524      * server name, which will be coerced into a URI in the form "ntp://{server name}".
525      */
526     @VisibleForTesting
527     @Nullable
parseNtpServerSetting(@ullable String ntpServerSetting)528     public static List<URI> parseNtpServerSetting(@Nullable String ntpServerSetting) {
529         if (TextUtils.isEmpty(ntpServerSetting)) {
530             return null;
531         } else {
532             String[] values = ntpServerSetting.split(NTP_SETTING_SERVER_NAME_DELIMITER_REGEXP);
533             if (values.length == 0) {
534                 return null;
535             }
536 
537             List<URI> uris = new ArrayList<>();
538             for (String value : values) {
539                 if (value.startsWith(URI_SCHEME_NTP + ":")) {
540                     try {
541                         uris.add(parseNtpUriStrict(value));
542                     } catch (URISyntaxException e) {
543                         Log.w(TAG, "Rejected NTP uri setting=" + ntpServerSetting, e);
544                         return null;
545                     }
546                 } else {
547                     // This is the legacy settings path. Assumes that the string is just a host name
548                     // and creates a URI in the form ntp://<host name>
549                     try {
550                         URI uri = new URI(URI_SCHEME_NTP, /*host=*/value,
551                                 /*path=*/null, /*fragment=*/null);
552                         // Paranoia: validate just in case the host name somehow results in a bad
553                         // URI.
554                         URI validatedUri = validateNtpServerUri(uri);
555                         uris.add(validatedUri);
556                     } catch (URISyntaxException e) {
557                         Log.w(TAG, "Rejected NTP legacy setting=" + ntpServerSetting, e);
558                         return null;
559                     }
560                 }
561             }
562             return uris;
563         }
564     }
565 
566     /**
567      * Checks that the supplied URI can be used to identify an NTP server.
568      * This method currently ignores Uri components that are not used, only checking the parts that
569      * must be present. Returns the supplied {@code uri} if validation is successful.
570      */
571     @NonNull
validateNtpServerUri(@onNull URI uri)572     private static URI validateNtpServerUri(@NonNull URI uri) throws URISyntaxException {
573         if (!uri.isAbsolute()) {
574             throw new URISyntaxException(uri.toString(), "Relative URI not supported");
575         }
576         if (!URI_SCHEME_NTP.equals(uri.getScheme())) {
577             throw new URISyntaxException(uri.toString(), "Unrecognized scheme");
578         }
579         String host = uri.getHost();
580         if (TextUtils.isEmpty(host)) {
581             throw new URISyntaxException(uri.toString(), "Missing host");
582         }
583         return uri;
584     }
585 
586     /** Prints debug information. */
dump(PrintWriter pw)587     public void dump(PrintWriter pw) {
588         synchronized (this) {
589             pw.println("getNtpConfig()=" + getNtpConfig());
590             pw.println("mNtpConfigForTests=" + mNtpConfigForTests);
591             pw.println("mLastSuccessfulNtpServerUri=" + mLastSuccessfulNtpServerUri);
592             pw.println("mTimeResult=" + mTimeResult);
593             if (mTimeResult != null) {
594                 pw.println("mTimeResult.getAgeMillis()="
595                         + Duration.ofMillis(mTimeResult.getAgeMillis()));
596             }
597         }
598     }
599 
600     /**
601      * The real implementation of {@link NtpTrustedTime}. Contains the parts that are more difficult
602      * to test.
603      */
604     private static final class NtpTrustedTimeImpl extends NtpTrustedTime {
605 
606         @GuardedBy("this")
607         private ConnectivityManager mConnectivityManager;
608 
609         @NonNull
610         private final Context mContext;
611 
NtpTrustedTimeImpl(@onNull Context context)612         private NtpTrustedTimeImpl(@NonNull Context context) {
613             mContext = Objects.requireNonNull(context);
614         }
615 
616         @Override
617         @VisibleForTesting
618         @Nullable
getNtpConfigInternal()619         public NtpConfig getNtpConfigInternal() {
620             final ContentResolver resolver = mContext.getContentResolver();
621             final Resources res = mContext.getResources();
622 
623             // The Settings value has priority over static config. Check settings first.
624             final String serverGlobalSetting =
625                     Settings.Global.getString(resolver, Settings.Global.NTP_SERVER);
626             final List<URI> settingsServerUris = parseNtpServerSetting(serverGlobalSetting);
627 
628             List<URI> ntpServerUris;
629             if (settingsServerUris != null) {
630                 ntpServerUris = settingsServerUris;
631             } else {
632                 String[] configValues =
633                         res.getStringArray(com.android.internal.R.array.config_ntpServers);
634                 try {
635                     List<URI> configServerUris = new ArrayList<>();
636                     for (String configValue : configValues) {
637                         configServerUris.add(parseNtpUriStrict(configValue));
638                     }
639                     ntpServerUris = configServerUris;
640                 } catch (URISyntaxException e) {
641                     ntpServerUris = null;
642                 }
643             }
644 
645             final int defaultTimeoutMillis =
646                     res.getInteger(com.android.internal.R.integer.config_ntpTimeout);
647             final Duration timeout = Duration.ofMillis(Settings.Global.getInt(
648                     resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis));
649             return ntpServerUris == null ? null : new NtpConfig(ntpServerUris, timeout);
650         }
651 
652         @Override
getDefaultNetwork()653         public Network getDefaultNetwork() {
654             ConnectivityManager connectivityManager = getConnectivityManager();
655             if (connectivityManager == null) {
656                 return null;
657             }
658             return connectivityManager.getActiveNetwork();
659         }
660 
661         @Override
isNetworkConnected(@onNull Network network)662         public boolean isNetworkConnected(@NonNull Network network) {
663             ConnectivityManager connectivityManager = getConnectivityManager();
664             if (connectivityManager == null) {
665                 return false;
666             }
667             final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
668 
669             // This connectivity check is to avoid performing a DNS lookup for the time server on a
670             // unconnected network. There are races to obtain time in Android when connectivity
671             // changes, which means that forceRefresh() can be called by various components before
672             // the network is actually available. This led in the past to DNS lookup failures being
673             // cached (~2 seconds) thereby preventing the device successfully making an NTP request
674             // when connectivity had actually been established.
675             // A side effect of check is that tests that run a fake NTP server on the device itself
676             // will only be able to use it if the active network is connected, even though loopback
677             // addresses are actually reachable.
678             if (ni == null || !ni.isConnected()) {
679                 if (LOGD) Log.d(TAG, "getNetwork: no connectivity");
680                 return false;
681             }
682             return true;
683         }
684 
getConnectivityManager()685         private synchronized ConnectivityManager getConnectivityManager() {
686             if (mConnectivityManager == null) {
687                 mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
688             }
689             if (mConnectivityManager == null) {
690                 if (LOGD) Log.d(TAG, "getConnectivityManager: no ConnectivityManager");
691             }
692             return mConnectivityManager;
693         }
694 
695         @Override
696         @Nullable
queryNtpServer( @onNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout)697         public TimeResult queryNtpServer(
698                 @NonNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout) {
699 
700             final SntpClient client = new SntpClient();
701             final String serverName = ntpServerUri.getHost();
702             final int port = ntpServerUri.getPort() == -1
703                     ? SntpClient.STANDARD_NTP_PORT : ntpServerUri.getPort();
704             final int timeoutMillis = saturatedCast(timeout.toMillis());
705             if (client.requestTime(serverName, port, timeoutMillis, network)) {
706                 int ntpUncertaintyMillis = saturatedCast(client.getRoundTripTime() / 2);
707                 InetSocketAddress ntpServerSocketAddress = client.getServerSocketAddress();
708                 return new TimeResult(
709                         client.getNtpTime(), client.getNtpTimeReference(), ntpUncertaintyMillis,
710                         ntpServerSocketAddress);
711             } else {
712                 return null;
713             }
714         }
715 
716         /**
717          * Casts a {@code long} to an {@code int}, clamping the value within the int range.
718          */
saturatedCast(long longValue)719         private static int saturatedCast(long longValue) {
720             if (longValue > Integer.MAX_VALUE) {
721                 return Integer.MAX_VALUE;
722             }
723             if (longValue < Integer.MIN_VALUE) {
724                 return Integer.MIN_VALUE;
725             }
726             return (int) longValue;
727         }
728     }
729 }
730