Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0f2b48d1 authored by Neil Fuller's avatar Neil Fuller
Browse files

Add support for multiple NTP servers

Add support for multiple NTP servers in NtpTrustedTime. The algorithm to
select the server is documented in comments in that class. Tests have
been updated accordingly.

This commit changes config.xml, removing (string) config_ntpServer and
replacing it with (string-array) config_ntpServers. It also modifies
command line and settings behavior.

Bug: 223365217
Test: atest frameworks/base/core/tests/coretests/src/android/util/NtpTrustedTimeTest.java
Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/timedetector/
Test: atest cts/tests/tests/os/src/android/os/cts/SystemClockSntpTest.java
Test: adb shell settings put global ntp_server "ntp://time.google.com\|ntp://time.android.com"
Test: Inspection: adb shell dumpsys network_time_update_service
Test: Inspection: adb shell dumpsys time_detector
Change-Id: I5e19a4d82c9771448940b9f1db1689f21508667b
parent d3d6a4d1
Loading
Loading
Loading
Loading
+21 −6
Original line number Diff line number Diff line
@@ -11999,25 +11999,40 @@ public final class Settings {
                "nitz_network_disconnect_retention";
        /**
         * The preferred NTP server. This setting overrides Android's static xml configuration when
         * present and valid.
         * SNTP client config: The preferred NTP server. This setting overrides the static
         * config.xml configuration when present and valid.
         *
         * <p>The legacy form is the NTP server name as a string.
         * <p>Newer code should use the form: ntp://{server name}[:port] (the standard NTP port,
         * 123, is used if not specified).
         *
         * <p>The settings value can consist of a pipe ("|") delimited list of server names or
         * ntp:// URIs. When present, all server name / ntp:// URIs must be valid or the entire
         * setting value will be ignored and Android's xml config will be used.
         *
         * <p>For example, the following examples are valid:
         * <ul>
         *     <li>time.android.com</li>
         *     <li>ntp://time.android.com</li>
         *     <li>ntp://time.android.com:123</li>
         *     <li>"time.android.com"</li>
         *     <li>"ntp://time.android.com"</li>
         *     <li>"ntp://time.android.com:123"</li>
         *     <li>"time.android.com|time.other"</li>
         *     <li>"ntp://time.android.com:123|ntp://time.other:123"</li>
         *     <li>"time.android.com|ntp://time.other:123"</li>
         * </ul>
         *
         * @hide
         */
        @Readable
        public static final String NTP_SERVER = "ntp_server";
        /** Timeout in milliseconds to wait for NTP server. {@hide} */
        /**
         * SNTP client config: Timeout to wait for an NTP server response. This setting overrides
         * the static config.xml configuration when present and valid.
         *
         * <p>The value is the timeout in milliseconds. It must be > 0.
         *
         * @hide
         */
        @Readable
        public static final String NTP_TIMEOUT = "ntp_timeout";
+150 −51
Original line number Diff line number Diff line
@@ -40,6 +40,9 @@ 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;
import java.util.function.Supplier;

@@ -54,6 +57,9 @@ import java.util.function.Supplier;
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.
@@ -62,21 +68,36 @@ public abstract class NtpTrustedTime implements TrustedTime {
     */
    public static final class NtpConfig {

        @NonNull private final URI mServerUri;
        @NonNull private final List<URI> mServerUris;
        @NonNull private final Duration mTimeout;

        /**
         * Creates an instance. If the arguments are invalid then an {@link
         * IllegalArgumentException} will be thrown. See {@link #parseNtpUriStrict(String)} and
         * {@link #parseNtpServerSetting(String)} to create valid URIs.
         * 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 URI serverUri, @NonNull Duration timeout)
        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 {
                mServerUri = validateNtpServerUri(Objects.requireNonNull(serverUri));
                    URI validatedServerUri = validateNtpServerUri(
                            Objects.requireNonNull(serverUri));
                    validatedServerUris.add(validatedServerUri);
                } catch (URISyntaxException e) {
                throw new IllegalArgumentException("Bad URI", e);
                    throw new IllegalArgumentException("Bad server URI", e);
                }
            }
            mServerUris = Collections.unmodifiableList(validatedServerUris);

            if (timeout.isNegative() || timeout.isZero()) {
                throw new IllegalArgumentException("timeout < 0");
@@ -84,9 +105,10 @@ public abstract class NtpTrustedTime implements TrustedTime {
            mTimeout = timeout;
        }

        /** Returns a non-empty, immutable list of NTP server URIs. */
        @NonNull
        public URI getServerUri() {
            return mServerUri;
        public List<URI> getServerUris() {
            return mServerUris;
        }

        @NonNull
@@ -97,7 +119,7 @@ public abstract class NtpTrustedTime implements TrustedTime {
        @Override
        public String toString() {
            return "NtpConnectionInfo{"
                    + "mServerUri=" + mServerUri
                    + "mServerUris=" + mServerUris
                    + ", mTimeout=" + mTimeout
                    + '}';
        }
@@ -199,6 +221,10 @@ public abstract class NtpTrustedTime implements TrustedTime {
    @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;
@@ -245,13 +271,59 @@ public abstract class NtpTrustedTime implements TrustedTime {
                Log.d(TAG, "forceRefresh: NTP request network=" + network
                        + " ntpConfig=" + ntpConfig);
            }
            TimeResult timeResult =
                    queryNtpServer(network, ntpConfig.getServerUri(), ntpConfig.getTimeout());

            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) {
                // Keep any previous time result.
                    mLastSuccessfulNtpServerUri = serverUri;
                    mTimeResult = timeResult;
                    return true;
                }
            }
            return timeResult != null;
            return false;
        }
    }

@@ -388,7 +460,9 @@ public abstract class NtpTrustedTime implements TrustedTime {
     * <p>NTP server config URIs are in the form "ntp://{hostname}[:port]". This is not a registered
     * IANA URI scheme.
     */
    public static URI parseNtpUriStrict(String ntpServerUriString) throws URISyntaxException {
    @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);
@@ -396,50 +470,68 @@ public abstract class NtpTrustedTime implements TrustedTime {
    }

    /**
     * Parses a setting string and returns a URI that will be accepted by {@link NtpConfig}, or
     * {@code null} if the string does not produce a URI considered valid.
     * 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 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 a string that only consists of a server name, which will be coerced into
     * a URI in the form "ntp://{server name}".
     * 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
    public static URI parseNtpServerSetting(String ntpServerSetting) {
    @Nullable
    public static List<URI> parseNtpServerSetting(@Nullable String ntpServerSetting) {
        if (TextUtils.isEmpty(ntpServerSetting)) {
            return null;
        } else if (ntpServerSetting.startsWith(URI_SCHEME_NTP + ":")) {
        } 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 {
                return parseNtpUriStrict(ntpServerSetting);
                        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>
                    // 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=*/ntpServerSetting,
                        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.
                return validateNtpServerUri(uri);
                        // 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.
     */
    private static URI validateNtpServerUri(URI uri) throws URISyntaxException {
    @NonNull
    private static URI validateNtpServerUri(@NonNull URI uri) throws URISyntaxException {
        if (!uri.isAbsolute()) {
            throw new URISyntaxException(uri.toString(), "Relative URI not supported");
        }
@@ -457,6 +549,8 @@ public abstract class NtpTrustedTime implements TrustedTime {
    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()="
@@ -508,17 +602,22 @@ public abstract class NtpTrustedTime implements TrustedTime {
            // The Settings value has priority over static config. Check settings first.
            final String serverGlobalSetting =
                    Settings.Global.getString(resolver, Settings.Global.NTP_SERVER);
            final URI settingsServerInfo = parseNtpServerSetting(serverGlobalSetting);
            final List<URI> settingsServerUris = parseNtpServerSetting(serverGlobalSetting);

            URI ntpServerUri;
            if (settingsServerInfo != null) {
                ntpServerUri = settingsServerInfo;
            List<URI> ntpServerUris;
            if (settingsServerUris != null) {
                ntpServerUris = settingsServerUris;
            } else {
                String configValue = res.getString(com.android.internal.R.string.config_ntpServer);
                String[] configValues =
                        res.getStringArray(com.android.internal.R.array.config_ntpServers);
                try {
                    ntpServerUri = parseNtpUriStrict(configValue);
                    List<URI> configServerUris = new ArrayList<>();
                    for (String configValue : configValues) {
                        configServerUris.add(parseNtpUriStrict(configValue));
                    }
                    ntpServerUris = configServerUris;
                } catch (URISyntaxException e) {
                    ntpServerUri = null;
                    ntpServerUris = null;
                }
            }

@@ -526,7 +625,7 @@ public abstract class NtpTrustedTime implements TrustedTime {
                    res.getInteger(com.android.internal.R.integer.config_ntpTimeout);
            final Duration timeout = Duration.ofMillis(Settings.Global.getInt(
                    resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis));
            return ntpServerUri == null ? null : new NtpConfig(ntpServerUri, timeout);
            return ntpServerUris == null ? null : new NtpConfig(ntpServerUris, timeout);
        }

        @Override
+15 −9
Original line number Diff line number Diff line
@@ -2308,19 +2308,25 @@
         it should be disabled in that locale's resources. -->
    <bool name="config_actionMenuItemAllCaps">true</bool>

    <!-- Remote server that can provide NTP responses.
         Values must be in the form: "ntp://<host>[:port]"
    <!-- SNTP client config: NTP servers to use to obtain an accurate time.
         Items must be in the form: "ntp://<host>[:port]"
         This is not a registered IANA URI scheme. -->
    <string translatable="false" name="config_ntpServer">ntp://time.android.com</string>
    <!-- Normal polling frequency in milliseconds -->
    <string-array translatable="false" name="config_ntpServers">
        <item>ntp://time.android.com</item>
    </string-array>
    <!-- SNTP client config: Timeout to wait for an NTP server response in milliseconds. -->
    <integer name="config_ntpTimeout">5000</integer>

    <!-- Automatic "network" time detection config: Normal network time polling frequency in
         milliseconds. -->
    <integer name="config_ntpPollingInterval">64800000</integer>
    <!-- Try-again polling interval in milliseconds, in case the network request failed -->
    <!-- Automatic "network" time detection config: Try-again network time polling interval in
         milliseconds, in case the network request failed -->
    <integer name="config_ntpPollingIntervalShorter">60000</integer>
    <!-- Number of times to try again with the shorter interval, before backing
         off until the normal polling interval. A value < 0 indicates infinite. -->
    <!-- Automatic "network" time detection config: Number of times to try network time polling with
         the shorter interval, before backing off until the normal polling interval. A value < 0
         indicates infinite. -->
    <integer name="config_ntpRetry">3</integer>
    <!-- Timeout to wait for NTP server response in milliseconds. -->
    <integer name="config_ntpTimeout">5000</integer>

    <!-- Default network policy warning threshold, in megabytes. -->
    <integer name="config_networkPolicyDefaultWarning">2048</integer>
+1 −1
Original line number Diff line number Diff line
@@ -697,7 +697,7 @@
  <java-symbol type="string" name="config_forceVoiceInteractionServicePackage" />
  <java-symbol type="string" name="config_mms_user_agent" />
  <java-symbol type="string" name="config_mms_user_agent_profile_url" />
  <java-symbol type="string" name="config_ntpServer" />
  <java-symbol type="array" name="config_ntpServers" />
  <java-symbol type="string" name="config_useragentprofile_url" />
  <java-symbol type="string" name="config_appsNotReportingCrashes" />
  <java-symbol type="string" name="contentServiceSync" />
+379 −97

File changed.

Preview size limit exceeded, changes collapsed.

Loading