| /* | |
| * Copyright (C) 2011 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package android.util; | |
| import android.annotation.NonNull; | |
| import android.annotation.Nullable; | |
| import android.compat.annotation.UnsupportedAppUsage; | |
| import android.content.ContentResolver; | |
| import android.content.Context; | |
| import android.content.res.Resources; | |
| import android.net.ConnectivityManager; | |
| import android.net.Network; | |
| import android.net.NetworkInfo; | |
| import android.net.SntpClient; | |
| import android.os.Build; | |
| import android.os.SystemClock; | |
| import android.provider.Settings; | |
| import android.text.TextUtils; | |
| import com.android.internal.annotations.GuardedBy; | |
| import com.android.internal.annotations.VisibleForTesting; | |
| import java.io.PrintWriter; | |
| import java.net.InetSocketAddress; | |
| import java.net.URI; | |
| import java.net.URISyntaxException; | |
| import java.time.Duration; | |
| import java.time.Instant; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Objects; | |
| /** | |
| * A singleton that connects with a remote NTP server as its trusted time source. This class | |
| * is thread-safe. The {@link #forceRefresh()} method is synchronous, i.e. it may occupy the | |
| * current thread while performing an NTP request. All other threads calling {@link #forceRefresh()} | |
| * will block during that request. | |
| * | |
| * @hide | |
| */ | |
| public abstract class NtpTrustedTime implements TrustedTime { | |
| private static final String URI_SCHEME_NTP = "ntp"; | |
| @VisibleForTesting | |
| public static final String NTP_SETTING_SERVER_NAME_DELIMITER = "|"; | |
| private static final String NTP_SETTING_SERVER_NAME_DELIMITER_REGEXP = "\\|"; | |
| /** | |
| * NTP server configuration. | |
| * | |
| * @hide | |
| */ | |
| public static final class NtpConfig { | |
| @NonNull private final List<URI> mServerUris; | |
| @NonNull private final Duration mTimeout; | |
| /** | |
| * Creates an instance with the supplied properties. There must be at least one NTP server | |
| * URI and the timeout must be non-zero / non-negative. | |
| * | |
| * <p>If the arguments are invalid then an {@link IllegalArgumentException} will be thrown. | |
| * See {@link #parseNtpUriStrict(String)} and {@link #parseNtpServerSetting(String)} to | |
| * create valid URIs. | |
| */ | |
| public NtpConfig(@NonNull List<URI> serverUris, @NonNull Duration timeout) | |
| throws IllegalArgumentException { | |
| Objects.requireNonNull(serverUris); | |
| if (serverUris.isEmpty()) { | |
| throw new IllegalArgumentException("Server URIs is empty"); | |
| } | |
| List<URI> validatedServerUris = new ArrayList<>(); | |
| for (URI serverUri : serverUris) { | |
| try { | |
| URI validatedServerUri = validateNtpServerUri( | |
| Objects.requireNonNull(serverUri)); | |
| validatedServerUris.add(validatedServerUri); | |
| } catch (URISyntaxException e) { | |
| throw new IllegalArgumentException("Bad server URI", e); | |
| } | |
| } | |
| mServerUris = Collections.unmodifiableList(validatedServerUris); | |
| if (timeout.isNegative() || timeout.isZero()) { | |
| throw new IllegalArgumentException("timeout < 0"); | |
| } | |
| mTimeout = timeout; | |
| } | |
| /** Returns a non-empty, immutable list of NTP server URIs. */ | |
| @NonNull | |
| public List<URI> getServerUris() { | |
| return mServerUris; | |
| } | |
| @NonNull | |
| public Duration getTimeout() { | |
| return mTimeout; | |
| } | |
| @Override | |
| public String toString() { | |
| return "NtpConnectionInfo{" | |
| + "mServerUris=" + mServerUris | |
| + ", mTimeout=" + mTimeout | |
| + '}'; | |
| } | |
| } | |
| /** | |
| * The result of a successful NTP query. | |
| * | |
| * @hide | |
| */ | |
| // Non-final for mocking frameworks | |
| public static class TimeResult { | |
| private final long mUnixEpochTimeMillis; | |
| private final long mElapsedRealtimeMillis; | |
| private final int mUncertaintyMillis; | |
| @NonNull private final InetSocketAddress mNtpServerSocketAddress; | |
| public TimeResult( | |
| long unixEpochTimeMillis, long elapsedRealtimeMillis, int uncertaintyMillis, | |
| @NonNull InetSocketAddress ntpServerSocketAddress) { | |
| mUnixEpochTimeMillis = unixEpochTimeMillis; | |
| mElapsedRealtimeMillis = elapsedRealtimeMillis; | |
| mUncertaintyMillis = uncertaintyMillis; | |
| mNtpServerSocketAddress = Objects.requireNonNull(ntpServerSocketAddress); | |
| } | |
| public long getTimeMillis() { | |
| return mUnixEpochTimeMillis; | |
| } | |
| public long getElapsedRealtimeMillis() { | |
| return mElapsedRealtimeMillis; | |
| } | |
| public int getUncertaintyMillis() { | |
| return mUncertaintyMillis; | |
| } | |
| /** | |
| * Calculates and returns the current Unix epoch time accounting for the age of this result. | |
| */ | |
| public long currentTimeMillis() { | |
| return mUnixEpochTimeMillis + getAgeMillis(); | |
| } | |
| /** Calculates and returns the age of this result. */ | |
| public long getAgeMillis() { | |
| return getAgeMillis(SystemClock.elapsedRealtime()); | |
| } | |
| /** | |
| * Calculates and returns the age of this result relative to currentElapsedRealtimeMillis. | |
| * | |
| * @param currentElapsedRealtimeMillis - reference elapsed real time | |
| */ | |
| public long getAgeMillis(long currentElapsedRealtimeMillis) { | |
| return currentElapsedRealtimeMillis - mElapsedRealtimeMillis; | |
| } | |
| @Override | |
| public boolean equals(Object o) { | |
| if (this == o) { | |
| return true; | |
| } | |
| if (!(o instanceof TimeResult)) { | |
| return false; | |
| } | |
| TimeResult that = (TimeResult) o; | |
| return mUnixEpochTimeMillis == that.mUnixEpochTimeMillis | |
| && mElapsedRealtimeMillis == that.mElapsedRealtimeMillis | |
| && mUncertaintyMillis == that.mUncertaintyMillis | |
| && mNtpServerSocketAddress.equals( | |
| that.mNtpServerSocketAddress); | |
| } | |
| @Override | |
| public int hashCode() { | |
| return Objects.hash(mUnixEpochTimeMillis, mElapsedRealtimeMillis, mUncertaintyMillis, | |
| mNtpServerSocketAddress); | |
| } | |
| @Override | |
| public String toString() { | |
| return "TimeResult{" | |
| + "unixEpochTime=" + Instant.ofEpochMilli(mUnixEpochTimeMillis) | |
| + ", elapsedRealtime=" + Duration.ofMillis(mElapsedRealtimeMillis) | |
| + ", mUncertaintyMillis=" + mUncertaintyMillis | |
| + ", mNtpServerSocketAddress=" + mNtpServerSocketAddress | |
| + '}'; | |
| } | |
| } | |
| private static final String TAG = "NtpTrustedTime"; | |
| private static final boolean LOGD = false; | |
| private static NtpTrustedTime sSingleton; | |
| /** An in-memory config override for use during tests. */ | |
| @GuardedBy("this") | |
| @Nullable | |
| private NtpConfig mNtpConfigForTests; | |
| @GuardedBy("this") | |
| @Nullable | |
| private URI mLastSuccessfulNtpServerUri; | |
| // Declared volatile and accessed outside synchronized blocks to avoid blocking reads during | |
| // forceRefresh(). | |
| private volatile TimeResult mTimeResult; | |
| protected NtpTrustedTime() { | |
| } | |
| @UnsupportedAppUsage | |
| public static synchronized NtpTrustedTime getInstance(Context context) { | |
| if (sSingleton == null) { | |
| Context appContext = context.getApplicationContext(); | |
| sSingleton = new NtpTrustedTimeImpl(appContext); | |
| } | |
| return sSingleton; | |
| } | |
| /** | |
| * Overrides the NTP server config for tests. Passing {@code null} to a parameter clears the | |
| * test value, i.e. so the normal value will be used next time. | |
| */ | |
| public void setServerConfigForTests(@NonNull NtpConfig ntpConfig) { | |
| synchronized (this) { | |
| mNtpConfigForTests = ntpConfig; | |
| } | |
| } | |
| /** Forces a refresh using the default network. */ | |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) | |
| public boolean forceRefresh() { | |
| synchronized (this) { | |
| Network network = getDefaultNetwork(); | |
| if (network == null) { | |
| if (LOGD) Log.d(TAG, "forceRefresh: no network available"); | |
| return false; | |
| } | |
| return forceRefreshLocked(network); | |
| } | |
| } | |
| /** Forces a refresh using the specified network. */ | |
| public boolean forceRefresh(@NonNull Network network) { | |
| Objects.requireNonNull(network); | |
| synchronized (this) { | |
| return forceRefreshLocked(network); | |
| } | |
| } | |
| @GuardedBy("this") | |
| private boolean forceRefreshLocked(@NonNull Network network) { | |
| Objects.requireNonNull(network); | |
| if (!isNetworkConnected(network)) { | |
| if (LOGD) Log.d(TAG, "forceRefreshLocked: network=" + network + " is not connected"); | |
| return false; | |
| } | |
| NtpConfig ntpConfig = getNtpConfig(); | |
| if (ntpConfig == null) { | |
| // missing server config, so no NTP time available | |
| if (LOGD) Log.d(TAG, "forceRefreshLocked: invalid server config"); | |
| return false; | |
| } | |
| if (LOGD) { | |
| Log.d(TAG, "forceRefreshLocked: NTP request network=" + network | |
| + " ntpConfig=" + ntpConfig); | |
| } | |
| List<URI> unorderedServerUris = ntpConfig.getServerUris(); | |
| // Android supports multiple NTP server URIs for situations where servers might be | |
| // unreachable for some devices due to network topology, e.g. we understand that devices | |
| // travelling to China often have difficulty accessing "time.android.com". Android | |
| // partners may want to configure alternative URIs for devices sold globally, or those | |
| // that are likely to travel to part of the world without access to the full internet. | |
| // | |
| // The server URI list is expected to contain one element in the general case, with two | |
| // or three as the anticipated maximum. The list is never empty. Server URIs are | |
| // considered to be in a rough priority order of servers to try initially (no | |
| // randomization), but besides that there is assumed to be no preference. | |
| // | |
| // The server selection algorithm below tries to stick with a successfully accessed NTP | |
| // server's URI where possible: | |
| // | |
| // The algorithm based on the assumption that a cluster of NTP servers sharing the same | |
| // host name, particularly commercially run ones, are likely to agree more closely on | |
| // the time than servers from different URIs, so it's best to be sticky. Switching | |
| // between URIs could result in flip-flopping between reference clocks or involve | |
| // talking to server clusters with different approaches to leap second handling. | |
| // | |
| // Stickiness may also be useful if some server URIs early in the list are permanently | |
| // black-holing requests, or if the responses are not routed back. In those cases it's | |
| // best not to try those URIs more than we have to, as might happen if the algorithm | |
| // always started at the beginning of the list. | |
| // | |
| // Generally, we have to assume that any of the configured servers are going to be "good | |
| // enough" as an external reference clock when reachable, so the stickiness is a very | |
| // lightly applied bias. There's no tracking of failure rates or back-off on a per-URI | |
| // basis; higher level code is expected to handle rate limiting of NTP requests in the | |
| // event of failure to contact any server. | |
| List<URI> orderedServerUris = new ArrayList<>(); | |
| for (URI serverUri : unorderedServerUris) { | |
| if (serverUri.equals(mLastSuccessfulNtpServerUri)) { | |
| orderedServerUris.add(0, serverUri); | |
| } else { | |
| orderedServerUris.add(serverUri); | |
| } | |
| } | |
| for (URI serverUri : orderedServerUris) { | |
| TimeResult timeResult = queryNtpServer(network, serverUri, ntpConfig.getTimeout()); | |
| // Only overwrite previous state if the request was successful. | |
| if (timeResult != null) { | |
| mLastSuccessfulNtpServerUri = serverUri; | |
| mTimeResult = timeResult; | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| @GuardedBy("this") | |
| private NtpConfig getNtpConfig() { | |
| if (mNtpConfigForTests != null) { | |
| return mNtpConfigForTests; | |
| } | |
| return getNtpConfigInternal(); | |
| } | |
| /** | |
| * Returns the {@link NtpConfig} to use during an NTP query. This method can return {@code null} | |
| * if there is no config, or the config found is invalid. | |
| * | |
| * <p>This method has been made public for easy replacement during tests. | |
| */ | |
| @VisibleForTesting | |
| @Nullable | |
| public abstract NtpConfig getNtpConfigInternal(); | |
| /** | |
| * Returns the default {@link Network} to use during an NTP query when no network is specified. | |
| * This method can return {@code null} if the device hasn't fully initialized or there is no | |
| * active network. | |
| * | |
| * <p>This method has been made public for easy replacement during tests. | |
| */ | |
| @VisibleForTesting | |
| @Nullable | |
| public abstract Network getDefaultNetwork(); | |
| /** | |
| * Returns {@code true} if there is likely to be connectivity on the supplied network. | |
| * | |
| * <p>This method has been made public for easy replacement during tests. | |
| */ | |
| @VisibleForTesting | |
| public abstract boolean isNetworkConnected(@NonNull Network network); | |
| /** | |
| * Queries the specified NTP server. This is a blocking call. Returns {@code null} if the query | |
| * fails. | |
| * | |
| * <p>This method has been made public for easy replacement during tests. | |
| */ | |
| @VisibleForTesting | |
| @Nullable | |
| public abstract TimeResult queryNtpServer( | |
| @NonNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout); | |
| /** | |
| * Only kept for UnsupportedAppUsage. | |
| * | |
| * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. | |
| */ | |
| @Deprecated | |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) | |
| public boolean hasCache() { | |
| return mTimeResult != null; | |
| } | |
| /** | |
| * Only kept for UnsupportedAppUsage. | |
| * | |
| * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. | |
| */ | |
| @Deprecated | |
| @Override | |
| public long getCacheAge() { | |
| TimeResult timeResult = mTimeResult; | |
| if (timeResult != null) { | |
| return SystemClock.elapsedRealtime() - timeResult.getElapsedRealtimeMillis(); | |
| } else { | |
| return Long.MAX_VALUE; | |
| } | |
| } | |
| /** | |
| * Only kept for UnsupportedAppUsage. | |
| * | |
| * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. | |
| */ | |
| @Deprecated | |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) | |
| public long currentTimeMillis() { | |
| TimeResult timeResult = mTimeResult; | |
| if (timeResult == null) { | |
| throw new IllegalStateException("Missing authoritative time source"); | |
| } | |
| if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit"); | |
| // current time is age after the last ntp cache; callers who | |
| // want fresh values will hit forceRefresh() first. | |
| return timeResult.currentTimeMillis(); | |
| } | |
| /** | |
| * Only kept for UnsupportedAppUsage. | |
| * | |
| * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. | |
| */ | |
| @Deprecated | |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) | |
| public long getCachedNtpTime() { | |
| if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit"); | |
| TimeResult timeResult = mTimeResult; | |
| return timeResult == null ? 0 : timeResult.getTimeMillis(); | |
| } | |
| /** | |
| * Only kept for UnsupportedAppUsage. | |
| * | |
| * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. | |
| */ | |
| @Deprecated | |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) | |
| public long getCachedNtpTimeReference() { | |
| TimeResult timeResult = mTimeResult; | |
| return timeResult == null ? 0 : timeResult.getElapsedRealtimeMillis(); | |
| } | |
| /** | |
| * Returns an object containing the latest NTP information available. Can return {@code null} if | |
| * no information is available. | |
| */ | |
| @Nullable | |
| public TimeResult getCachedTimeResult() { | |
| return mTimeResult; | |
| } | |
| /** Sets the last received NTP time. Intended for use during tests. */ | |
| public void setCachedTimeResult(TimeResult timeResult) { | |
| synchronized (this) { | |
| mTimeResult = timeResult; | |
| } | |
| } | |
| /** Clears the last received NTP time. Intended for use during tests. */ | |
| public void clearCachedTimeResult() { | |
| synchronized (this) { | |
| mTimeResult = null; | |
| } | |
| } | |
| /** | |
| * Parses and returns an NTP server config URI, or throws an exception if the URI doesn't | |
| * conform to expectations. | |
| * | |
| * <p>NTP server config URIs are in the form "ntp://{hostname}[:port]". This is not a registered | |
| * IANA URI scheme. | |
| */ | |
| @NonNull | |
| public static URI parseNtpUriStrict(@NonNull String ntpServerUriString) | |
| throws URISyntaxException { | |
| // java.net.URI is used in preference to android.net.Uri, since android.net.Uri is very | |
| // forgiving of obvious errors. URI catches issues sooner. | |
| URI unvalidatedUri = new URI(ntpServerUriString); | |
| return validateNtpServerUri(unvalidatedUri); | |
| } | |
| /** | |
| * Parses a setting string and returns a list of URIs that will be accepted by {@link | |
| * NtpConfig}, or {@code null} if the string is invalid. | |
| * | |
| * <p>The setting string is expected to be one or more server values separated by a pipe ("|") | |
| * character. | |
| * | |
| * <p>NTP server config URIs are in the form "ntp://{hostname}[:port]". This is not a registered | |
| * IANA URI scheme. | |
| * | |
| * <p>Unlike {@link #parseNtpUriStrict(String)} this method will not throw an exception. It | |
| * checks each value for a leading "ntp:" and will call through to {@link | |
| * #parseNtpUriStrict(String)} to attempt to parse it, returning {@code null} if it fails. | |
| * To support legacy settings values, it will also accept string values that only consists of a | |
| * server name, which will be coerced into a URI in the form "ntp://{server name}". | |
| */ | |
| @VisibleForTesting | |
| @Nullable | |
| public static List<URI> parseNtpServerSetting(@Nullable String ntpServerSetting) { | |
| if (TextUtils.isEmpty(ntpServerSetting)) { | |
| return null; | |
| } else { | |
| String[] values = ntpServerSetting.split(NTP_SETTING_SERVER_NAME_DELIMITER_REGEXP); | |
| if (values.length == 0) { | |
| return null; | |
| } | |
| List<URI> uris = new ArrayList<>(); | |
| for (String value : values) { | |
| if (value.startsWith(URI_SCHEME_NTP + ":")) { | |
| try { | |
| uris.add(parseNtpUriStrict(value)); | |
| } catch (URISyntaxException e) { | |
| Log.w(TAG, "Rejected NTP uri setting=" + ntpServerSetting, e); | |
| return null; | |
| } | |
| } else { | |
| // This is the legacy settings path. Assumes that the string is just a host name | |
| // and creates a URI in the form ntp://<host name> | |
| try { | |
| URI uri = new URI(URI_SCHEME_NTP, /*host=*/value, | |
| /*path=*/null, /*fragment=*/null); | |
| // Paranoia: validate just in case the host name somehow results in a bad | |
| // URI. | |
| URI validatedUri = validateNtpServerUri(uri); | |
| uris.add(validatedUri); | |
| } catch (URISyntaxException e) { | |
| Log.w(TAG, "Rejected NTP legacy setting=" + ntpServerSetting, e); | |
| return null; | |
| } | |
| } | |
| } | |
| return uris; | |
| } | |
| } | |
| /** | |
| * Checks that the supplied URI can be used to identify an NTP server. | |
| * This method currently ignores Uri components that are not used, only checking the parts that | |
| * must be present. Returns the supplied {@code uri} if validation is successful. | |
| */ | |
| @NonNull | |
| private static URI validateNtpServerUri(@NonNull URI uri) throws URISyntaxException { | |
| if (!uri.isAbsolute()) { | |
| throw new URISyntaxException(uri.toString(), "Relative URI not supported"); | |
| } | |
| if (!URI_SCHEME_NTP.equals(uri.getScheme())) { | |
| throw new URISyntaxException(uri.toString(), "Unrecognized scheme"); | |
| } | |
| String host = uri.getHost(); | |
| if (TextUtils.isEmpty(host)) { | |
| throw new URISyntaxException(uri.toString(), "Missing host"); | |
| } | |
| return uri; | |
| } | |
| /** Prints debug information. */ | |
| public void dump(PrintWriter pw) { | |
| synchronized (this) { | |
| pw.println("getNtpConfig()=" + getNtpConfig()); | |
| pw.println("mNtpConfigForTests=" + mNtpConfigForTests); | |
| pw.println("mLastSuccessfulNtpServerUri=" + mLastSuccessfulNtpServerUri); | |
| pw.println("mTimeResult=" + mTimeResult); | |
| if (mTimeResult != null) { | |
| pw.println("mTimeResult.getAgeMillis()=" | |
| + Duration.ofMillis(mTimeResult.getAgeMillis())); | |
| } | |
| } | |
| } | |
| /** | |
| * The real implementation of {@link NtpTrustedTime}. Contains the parts that are more difficult | |
| * to test. | |
| */ | |
| private static final class NtpTrustedTimeImpl extends NtpTrustedTime { | |
| @GuardedBy("this") | |
| private ConnectivityManager mConnectivityManager; | |
| @NonNull | |
| private final Context mContext; | |
| private NtpTrustedTimeImpl(@NonNull Context context) { | |
| mContext = Objects.requireNonNull(context); | |
| } | |
| @Override | |
| @VisibleForTesting | |
| @Nullable | |
| public NtpConfig getNtpConfigInternal() { | |
| final ContentResolver resolver = mContext.getContentResolver(); | |
| final Resources res = mContext.getResources(); | |
| // The Settings value has priority over static config. Check settings first. | |
| final String serverGlobalSetting = | |
| Settings.Global.getString(resolver, Settings.Global.NTP_SERVER); | |
| final List<URI> settingsServerUris = parseNtpServerSetting(serverGlobalSetting); | |
| List<URI> ntpServerUris; | |
| if (settingsServerUris != null) { | |
| ntpServerUris = settingsServerUris; | |
| } else { | |
| String[] configValues = | |
| res.getStringArray(com.android.internal.R.array.config_ntpServers); | |
| try { | |
| List<URI> configServerUris = new ArrayList<>(); | |
| for (String configValue : configValues) { | |
| configServerUris.add(parseNtpUriStrict(configValue)); | |
| } | |
| ntpServerUris = configServerUris; | |
| } catch (URISyntaxException e) { | |
| ntpServerUris = null; | |
| } | |
| } | |
| final int defaultTimeoutMillis = | |
| res.getInteger(com.android.internal.R.integer.config_ntpTimeout); | |
| final Duration timeout = Duration.ofMillis(Settings.Global.getInt( | |
| resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis)); | |
| return ntpServerUris == null ? null : new NtpConfig(ntpServerUris, timeout); | |
| } | |
| @Override | |
| public Network getDefaultNetwork() { | |
| ConnectivityManager connectivityManager = getConnectivityManager(); | |
| if (connectivityManager == null) { | |
| return null; | |
| } | |
| return connectivityManager.getActiveNetwork(); | |
| } | |
| @Override | |
| public boolean isNetworkConnected(@NonNull Network network) { | |
| ConnectivityManager connectivityManager = getConnectivityManager(); | |
| if (connectivityManager == null) { | |
| return false; | |
| } | |
| final NetworkInfo ni = connectivityManager.getNetworkInfo(network); | |
| // This connectivity check is to avoid performing a DNS lookup for the time server on a | |
| // unconnected network. There are races to obtain time in Android when connectivity | |
| // changes, which means that forceRefresh() can be called by various components before | |
| // the network is actually available. This led in the past to DNS lookup failures being | |
| // cached (~2 seconds) thereby preventing the device successfully making an NTP request | |
| // when connectivity had actually been established. | |
| // A side effect of check is that tests that run a fake NTP server on the device itself | |
| // will only be able to use it if the active network is connected, even though loopback | |
| // addresses are actually reachable. | |
| if (ni == null || !ni.isConnected()) { | |
| if (LOGD) Log.d(TAG, "getNetwork: no connectivity"); | |
| return false; | |
| } | |
| return true; | |
| } | |
| private synchronized ConnectivityManager getConnectivityManager() { | |
| if (mConnectivityManager == null) { | |
| mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); | |
| } | |
| if (mConnectivityManager == null) { | |
| if (LOGD) Log.d(TAG, "getConnectivityManager: no ConnectivityManager"); | |
| } | |
| return mConnectivityManager; | |
| } | |
| @Override | |
| @Nullable | |
| public TimeResult queryNtpServer( | |
| @NonNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout) { | |
| final SntpClient client = new SntpClient(); | |
| final String serverName = ntpServerUri.getHost(); | |
| final int port = ntpServerUri.getPort() == -1 | |
| ? SntpClient.STANDARD_NTP_PORT : ntpServerUri.getPort(); | |
| final int timeoutMillis = saturatedCast(timeout.toMillis()); | |
| if (client.requestTime(serverName, port, timeoutMillis, network)) { | |
| int ntpUncertaintyMillis = saturatedCast(client.getRoundTripTime() / 2); | |
| InetSocketAddress ntpServerSocketAddress = client.getServerSocketAddress(); | |
| return new TimeResult( | |
| client.getNtpTime(), client.getNtpTimeReference(), ntpUncertaintyMillis, | |
| ntpServerSocketAddress); | |
| } else { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Casts a {@code long} to an {@code int}, clamping the value within the int range. | |
| */ | |
| private static int saturatedCast(long longValue) { | |
| if (longValue > Integer.MAX_VALUE) { | |
| return Integer.MAX_VALUE; | |
| } | |
| if (longValue < Integer.MIN_VALUE) { | |
| return Integer.MIN_VALUE; | |
| } | |
| return (int) longValue; | |
| } | |
| } | |
| } |