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

Commit 5e1aceef authored by Neil Fuller's avatar Neil Fuller Committed by Android (Google) Code Review
Browse files

Merge "Make NtpTrustedTime safer / expand docs"

parents dca6d8fe 65f0f31b
Loading
Loading
Loading
Loading
+198 −98
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.util;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentResolver;
import android.content.Context;
@@ -25,172 +27,270 @@ import android.net.Network;
import android.net.NetworkInfo;
import android.net.SntpClient;
import android.os.SystemClock;
import android.os.TimestampedValue;
import android.provider.Settings;
import android.text.TextUtils;

import com.android.internal.annotations.GuardedBy;

import java.util.Objects;
import java.util.function.Supplier;

/**
 * {@link TrustedTime} that connects with a remote NTP server as its trusted
 * time source.
 * 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 class NtpTrustedTime implements TrustedTime {

    /**
     * The result of a successful NTP query.
     *
     * @hide
     */
    public static class TimeResult {
        private final long mTimeMillis;
        private final long mElapsedRealtimeMillis;
        private final long mCertaintyMillis;

        public TimeResult(long timeMillis, long elapsedRealtimeMillis, long certaintyMillis) {
            mTimeMillis = timeMillis;
            mElapsedRealtimeMillis = elapsedRealtimeMillis;
            mCertaintyMillis = certaintyMillis;
        }

        public long getTimeMillis() {
            return mTimeMillis;
        }

        public long getElapsedRealtimeMillis() {
            return mElapsedRealtimeMillis;
        }

        public long getCertaintyMillis() {
            return mCertaintyMillis;
        }

        /** Calculates and returns the current time accounting for the age of this result. */
        public long currentTimeMillis() {
            return mTimeMillis + getAgeMillis();
        }

        /** Calculates and returns the age of this result. */
        public long getAgeMillis() {
            return SystemClock.elapsedRealtime() - mElapsedRealtimeMillis;
        }

        @Override
        public String toString() {
            return "TimeResult{"
                    + "mTimeMillis=" + mTimeMillis
                    + ", mElapsedRealtimeMillis=" + mElapsedRealtimeMillis
                    + ", mCertaintyMillis=" + mCertaintyMillis
                    + '}';
        }
    }

    private static final String TAG = "NtpTrustedTime";
    private static final boolean LOGD = false;

    private static NtpTrustedTime sSingleton;
    private static Context sContext;

    private final String mServer;
    private final long mTimeout;
    @NonNull
    private final Context mContext;

    /**
     * A supplier that returns the ConnectivityManager. The Supplier can return null if
     * ConnectivityService isn't running yet.
     */
    private final Supplier<ConnectivityManager> mConnectivityManagerSupplier =
            new Supplier<ConnectivityManager>() {
        private ConnectivityManager mConnectivityManager;

    private ConnectivityManager mCM;
        @Nullable
        @Override
        public synchronized ConnectivityManager get() {
            // We can't do this at initialization time: ConnectivityService might not be running
            // yet.
            if (mConnectivityManager == null) {
                mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
            }
            return mConnectivityManager;
        }
    };

    private boolean mHasCache;
    private long mCachedNtpTime;
    private long mCachedNtpElapsedRealtime;
    private long mCachedNtpCertainty;
    // Declared volatile and accessed outside of synchronized blocks to avoid blocking reads during
    // forceRefresh().
    private volatile TimeResult mTimeResult;

    private NtpTrustedTime(String server, long timeout) {
        if (LOGD) Log.d(TAG, "creating NtpTrustedTime using " + server);
        mServer = server;
        mTimeout = timeout;
    private NtpTrustedTime(Context context) {
        mContext = Objects.requireNonNull(context);
    }

    @UnsupportedAppUsage
    public static synchronized NtpTrustedTime getInstance(Context context) {
        if (sSingleton == null) {
            final Resources res = context.getResources();
            final ContentResolver resolver = context.getContentResolver();

            final String defaultServer = res.getString(
                    com.android.internal.R.string.config_ntpServer);
            final long defaultTimeout = res.getInteger(
                    com.android.internal.R.integer.config_ntpTimeout);

            final String secureServer = Settings.Global.getString(
                    resolver, Settings.Global.NTP_SERVER);
            final long timeout = Settings.Global.getLong(
                    resolver, Settings.Global.NTP_TIMEOUT, defaultTimeout);

            final String server = secureServer != null ? secureServer : defaultServer;
            sSingleton = new NtpTrustedTime(server, timeout);
            sContext = context;
            Context appContext = context.getApplicationContext();
            sSingleton = new NtpTrustedTime(appContext);
        }

        return sSingleton;
    }

    @Override
    @UnsupportedAppUsage
    public boolean forceRefresh() {
        // We can't do this at initialization time: ConnectivityService might not be running yet.
        synchronized (this) {
            if (mCM == null) {
                mCM = sContext.getSystemService(ConnectivityManager.class);
            }
        }

        final Network network = mCM == null ? null : mCM.getActiveNetwork();
        return forceRefresh(network);
    }

    public boolean forceRefresh(Network network) {
        if (TextUtils.isEmpty(mServer)) {
            // missing server, so no trusted time available
            NtpConnectionInfo connectionInfo = getNtpConnectionInfo();
            if (connectionInfo == null) {
                // missing server config, so no trusted time available
                if (LOGD) Log.d(TAG, "forceRefresh: invalid server config");
                return false;
            }

        // We can't do this at initialization time: ConnectivityService might not be running yet.
        synchronized (this) {
            if (mCM == null) {
                mCM = sContext.getSystemService(ConnectivityManager.class);
            }
            ConnectivityManager connectivityManager = mConnectivityManagerSupplier.get();
            if (connectivityManager == null) {
                if (LOGD) Log.d(TAG, "forceRefresh: no ConnectivityManager");
                return false;
            }

        final NetworkInfo ni = mCM == null ? null : mCM.getNetworkInfo(network);
            final Network network = connectivityManager.getActiveNetwork();
            final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
            if (ni == null || !ni.isConnected()) {
                if (LOGD) Log.d(TAG, "forceRefresh: no connectivity");
                return false;
            }


            if (LOGD) Log.d(TAG, "forceRefresh() from cache miss");
            final SntpClient client = new SntpClient();
        if (client.requestTime(mServer, (int) mTimeout, network)) {
            mHasCache = true;
            mCachedNtpTime = client.getNtpTime();
            mCachedNtpElapsedRealtime = client.getNtpTimeReference();
            mCachedNtpCertainty = client.getRoundTripTime() / 2;
            final String serverName = connectionInfo.getServer();
            final int timeoutMillis = connectionInfo.getTimeoutMillis();
            if (client.requestTime(serverName, timeoutMillis, network)) {
                long ntpCertainty = client.getRoundTripTime() / 2;
                mTimeResult = new TimeResult(
                        client.getNtpTime(), client.getNtpTimeReference(), ntpCertainty);
                return true;
            } else {
                return false;
            }
        }
    }

    @Override
    /**
     * Only kept for UnsupportedAppUsage.
     *
     * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
     */
    @Deprecated
    @UnsupportedAppUsage
    public boolean hasCache() {
        return mHasCache;
        return mTimeResult != null;
    }

    /**
     * Only kept for UnsupportedAppUsage.
     *
     * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
     */
    @Deprecated
    @Override
    public long getCacheAge() {
        if (mHasCache) {
            return SystemClock.elapsedRealtime() - mCachedNtpElapsedRealtime;
        TimeResult timeResult = mTimeResult;
        if (timeResult != null) {
            return SystemClock.elapsedRealtime() - timeResult.getElapsedRealtimeMillis();
        } else {
            return Long.MAX_VALUE;
        }
    }

    @Override
    public long getCacheCertainty() {
        if (mHasCache) {
            return mCachedNtpCertainty;
        } else {
            return Long.MAX_VALUE;
        }
    }

    @Override
    /**
     * Only kept for UnsupportedAppUsage.
     *
     * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
     */
    @Deprecated
    @UnsupportedAppUsage
    public long currentTimeMillis() {
        if (!mHasCache) {
        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 makeAuthoritative() first.
        return mCachedNtpTime + getCacheAge();
        // 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
    public long getCachedNtpTime() {
        if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit");
        return mCachedNtpTime;
        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
    public long getCachedNtpTimeReference() {
        return mCachedNtpElapsedRealtime;
        TimeResult timeResult = mTimeResult;
        return timeResult == null ? 0 : timeResult.getElapsedRealtimeMillis();
    }

    /**
     * Returns the combination of {@link #getCachedNtpTime()} and {@link
     * #getCachedNtpTimeReference()} as a {@link TimestampedValue}. This method is useful when
     * passing the time to another component that will adjust for elapsed time.
     *
     * @throws IllegalStateException if there is no cached value
     * Returns an object containing the latest NTP information available. Can return {@code null} if
     * no information is available.
     */
    public TimestampedValue<Long> getCachedNtpTimeSignal() {
        if (!mHasCache) {
            throw new IllegalStateException("Missing authoritative time source");
    @Nullable
    public TimeResult getCachedTimeResult() {
        return mTimeResult;
    }
        if (LOGD) Log.d(TAG, "getCachedNtpTimeSignal() cache hit");

        return new TimestampedValue<>(mCachedNtpElapsedRealtime, mCachedNtpTime);
    private static class NtpConnectionInfo {

        @NonNull private final String mServer;
        private final int mTimeoutMillis;

        NtpConnectionInfo(@NonNull String server, int timeoutMillis) {
            mServer = Objects.requireNonNull(server);
            mTimeoutMillis = timeoutMillis;
        }

        @NonNull
        public String getServer() {
            return mServer;
        }

        int getTimeoutMillis() {
            return mTimeoutMillis;
        }
    }

    @GuardedBy("this")
    private NtpConnectionInfo getNtpConnectionInfo() {
        final ContentResolver resolver = mContext.getContentResolver();

        final Resources res = mContext.getResources();
        final String defaultServer = res.getString(
                com.android.internal.R.string.config_ntpServer);
        final int defaultTimeoutMillis = res.getInteger(
                com.android.internal.R.integer.config_ntpTimeout);

        final String secureServer = Settings.Global.getString(
                resolver, Settings.Global.NTP_SERVER);
        final int timeoutMillis = Settings.Global.getInt(
                resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis);

        final String server = secureServer != null ? secureServer : defaultServer;
        return TextUtils.isEmpty(server) ? null : new NtpConnectionInfo(server, timeoutMillis);
    }
}
+16 −10
Original line number Diff line number Diff line
@@ -20,42 +20,48 @@ import android.compat.annotation.UnsupportedAppUsage;

/**
 * Interface that provides trusted time information, possibly coming from an NTP
 * server. Implementations may cache answers until {@link #forceRefresh()}.
 * server.
 *
 * @hide
 * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
 */
public interface TrustedTime {
    /**
     * Force update with an external trusted time source, returning {@code true}
     * when successful.
     *
     * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
     */
    @Deprecated
    @UnsupportedAppUsage
    public boolean forceRefresh();

    /**
     * Check if this instance has cached a response from a trusted time source.
     *
     * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
     */
    @Deprecated
    @UnsupportedAppUsage
    public boolean hasCache();
    boolean hasCache();

    /**
     * Return time since last trusted time source contact, or
     * {@link Long#MAX_VALUE} if never contacted.
     *
     * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
     */
    @Deprecated
    @UnsupportedAppUsage
    public long getCacheAge();

    /**
     * Return certainty of cached trusted time in milliseconds, or
     * {@link Long#MAX_VALUE} if never contacted. Smaller values are more
     * precise.
     */
    public long getCacheCertainty();

    /**
     * Return current time similar to {@link System#currentTimeMillis()},
     * possibly using a cached authoritative time source.
     *
     * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
     */
    @Deprecated
    @UnsupportedAppUsage
    public long currentTimeMillis();
    long currentTimeMillis();
}
+3 −2
Original line number Diff line number Diff line
@@ -2079,8 +2079,9 @@ class AlarmManagerService extends SystemService {
        @Override
        public long currentNetworkTimeMillis() {
            final NtpTrustedTime time = NtpTrustedTime.getInstance(getContext());
            if (time.hasCache()) {
                return time.currentTimeMillis();
            NtpTrustedTime.TimeResult ntpResult = time.getCachedTimeResult();
            if (ntpResult != null) {
                return ntpResult.currentTimeMillis();
            } else {
                throw new ParcelableException(new DateTimeException("Missing NTP fix"));
            }
+11 −5
Original line number Diff line number Diff line
@@ -154,17 +154,20 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU

    private void onPollNetworkTimeUnderWakeLock(int event) {
        // Force an NTP fix when outdated
        if (mTime.getCacheAge() >= mPollingIntervalMs) {
        NtpTrustedTime.TimeResult cachedNtpResult = mTime.getCachedTimeResult();
        if (cachedNtpResult == null || cachedNtpResult.getAgeMillis() >= mPollingIntervalMs) {
            if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh");
            mTime.forceRefresh();
            cachedNtpResult = mTime.getCachedTimeResult();
        }

        if (mTime.getCacheAge() < mPollingIntervalMs) {
        if (cachedNtpResult != null && cachedNtpResult.getAgeMillis() < mPollingIntervalMs) {
            // Obtained fresh fix; schedule next normal update
            resetAlarm(mPollingIntervalMs);

            // Suggest the time to the time detector. It may choose use it to set the system clock.
            TimestampedValue<Long> timeSignal = mTime.getCachedNtpTimeSignal();
            TimestampedValue<Long> timeSignal = new TimestampedValue<>(
                    cachedNtpResult.getElapsedRealtimeMillis(), cachedNtpResult.getTimeMillis());
            NetworkTimeSuggestion timeSuggestion = new NetworkTimeSuggestion(timeSignal);
            timeSuggestion.addDebugInfo("Origin: NetworkTimeUpdateServiceImpl. event=" + event);
            mTimeDetector.suggestNetworkTime(timeSuggestion);
@@ -275,8 +278,11 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
        TimeUtils.formatDuration(mPollingIntervalShorterMs, pw);
        pw.println("\nTryAgainTimesMax: " + mTryAgainTimesMax);
        pw.println("\nTryAgainCounter: " + mTryAgainCounter);
        pw.println("NTP cache age: " + mTime.getCacheAge());
        pw.println("NTP cache certainty: " + mTime.getCacheCertainty());
        NtpTrustedTime.TimeResult ntpResult = mTime.getCachedTimeResult();
        pw.println("NTP cache result: " + ntpResult);
        if (ntpResult != null) {
            pw.println("NTP result age: " + ntpResult.getAgeMillis());
        }
        pw.println();
    }
}
+9 −8
Original line number Diff line number Diff line
@@ -130,7 +130,8 @@ class NtpTimeHelper {

        // force refresh NTP cache when outdated
        boolean refreshSuccess = true;
        if (mNtpTime.getCacheAge() >= NTP_INTERVAL) {
        NtpTrustedTime.TimeResult ntpResult = mNtpTime.getCachedTimeResult();
        if (ntpResult == null || ntpResult.getAgeMillis() >= NTP_INTERVAL) {
            // Blocking network operation.
            refreshSuccess = mNtpTime.forceRefresh();
        }
@@ -140,17 +141,17 @@ class NtpTimeHelper {

            // only update when NTP time is fresh
            // If refreshSuccess is false, cacheAge does not drop down.
            if (mNtpTime.getCacheAge() < NTP_INTERVAL) {
                long time = mNtpTime.getCachedNtpTime();
                long timeReference = mNtpTime.getCachedNtpTimeReference();
                long certainty = mNtpTime.getCacheCertainty();
            ntpResult = mNtpTime.getCachedTimeResult();
            if (ntpResult != null && ntpResult.getAgeMillis() < NTP_INTERVAL) {
                long time = ntpResult.getTimeMillis();
                long timeReference = ntpResult.getElapsedRealtimeMillis();
                long certainty = ntpResult.getCertaintyMillis();

                if (DEBUG) {
                    long now = System.currentTimeMillis();
                    Log.d(TAG, "NTP server returned: "
                            + time + " (" + new Date(time)
                            + ") reference: " + timeReference
                            + " certainty: " + certainty
                            + time + " (" + new Date(time) + ")"
                            + " ntpResult: " + ntpResult
                            + " system time offset: " + (time - now));
                }

Loading