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

Commit b2a29979 authored by Neil Fuller's avatar Neil Fuller Committed by Luca Stefani
Browse files

Make NtpTrustedTime safer / expand docs

This commit makes a number of changes:
1) Documents / enforces thread safety, removes or deprecates unsafe
check-then-do methods / adds a way to get the NTP query result
atomically.
2) Delays configuration lookup until point of use: the config can change
due to various possible config overlays, e.g. MCC-based config.

(1) is because the threading model is currently unclear / possibly
unsafe - it looks like NtpTrustedTime is supposed to be single threaded
but it's also a singleton so could be accessed from multiple threads.
If NtpTrustedTime were not a singleton things might be easier but the
@UnsupportedAppUsage makes it difficult to change now.
(2) is to address the same issue as https://r.android.com/1182530,
contributed by Luca Stefani.

Bug: 140712361
Test: build only
Merged-In: Ie09da9db5d853b59829886a020de21a88da5dd51
Change-Id: Ie09da9db5d853b59829886a020de21a88da5dd51
(cherry picked from commit 65f0f31b)
parent 323e5bab
Loading
Loading
Loading
Loading
+205 −87
Original line number Original line Diff line number Diff line
@@ -16,6 +16,8 @@


package android.util;
package android.util;


import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UnsupportedAppUsage;
import android.annotation.UnsupportedAppUsage;
import android.content.ContentResolver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Context;
@@ -28,151 +30,267 @@ import android.os.SystemClock;
import android.provider.Settings;
import android.provider.Settings;
import android.text.TextUtils;
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
 * A singleton that connects with a remote NTP server as its trusted time source. This class
 * time source.
 * 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
 * @hide
 */
 */
public class NtpTrustedTime implements TrustedTime {
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 String TAG = "NtpTrustedTime";
    private static final boolean LOGD = false;
    private static final boolean LOGD = false;


    private static NtpTrustedTime sSingleton;
    private static NtpTrustedTime sSingleton;
    private static Context sContext;


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


    private ConnectivityManager mCM;
    /**
     * 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 boolean mHasCache;
        @Nullable
    private long mCachedNtpTime;
        @Override
    private long mCachedNtpElapsedRealtime;
        public synchronized ConnectivityManager get() {
    private long mCachedNtpCertainty;
            // We can't do this at initialization time: ConnectivityService might not be running
            // yet.
            if (mConnectivityManager == null) {
                mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
            }
            return mConnectivityManager;
        }
    };

    // Declared volatile and accessed outside of synchronized blocks to avoid blocking reads during
    // forceRefresh().
    private volatile TimeResult mTimeResult;


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


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

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

        return sSingleton;
        return sSingleton;
    }
    }


    @Override
    @UnsupportedAppUsage
    @UnsupportedAppUsage
    public boolean forceRefresh() {
    public boolean forceRefresh() {
        // We can't do this at initialization time: ConnectivityService might not be running yet.
        synchronized (this) {
        synchronized (this) {
            if (mCM == null) {
            NtpConnectionInfo connectionInfo = getNtpConnectionInfo();
                mCM = sContext.getSystemService(ConnectivityManager.class);
            if (connectionInfo == null) {
            }
                // missing server config, so no trusted time available
        }
                if (LOGD) Log.d(TAG, "forceRefresh: invalid server config");

        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
                return false;
                return false;
            }
            }


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

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



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


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


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


    @Override
    /**
    public long getCacheCertainty() {
     * Only kept for UnsupportedAppUsage.
        if (mHasCache) {
     *
            return mCachedNtpCertainty;
     * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
        } else {
     */
            return Long.MAX_VALUE;
    @Deprecated
        }
    }

    @Override
    @UnsupportedAppUsage
    @UnsupportedAppUsage
    public long currentTimeMillis() {
    public long currentTimeMillis() {
        if (!mHasCache) {
        TimeResult timeResult = mTimeResult;
        if (timeResult == null) {
            throw new IllegalStateException("Missing authoritative time source");
            throw new IllegalStateException("Missing authoritative time source");
        }
        }
        if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit");
        if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit");


        // current time is age after the last ntp cache; callers who
        // current time is age after the last ntp cache; callers who
        // want fresh values will hit makeAuthoritative() first.
        // want fresh values will hit forceRefresh() first.
        return mCachedNtpTime + getCacheAge();
        return timeResult.currentTimeMillis();
    }
    }


    /**
     * Only kept for UnsupportedAppUsage.
     *
     * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
     */
    @Deprecated
    @UnsupportedAppUsage
    @UnsupportedAppUsage
    public long getCachedNtpTime() {
    public long getCachedNtpTime() {
        if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit");
        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
    @UnsupportedAppUsage
    public long getCachedNtpTimeReference() {
    public long getCachedNtpTimeReference() {
        return mCachedNtpElapsedRealtime;
        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;
    }

    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 Original line Diff line number Diff line
@@ -20,42 +20,48 @@ import android.annotation.UnsupportedAppUsage;


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


    /**
    /**
     * Check if this instance has cached a response from a trusted time source.
     * 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
    @UnsupportedAppUsage
    public boolean hasCache();
    boolean hasCache();


    /**
    /**
     * Return time since last trusted time source contact, or
     * Return time since last trusted time source contact, or
     * {@link Long#MAX_VALUE} if never contacted.
     * {@link Long#MAX_VALUE} if never contacted.
     *
     * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
     */
     */
    @Deprecated
    @UnsupportedAppUsage
    @UnsupportedAppUsage
    public long getCacheAge();
    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()},
     * Return current time similar to {@link System#currentTimeMillis()},
     * possibly using a cached authoritative time source.
     * possibly using a cached authoritative time source.
     *
     * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
     */
     */
    @Deprecated
    @UnsupportedAppUsage
    @UnsupportedAppUsage
    public long currentTimeMillis();
    long currentTimeMillis();
}
}
+3 −2
Original line number Original line Diff line number Diff line
@@ -2167,8 +2167,9 @@ class AlarmManagerService extends SystemService {
        @Override
        @Override
        public long currentNetworkTimeMillis() {
        public long currentNetworkTimeMillis() {
            final NtpTrustedTime time = NtpTrustedTime.getInstance(getContext());
            final NtpTrustedTime time = NtpTrustedTime.getInstance(getContext());
            if (time.hasCache()) {
            NtpTrustedTime.TimeResult ntpResult = time.getCachedTimeResult();
                return time.currentTimeMillis();
            if (ntpResult != null) {
                return ntpResult.currentTimeMillis();
            } else {
            } else {
                throw new ParcelableException(new DateTimeException("Missing NTP fix"));
                throw new ParcelableException(new DateTimeException("Missing NTP fix"));
            }
            }
+9 −4
Original line number Original line Diff line number Diff line
@@ -165,12 +165,14 @@ public class NewNetworkTimeUpdateService extends Binder implements NetworkTimeUp


    private void onPollNetworkTimeUnderWakeLock(int event) {
    private void onPollNetworkTimeUnderWakeLock(int event) {
        // Force an NTP fix when outdated
        // 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");
            if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh");
            mTime.forceRefresh();
            mTime.forceRefresh();
            cachedNtpResult = mTime.getCachedTimeResult();
        }
        }


        if (mTime.getCacheAge() < mPollingIntervalMs) {
        if (cachedNtpResult != null && cachedNtpResult.getAgeMillis() < mPollingIntervalMs) {
            // Obtained fresh fix; schedule next normal update
            // Obtained fresh fix; schedule next normal update
            resetAlarm(mPollingIntervalMs);
            resetAlarm(mPollingIntervalMs);
            if (isAutomaticTimeRequested()) {
            if (isAutomaticTimeRequested()) {
@@ -322,8 +324,11 @@ public class NewNetworkTimeUpdateService extends Binder implements NetworkTimeUp
        pw.print("TimeErrorThresholdMs: ");
        pw.print("TimeErrorThresholdMs: ");
        TimeUtils.formatDuration(mTimeErrorThresholdMs, pw);
        TimeUtils.formatDuration(mTimeErrorThresholdMs, pw);
        pw.println("\nTryAgainCounter: " + mTryAgainCounter);
        pw.println("\nTryAgainCounter: " + mTryAgainCounter);
        pw.println("NTP cache age: " + mTime.getCacheAge());
        NtpTrustedTime.TimeResult ntpResult = mTime.getCachedTimeResult();
        pw.println("NTP cache certainty: " + mTime.getCacheCertainty());
        pw.println("NTP cache result: " + ntpResult);
        if (ntpResult != null) {
            pw.println("NTP result age: " + ntpResult.getAgeMillis());
        }
        pw.println();
        pw.println();
    }
    }
}
}
+9 −4
Original line number Original line Diff line number Diff line
@@ -165,12 +165,14 @@ public class OldNetworkTimeUpdateService extends Binder implements NetworkTimeUp


    private void onPollNetworkTimeUnderWakeLock(int event) {
    private void onPollNetworkTimeUnderWakeLock(int event) {
        // Force an NTP fix when outdated
        // 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");
            if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh");
            mTime.forceRefresh();
            mTime.forceRefresh();
            cachedNtpResult = mTime.getCachedTimeResult();
        }
        }


        if (mTime.getCacheAge() < mPollingIntervalMs) {
        if (cachedNtpResult != null && cachedNtpResult.getAgeMillis() < mPollingIntervalMs) {
            // Obtained fresh fix; schedule next normal update
            // Obtained fresh fix; schedule next normal update
            resetAlarm(mPollingIntervalMs);
            resetAlarm(mPollingIntervalMs);
            if (isAutomaticTimeRequested()) {
            if (isAutomaticTimeRequested()) {
@@ -322,8 +324,11 @@ public class OldNetworkTimeUpdateService extends Binder implements NetworkTimeUp
        pw.print("TimeErrorThresholdMs: ");
        pw.print("TimeErrorThresholdMs: ");
        TimeUtils.formatDuration(mTimeErrorThresholdMs, pw);
        TimeUtils.formatDuration(mTimeErrorThresholdMs, pw);
        pw.println("\nTryAgainCounter: " + mTryAgainCounter);
        pw.println("\nTryAgainCounter: " + mTryAgainCounter);
        pw.println("NTP cache age: " + mTime.getCacheAge());
        NtpTrustedTime.TimeResult ntpResult = mTime.getCachedTimeResult();
        pw.println("NTP cache certainty: " + mTime.getCacheCertainty());
        pw.println("NTP cache result: " + ntpResult);
        if (ntpResult != null) {
            pw.println("NTP result age: " + ntpResult.getAgeMillis());
        }
        pw.println();
        pw.println();
    }
    }
}
}
Loading