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

Commit bd64b8d5 authored by Neil Fuller's avatar Neil Fuller
Browse files

Add a new NetworkTimeHelper impl

Add a new NetworkTimeHelper impl and changes to support it in the
time detector service code.

Background:

When the location code that became NtpNetworkTimeHelper was first
written, Android devices were not guaranteed to be requesting the time
regularly from network time sources: it was only done if the user had
enabled automatic time detection.

That changed a few releases ago, and so the location code should always
be able to ask the time detector for the latest network time signal.

This is part of a wider goal to remove several dependencies in the
Android platform on low-level NTP client code (NtpTrustedTime class).
The NTP protocol usage should be an implementation detail, not something
that is widely known to unrelated classes that want an accurate time.

With the new impl, the SDK SystemClock.currentNetworkTimeClock() call
will ask the time detector service for the latest network time too, and
not interact with the NTP client singleton directly as it does today. It
currently needs to use the NTP client because both the location code and
time detector use the NtpTrustedTime singleton independently and it is
therefore the closest thing on Android today to an authority of "what is
the latest network time the device has obtained?".

Once the time detector is the authority on "latest network time", it
will allow the platform to apply stringent checks to things like time
sync accuracy, which is currently not well checked and heavily dependent
on network round-trip time and network delay symmmetry.

This refactoring is also potentially important for form factors like
Wear, which disable NetworkTimeUpdateService and therefore won't trigger
NtpTrustedTime while attempting to sync. There's a good chance the
location time sync is also broken on Wear (if present) because of the
unusual networking constraints. The API
SystemClock.currentNetworkTimeClock(), which was added to the public SDK
in Android T, may be unreliable or broken on Wear. In future, Wear could
call suggestNetworkTime() on the time detector service from its own
equivalent of NetworkTimeUpdateService and restore
SystemClock.currentNetworkTimeClock() behavior, while also supporting
the location stack's needs (if that is also used on Wear).

Centralizing network sync under NetworkTimeUpdateService will mean that
fewer components on devices will be syncing time for their own ends,
potentially reducing load on time servers too.

This centralization also supports options for changing how "network
time" is obtained in future, e.g. allowing easier integration of newer
protocols like NTS or Roughtime, or partner plug-ins to support
proprietary protocols.

New implementation details:

The TimeZoneDetectorNetworkTimeHelper implementation retrieves the
latest network time suggestion from the TimeDetectorInternal API. It
attempts to pass time to the GNSS code as often as the original
implementation, even when a new time signal isn't available and its
potentially repeating itself, in case GNSS code has become reliant on
that. Generally, it's hard to tell what the contract should be,
particularly with the unusual behavior around "on demand" Vs "periodic"
and the historic bug there.

The new implementation should become the default when it is considered
safe to do so, i.e. after testing when we are confident that
NetworkTimeUpdateService is behaving as well as the old
NtpNetworkTimeHelper impl when detecting connectivity, etc. This can be
done with a single boolean compile-time flag.

Other changes:

The time detector is now a dependency of the location stack, so the
SystemServer service bootstrap ordering has been adjusted.

Bug: 222295093
Test: atest services/robotests/src/com/android/server/location/gnss/TimeDetectorNetworkTimeHelperTest.java
Test: atest services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
Test: atest services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
Change-Id: I2f9a14776e9fafe426213df7cb0307a3fe541fad
parent eea78707
Loading
Loading
Loading
Loading
+19 −2
Original line number Diff line number Diff line
@@ -31,6 +31,14 @@ import java.io.PrintWriter;
 */
abstract class NetworkTimeHelper {

    /**
     * This compile-time value can be changed to switch between new and old ways to obtain network
     * time for GNSS. If you have to turn this from {@code true} to {@code false} then please create
     * a platform bug. This switch will be removed in a future release. If there are problems with
     * the new impl we'd like to hear about them.
     */
    static final boolean USE_TIME_DETECTOR_IMPL = false;

    /**
     * The callback interface used by {@link NetworkTimeHelper} to report the time to {@link
     * GnssLocationProvider}. The callback can happen at any time using the thread associated with
@@ -47,8 +55,14 @@ abstract class NetworkTimeHelper {
    static NetworkTimeHelper create(
            @NonNull Context context, @NonNull Looper looper,
            @NonNull InjectTimeCallback injectTimeCallback) {
        if (USE_TIME_DETECTOR_IMPL) {
            TimeDetectorNetworkTimeHelper.Environment environment =
                    new TimeDetectorNetworkTimeHelper.EnvironmentImpl(looper);
            return new TimeDetectorNetworkTimeHelper(environment, injectTimeCallback);
        } else {
            return new NtpNetworkTimeHelper(context, looper, injectTimeCallback);
        }
    }

    /**
     * Sets the "on demand time injection" mode.
@@ -74,7 +88,9 @@ abstract class NetworkTimeHelper {
     * Notifies that network connectivity has been established.
     *
     * <p>Called by {@link GnssLocationProvider} when the device establishes a data network
     * connection.
     * connection. This call should be removed eventually because it should be handled by the {@link
     * NetworkTimeHelper} implementation itself, but has been retained for compatibility while
     * switching implementations.
     */
    abstract void onNetworkAvailable();

@@ -82,4 +98,5 @@ abstract class NetworkTimeHelper {
     * Dumps internal state during bugreports useful for debugging.
     */
    abstract void dump(@NonNull PrintWriter pw);

}
+339 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 com.android.server.location.gnss;

import android.annotation.DurationMillisLong;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.time.UnixEpochTime;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.timedetector.NetworkTimeSuggestion;
import com.android.server.timedetector.TimeDetectorInternal;
import com.android.server.timezonedetector.StateChangeListener;

import java.io.PrintWriter;
import java.util.Objects;

/**
 * Handles injecting network time to GNSS by using information from the platform time detector.
 */
public class TimeDetectorNetworkTimeHelper extends NetworkTimeHelper {

    /** Returns {@code true} if the TimeDetectorNetworkTimeHelper is being used. */
    public static boolean isInUse() {
        return NetworkTimeHelper.USE_TIME_DETECTOR_IMPL;
    }

    /**
     * An interface exposed for easier testing that the surrounding class uses for interacting with
     * platform services, handlers, etc.
     */
    interface Environment {

        /**
         * Returns the current elapsed realtime value. The same as calling {@link
         * SystemClock#elapsedRealtime()} but easier to fake in tests.
         */
        @ElapsedRealtimeLong long elapsedRealtimeMillis();

        /**
         * Returns the latest / best network time available from the time detector service.
         */
        @Nullable NetworkTimeSuggestion getLatestNetworkTime();

        /**
         * Sets a listener that will receive a callback when the value returned by {@link
         * #getLatestNetworkTime()} has changed.
         */
        void setNetworkTimeUpdateListener(StateChangeListener stateChangeListener);

        /**
         * Requests asynchronous execution of {@link
         * TimeDetectorNetworkTimeHelper#queryAndInjectNetworkTime}, to execute as soon as possible.
         * The thread used is the same as used by {@link #requestDelayedTimeQueryCallback}.
         * Only one immediate callback can be requested at a time; requesting a new immediate
         * callback will clear any previously requested one.
         */
        void requestImmediateTimeQueryCallback(TimeDetectorNetworkTimeHelper helper, String reason);

        /**
         * Requests a delayed call to
         * {@link TimeDetectorNetworkTimeHelper#delayedQueryAndInjectNetworkTime()}.
         * The thread used is the same as used by {@link #requestImmediateTimeQueryCallback}.
         * Only one delayed callback can be scheduled at a time; requesting a new delayed callback
         * will clear any previously requested one.
         */
        void requestDelayedTimeQueryCallback(
                TimeDetectorNetworkTimeHelper helper, @DurationMillisLong long delayMillis);

        /**
         * Clear a delayed time query callback. This has no effect if no delayed callback is
         * currently set.
         */
        void clearDelayedTimeQueryCallback();
    }

    private static final String TAG = "TDNetworkTimeHelper";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    /** The maximum age of a network time signal that will be passed to GNSS. */
    @VisibleForTesting
    static final int MAX_NETWORK_TIME_AGE_MILLIS = 24 * 60 * 60 * 1000;

    /**
     * The maximum time that is allowed to pass before a network time signal should be evaluated to
     * be passed to GNSS when mOnDemandTimeInjection == false.
     */
    static final int NTP_REFRESH_INTERVAL_MILLIS = MAX_NETWORK_TIME_AGE_MILLIS;

    private final LocalLog mDumpLog = new LocalLog(10, /*useLocalTimestamps=*/false);

    /** The object the helper uses to interact with other components. */
    @NonNull private final Environment mEnvironment;
    @NonNull private final InjectTimeCallback mInjectTimeCallback;

    /** Set to true if the GNSS engine requested on-demand NTP time injections. */
    @GuardedBy("this")
    private boolean mPeriodicTimeInjectionEnabled;

    /**
     * Set to true when a network time has been injected. Used to ensure that a network time is
     * injected if this object wasn't listening when a network time signal first became available.
     */
    @GuardedBy("this")
    private boolean mNetworkTimeInjected;

    TimeDetectorNetworkTimeHelper(
            @NonNull Environment environment, @NonNull InjectTimeCallback injectTimeCallback) {
        mInjectTimeCallback = Objects.requireNonNull(injectTimeCallback);
        mEnvironment = Objects.requireNonNull(environment);

        // Start listening for new network time updates immediately.
        mEnvironment.setNetworkTimeUpdateListener(this::onNetworkTimeAvailable);
    }

    @Override
    synchronized void setPeriodicTimeInjectionMode(boolean periodicTimeInjectionEnabled) {
        // Periodic time injection has a complicated history. See b/73893222. When it is true, it
        // doesn't mean ONLY send it periodically.
        //
        // periodicTimeInjectionEnabled == true means the GNSS would like to be told the time
        // periodically in addition to all the other triggers (e.g. network available).

        mPeriodicTimeInjectionEnabled = periodicTimeInjectionEnabled;
        if (!periodicTimeInjectionEnabled) {
            // Cancel any previously scheduled periodic query.
            removePeriodicNetworkTimeQuery();
        }

        // Inject the latest network time in all cases if it is available.
        // Calling queryAndInjectNetworkTime() will cause a time signal to be injected if one is
        // available AND will cause the next periodic query to be scheduled.
        String reason = "setPeriodicTimeInjectionMode(" + periodicTimeInjectionEnabled + ")";
        mEnvironment.requestImmediateTimeQueryCallback(this, reason);
    }

    void onNetworkTimeAvailable() {
        // A new network time could become available at any time. Make sure it is passed to GNSS.
        mEnvironment.requestImmediateTimeQueryCallback(this, "onNetworkTimeAvailable");
    }

    @Override
    void onNetworkAvailable() {
        // In the original NetworkTimeHelper implementation, onNetworkAvailable() would cause an NTP
        // refresh to be made if it had previously been blocked by network issues. This
        // implementation generally relies on components associated with the time detector to
        // monitor the network and call onNetworkTimeAvailable() when a time is available. However,
        // it also checks mNetworkTimeInjected in case this component wasn't listening for
        // onNetworkTimeAvailable() when the last one became available.
        synchronized (this) {
            if (!mNetworkTimeInjected) {
                // Guard against ordering issues: This check should ensure that if a network time
                // became available before this class started listening then the initial network
                // time will still be injected.
                mEnvironment.requestImmediateTimeQueryCallback(this, "onNetworkAvailable");
            }
        }
    }

    @Override
    void demandUtcTimeInjection() {
        mEnvironment.requestImmediateTimeQueryCallback(this, "demandUtcTimeInjection");
    }

    // This method should always be invoked on the mEnvironment thread.
    void delayedQueryAndInjectNetworkTime() {
        queryAndInjectNetworkTime("delayedTimeQueryCallback");
    }

    // This method should always be invoked on the mEnvironment thread.
    synchronized void queryAndInjectNetworkTime(@NonNull String reason) {
        NetworkTimeSuggestion latestNetworkTime = mEnvironment.getLatestNetworkTime();

        maybeInjectNetworkTime(latestNetworkTime, reason);

        // Deschedule (if needed) any previously scheduled periodic query.
        removePeriodicNetworkTimeQuery();

        if (mPeriodicTimeInjectionEnabled) {
            int maxDelayMillis = NTP_REFRESH_INTERVAL_MILLIS;
            String debugMsg = "queryAndInjectNtpTime: Scheduling periodic query"
                            + " reason=" + reason
                            + " latestNetworkTime=" + latestNetworkTime
                            + " maxDelayMillis=" + maxDelayMillis;
            logToDumpLog(debugMsg);

            // GNSS is expecting periodic injections, so schedule the next one.
            mEnvironment.requestDelayedTimeQueryCallback(this, maxDelayMillis);
        }
    }

    private long calculateTimeSignalAgeMillis(
            @Nullable NetworkTimeSuggestion networkTimeSuggestion) {
        if (networkTimeSuggestion == null) {
            return Long.MAX_VALUE;
        }

        long suggestionElapsedRealtimeMillis =
                networkTimeSuggestion.getUnixEpochTime().getElapsedRealtimeMillis();
        long currentElapsedRealtimeMillis = mEnvironment.elapsedRealtimeMillis();
        return currentElapsedRealtimeMillis - suggestionElapsedRealtimeMillis;
    }

    @GuardedBy("this")
    private void maybeInjectNetworkTime(
            @Nullable NetworkTimeSuggestion latestNetworkTime, @NonNull String reason) {
        // Historically, time would only be injected if it was under a certain age. This has been
        // kept in case it is assumed by GNSS implementations.
        if (calculateTimeSignalAgeMillis(latestNetworkTime) > MAX_NETWORK_TIME_AGE_MILLIS) {
            String debugMsg = "maybeInjectNetworkTime: Not injecting latest network time"
                    + " latestNetworkTime=" + latestNetworkTime
                    + " reason=" + reason;
            logToDumpLog(debugMsg);
            return;
        }

        UnixEpochTime unixEpochTime = latestNetworkTime.getUnixEpochTime();
        long unixEpochTimeMillis = unixEpochTime.getUnixEpochTimeMillis();
        long currentTimeMillis = System.currentTimeMillis();
        String debugMsg = "maybeInjectNetworkTime: Injecting latest network time"
                + " latestNetworkTime=" + latestNetworkTime
                + " reason=" + reason
                + " System time offset millis=" + (unixEpochTimeMillis - currentTimeMillis);
        logToDumpLog(debugMsg);

        long timeReferenceMillis = unixEpochTime.getElapsedRealtimeMillis();
        int uncertaintyMillis = latestNetworkTime.getUncertaintyMillis();
        mInjectTimeCallback.injectTime(unixEpochTimeMillis, timeReferenceMillis, uncertaintyMillis);
        mNetworkTimeInjected = true;
    }

    @Override
    void dump(@NonNull PrintWriter pw) {
        pw.println("TimeDetectorNetworkTimeHelper:");

        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
        ipw.increaseIndent();
        synchronized (this) {
            ipw.println("mPeriodicTimeInjectionEnabled=" + mPeriodicTimeInjectionEnabled);
        }

        ipw.println("Debug log:");
        mDumpLog.dump(ipw);
    }

    private void logToDumpLog(@NonNull String message) {
        mDumpLog.log(message);
        if (DEBUG) {
            Log.d(TAG, message);
        }
    }

    private void removePeriodicNetworkTimeQuery() {
        // De-schedule any previously scheduled refresh. This is idempotent and has no effect if
        // there isn't one.
        mEnvironment.clearDelayedTimeQueryCallback();
    }

    /** The real implementation of {@link Environment} used outside of tests. */
    static class EnvironmentImpl implements Environment {

        /** Used to ensure one scheduled runnable is queued at a time. */
        private final Object mScheduledRunnableToken = new Object();
        /** Used to ensure one immediate runnable is queued at a time. */
        private final Object mImmediateRunnableToken = new Object();
        private final Handler mHandler;
        private final TimeDetectorInternal mTimeDetectorInternal;

        EnvironmentImpl(Looper looper) {
            mHandler = new Handler(looper);
            mTimeDetectorInternal = LocalServices.getService(TimeDetectorInternal.class);
        }

        @Override
        public long elapsedRealtimeMillis() {
            return SystemClock.elapsedRealtime();
        }

        @Override
        public NetworkTimeSuggestion getLatestNetworkTime() {
            return mTimeDetectorInternal.getLatestNetworkSuggestion();
        }

        @Override
        public void setNetworkTimeUpdateListener(StateChangeListener stateChangeListener) {
            mTimeDetectorInternal.addNetworkTimeUpdateListener(stateChangeListener);
        }

        @Override
        public void requestImmediateTimeQueryCallback(TimeDetectorNetworkTimeHelper helper,
                String reason) {
            // Ensure only one immediate callback is scheduled at a time. There's no
            // post(Runnable, Object), so we postDelayed() with a zero wait.
            synchronized (this) {
                mHandler.removeCallbacksAndMessages(mImmediateRunnableToken);
                mHandler.postDelayed(() -> helper.queryAndInjectNetworkTime(reason),
                        mImmediateRunnableToken, 0);
            }
        }

        @Override
        public void requestDelayedTimeQueryCallback(TimeDetectorNetworkTimeHelper helper,
                long delayMillis) {
            synchronized (this) {
                clearDelayedTimeQueryCallback();
                mHandler.postDelayed(helper::delayedQueryAndInjectNetworkTime,
                        mScheduledRunnableToken, delayMillis);
            }
        }

        @Override
        public synchronized void clearDelayedTimeQueryCallback() {
            mHandler.removeCallbacksAndMessages(mScheduledRunnableToken);
        }
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -129,4 +129,9 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment {
    public void dumpDebugLog(@NonNull PrintWriter printWriter) {
        SystemClockTime.dump(printWriter);
    }

    @Override
    public void runAsync(@NonNull Runnable runnable) {
        mHandler.post(runnable);
    }
}
+19 −1
Original line number Diff line number Diff line
@@ -17,10 +17,13 @@
package com.android.server.timedetector;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.time.TimeCapabilitiesAndConfig;
import android.app.time.TimeConfiguration;
import android.app.timedetector.ManualTimeSuggestion;

import com.android.server.timezonedetector.StateChangeListener;

/**
 * The internal (in-process) system server API for the time detector service.
 *
@@ -61,10 +64,25 @@ public interface TimeDetectorInternal {
    /**
     * Suggests a network time to the time detector. The suggestion may not be used by the time
     * detector to set the device's time depending on device configuration and user settings, but
     * can replace previous network suggestions received.
     * can replace previous network suggestions received. See also
     * {@link #addNetworkTimeUpdateListener(StateChangeListener)} and
     * {@link #getLatestNetworkSuggestion()}.
     */
    void suggestNetworkTime(@NonNull NetworkTimeSuggestion suggestion);

    /**
     * Adds a listener that will be notified when a new network time is available. See {@link
     * #getLatestNetworkSuggestion()}.
     */
    void addNetworkTimeUpdateListener(
            @NonNull StateChangeListener networkSuggestionUpdateListener);

    /**
     * Returns the latest / best network time received by the time detector.
     */
    @Nullable
    NetworkTimeSuggestion getLatestNetworkSuggestion();

    /**
     * Suggests a GNSS-derived time to the time detector. The suggestion may not be used by the time
     * detector to set the device's time depending on device configuration and user settings, but
+14 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.content.Context;
import android.os.Handler;

import com.android.server.timezonedetector.CurrentUserIdentityInjector;
import com.android.server.timezonedetector.StateChangeListener;

import java.util.Objects;

@@ -86,6 +87,19 @@ public class TimeDetectorInternalImpl implements TimeDetectorInternal {
        mHandler.post(() -> mTimeDetectorStrategy.suggestNetworkTime(suggestion));
    }

    @Override
    public void addNetworkTimeUpdateListener(
            @NonNull StateChangeListener networkTimeUpdateListener) {
        Objects.requireNonNull(networkTimeUpdateListener);
        mTimeDetectorStrategy.addNetworkTimeUpdateListener(networkTimeUpdateListener);
    }

    @Override
    @NonNull
    public NetworkTimeSuggestion getLatestNetworkSuggestion() {
        return mTimeDetectorStrategy.getLatestNetworkSuggestion();
    }

    @Override
    public void suggestGnssTime(@NonNull GnssTimeSuggestion suggestion) {
        Objects.requireNonNull(suggestion);
Loading