| /* |
| * 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; |
| } |
| } |
| } |