Merge "Add a new NetworkTimeHelper impl"
diff --git a/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java b/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java
index 3a25146..f5114b7 100644
--- a/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java
+++ b/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java
@@ -32,6 +32,14 @@
 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
      * the looper passed to {@link #create(Context, Looper, InjectTimeCallback)}.
@@ -47,7 +55,13 @@
     static NetworkTimeHelper create(
             @NonNull Context context, @NonNull Looper looper,
             @NonNull InjectTimeCallback injectTimeCallback) {
-        return new NtpNetworkTimeHelper(context, looper, 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);
+        }
     }
 
     /**
@@ -74,7 +88,9 @@
      * 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 @@
      * Dumps internal state during bugreports useful for debugging.
      */
     abstract void dump(@NonNull PrintWriter pw);
+
 }
diff --git a/services/core/java/com/android/server/location/gnss/TimeDetectorNetworkTimeHelper.java b/services/core/java/com/android/server/location/gnss/TimeDetectorNetworkTimeHelper.java
new file mode 100644
index 0000000..15366d3
--- /dev/null
+++ b/services/core/java/com/android/server/location/gnss/TimeDetectorNetworkTimeHelper.java
@@ -0,0 +1,339 @@
+/*
+ * 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);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
index 5801920..fc960d8 100644
--- a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
+++ b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
@@ -129,4 +129,9 @@
     public void dumpDebugLog(@NonNull PrintWriter printWriter) {
         SystemClockTime.dump(printWriter);
     }
+
+    @Override
+    public void runAsync(@NonNull Runnable runnable) {
+        mHandler.post(runnable);
+    }
 }
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorInternal.java b/services/core/java/com/android/server/timedetector/TimeDetectorInternal.java
index 5df5cbc..4b65c55 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorInternal.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorInternal.java
@@ -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,11 +64,26 @@
     /**
      * 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
      * can replace previous GNSS suggestions received.
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorInternalImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorInternalImpl.java
index af168f8..7e96a43 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorInternalImpl.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorInternalImpl.java
@@ -24,6 +24,7 @@
 import android.os.Handler;
 
 import com.android.server.timezonedetector.CurrentUserIdentityInjector;
+import com.android.server.timezonedetector.StateChangeListener;
 
 import java.util.Objects;
 
@@ -87,6 +88,19 @@
     }
 
     @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);
 
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java
index a9dcff4..0da967a 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorService.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorService.java
@@ -47,6 +47,7 @@
 import com.android.internal.util.DumpUtils;
 import com.android.server.FgThread;
 import com.android.server.SystemService;
+import com.android.server.location.gnss.TimeDetectorNetworkTimeHelper;
 import com.android.server.timezonedetector.CallerIdentityInjector;
 import com.android.server.timezonedetector.CurrentUserIdentityInjector;
 
@@ -405,13 +406,19 @@
         // TODO(b/222295093): Return the latest network time from mTimeDetectorStrategy once we can
         //  be sure that all uses of NtpTrustedTime results in a suggestion being made to the time
         //  detector. mNtpTrustedTime can be removed once this happens.
-        NtpTrustedTime.TimeResult ntpResult = mNtpTrustedTime.getCachedTimeResult();
-        if (ntpResult != null) {
-            UnixEpochTime unixEpochTime = new UnixEpochTime(
-                    ntpResult.getElapsedRealtimeMillis(), ntpResult.getTimeMillis());
-            return new NetworkTimeSuggestion(unixEpochTime, ntpResult.getUncertaintyMillis());
+        if (TimeDetectorNetworkTimeHelper.isInUse()) {
+            // The new implementation.
+            return mTimeDetectorStrategy.getLatestNetworkSuggestion();
         } else {
-            return null;
+            // The old implementation.
+            NtpTrustedTime.TimeResult ntpResult = mNtpTrustedTime.getCachedTimeResult();
+            if (ntpResult != null) {
+                UnixEpochTime unixEpochTime = new UnixEpochTime(
+                        ntpResult.getElapsedRealtimeMillis(), ntpResult.getTimeMillis());
+                return new NetworkTimeSuggestion(unixEpochTime, ntpResult.getUncertaintyMillis());
+            } else {
+                return null;
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
index dbd7172..11cec66 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
@@ -29,6 +29,7 @@
 
 import com.android.internal.util.Preconditions;
 import com.android.server.timezonedetector.Dumpable;
+import com.android.server.timezonedetector.StateChangeListener;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -104,11 +105,19 @@
     /**
      * Processes the suggested network time. The suggestion may not be used to set the device's time
      * depending on device configuration and user settings, but can replace previous network
-     * suggestions received.
+     * 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 (accepted) network time suggestion. Returns {@code null} if there isn't
      * one.
      */
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
index d679bbe..b293bac 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
@@ -36,6 +36,7 @@
 import android.app.timedetector.TelephonyTimeSuggestion;
 import android.content.Context;
 import android.os.Handler;
+import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 
@@ -125,6 +126,9 @@
     private final ReferenceWithHistory<ExternalTimeSuggestion> mLastExternalSuggestion =
             new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
 
+    @GuardedBy("this")
+    private final ArraySet<StateChangeListener> mNetworkTimeUpdateListeners = new ArraySet<>();
+
     /**
      * Used by {@link TimeDetectorStrategyImpl} to interact with device configuration / settings
      * / system properties. It can be faked for testing.
@@ -180,6 +184,11 @@
          * Dumps the time debug log to the supplied {@link PrintWriter}.
          */
         void dumpDebugLog(PrintWriter printWriter);
+
+        /**
+         * Requests that the supplied runnable is invoked asynchronously.
+         */
+        void runAsync(@NonNull Runnable runnable);
     }
 
     static TimeDetectorStrategy create(
@@ -307,6 +316,7 @@
         NetworkTimeSuggestion lastNetworkSuggestion = mLastNetworkSuggestion.get();
         if (lastNetworkSuggestion == null || !lastNetworkSuggestion.equals(suggestion)) {
             mLastNetworkSuggestion.set(suggestion);
+            notifyNetworkTimeUpdateListenersAsynchronously();
         }
 
         // Now perform auto time detection. The new suggestion may be used to modify the system
@@ -315,6 +325,20 @@
         doAutoTimeDetection(reason);
     }
 
+    @GuardedBy("this")
+    private void notifyNetworkTimeUpdateListenersAsynchronously() {
+        for (StateChangeListener listener : mNetworkTimeUpdateListeners) {
+            // This is queuing asynchronous notification, so no need to surrender the "this" lock.
+            mEnvironment.runAsync(listener::onChange);
+        }
+    }
+
+    @Override
+    public synchronized void addNetworkTimeUpdateListener(
+            @NonNull StateChangeListener networkSuggestionUpdateListener) {
+        mNetworkTimeUpdateListeners.add(networkSuggestionUpdateListener);
+    }
+
     @Override
     @Nullable
     public synchronized NetworkTimeSuggestion getLatestNetworkSuggestion() {
@@ -325,6 +349,8 @@
     public synchronized void clearLatestNetworkSuggestion() {
         mLastNetworkSuggestion.set(null);
 
+        notifyNetworkTimeUpdateListenersAsynchronously();
+
         // The loss of network time may change the time signal to use to set the system clock.
         String reason = "Network time cleared";
         doAutoTimeDetection(reason);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index b117cae..850b5b6 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2095,6 +2095,14 @@
             mSystemServiceManager.startService(DeviceStorageMonitorService.class);
             t.traceEnd();
 
+            t.traceBegin("StartTimeDetectorService");
+            try {
+                mSystemServiceManager.startService(TIME_DETECTOR_SERVICE_CLASS);
+            } catch (Throwable e) {
+                reportWtf("starting TimeDetectorService service", e);
+            }
+            t.traceEnd();
+
             t.traceBegin("StartLocationManagerService");
             mSystemServiceManager.startService(LocationManagerService.Lifecycle.class);
             t.traceEnd();
@@ -2108,14 +2116,6 @@
             }
             t.traceEnd();
 
-            t.traceBegin("StartTimeDetectorService");
-            try {
-                mSystemServiceManager.startService(TIME_DETECTOR_SERVICE_CLASS);
-            } catch (Throwable e) {
-                reportWtf("starting TimeDetectorService service", e);
-            }
-            t.traceEnd();
-
             t.traceBegin("StartTimeZoneDetectorService");
             try {
                 mSystemServiceManager.startService(TIME_ZONE_DETECTOR_SERVICE_CLASS);
diff --git a/services/robotests/src/com/android/server/location/gnss/TimeDetectorNetworkTimeHelperTest.java b/services/robotests/src/com/android/server/location/gnss/TimeDetectorNetworkTimeHelperTest.java
new file mode 100644
index 0000000..3e2e46c
--- /dev/null
+++ b/services/robotests/src/com/android/server/location/gnss/TimeDetectorNetworkTimeHelperTest.java
@@ -0,0 +1,399 @@
+/*
+ * 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 static com.android.server.location.gnss.TimeDetectorNetworkTimeHelper.MAX_NETWORK_TIME_AGE_MILLIS;
+import static com.android.server.location.gnss.TimeDetectorNetworkTimeHelper.NTP_REFRESH_INTERVAL_MILLIS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.app.time.UnixEpochTime;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.location.gnss.NetworkTimeHelper.InjectTimeCallback;
+import com.android.server.location.gnss.TimeDetectorNetworkTimeHelper.Environment;
+import com.android.server.timedetector.NetworkTimeSuggestion;
+import com.android.server.timezonedetector.StateChangeListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+/**
+ * Unit tests for {@link TimeDetectorNetworkTimeHelper}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class TimeDetectorNetworkTimeHelperTest {
+
+    private static final NetworkTimeSuggestion ARBITRARY_NETWORK_TIME =
+            new NetworkTimeSuggestion(new UnixEpochTime(1234L, 7777L), 123);
+
+    private FakeEnvironment mFakeEnvironment;
+    @Mock private InjectTimeCallback mMockInjectTimeCallback;
+    private TimeDetectorNetworkTimeHelper mTimeDetectorNetworkTimeHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mFakeEnvironment = new FakeEnvironment();
+        mTimeDetectorNetworkTimeHelper = new TimeDetectorNetworkTimeHelper(
+                mFakeEnvironment, mMockInjectTimeCallback);
+
+        // TimeDetectorNetworkTimeHelper should register for network time updates during
+        // construction.
+        mFakeEnvironment.assertHasNetworkTimeChangeListener();
+    }
+
+    @Test
+    public void setPeriodicTimeInjectionMode_true() {
+        testSetPeriodicTimeInjectionMode(true);
+    }
+
+    @Test
+    public void setPeriodicTimeInjectionMode_false() {
+        testSetPeriodicTimeInjectionMode(false);
+    }
+
+    private void testSetPeriodicTimeInjectionMode(boolean periodicTimeInjectionMode) {
+        NetworkTimeSuggestion networkTime = ARBITRARY_NETWORK_TIME;
+        int millisElapsedSinceNetworkTimeReceived = 1000;
+        mFakeEnvironment.pokeLatestNetworkTime(networkTime);
+
+        long currentElapsedRealtimeMillis =
+                networkTime.getUnixEpochTime().getElapsedRealtimeMillis()
+                        + millisElapsedSinceNetworkTimeReceived;
+        mFakeEnvironment.pokeElapsedRealtimeMillis(currentElapsedRealtimeMillis);
+
+        mTimeDetectorNetworkTimeHelper.setPeriodicTimeInjectionMode(periodicTimeInjectionMode);
+
+        // All injections are async, so we have to simulate the async work taking place.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+        mFakeEnvironment.assertHasImmediateCallback();
+        mFakeEnvironment.simulateTimeAdvancing(1);
+
+        // Any call to setPeriodicTimeInjectionMode() should result in an (async) injected time
+        verify(mMockInjectTimeCallback).injectTime(
+                networkTime.getUnixEpochTime().getUnixEpochTimeMillis(),
+                networkTime.getUnixEpochTime().getElapsedRealtimeMillis(),
+                networkTime.getUncertaintyMillis());
+
+        // Check whether the scheduled async is set up / not set up for the periodic request.
+        if (periodicTimeInjectionMode) {
+            mFakeEnvironment.assertHasScheduledAsyncCallback(
+                    mFakeEnvironment.elapsedRealtimeMillis() + NTP_REFRESH_INTERVAL_MILLIS);
+        } else {
+            mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+        }
+    }
+
+    @Test
+    public void periodicInjectionBehavior() {
+        // Set the elapsed realtime clock to an arbitrary start value.
+        mFakeEnvironment.pokeElapsedRealtimeMillis(12345L);
+
+        // Configure periodic time injections. Doing so should cause a time query, but no time is
+        // available.
+        mTimeDetectorNetworkTimeHelper.setPeriodicTimeInjectionMode(true);
+
+        // All query/injections are async, so we have to simulate the async work taking place.
+        mFakeEnvironment.assertHasImmediateCallback();
+        mFakeEnvironment.simulateTimeAdvancing(1);
+
+        // No time available, so no injection.
+        verifyNoMoreInteractions(mMockInjectTimeCallback);
+
+        // A periodic check should be scheduled.
+        mFakeEnvironment.assertHasScheduledAsyncCallback(
+                mFakeEnvironment.elapsedRealtimeMillis() + NTP_REFRESH_INTERVAL_MILLIS);
+
+        // Time passes...
+        mFakeEnvironment.simulateTimeAdvancing(NTP_REFRESH_INTERVAL_MILLIS / 2);
+
+        // A network time becomes available: This should cause the registered listener to trigger.
+        NetworkTimeSuggestion networkTime = ARBITRARY_NETWORK_TIME;
+        mFakeEnvironment.simulateLatestNetworkTimeChange(networkTime);
+
+        // All query/injections are async, so we have to simulate the async work taking place,
+        // causing a query, time injection and a re-schedule.
+        mFakeEnvironment.simulateTimeAdvancing(1);
+        verify(mMockInjectTimeCallback).injectTime(
+                networkTime.getUnixEpochTime().getUnixEpochTimeMillis(),
+                networkTime.getUnixEpochTime().getElapsedRealtimeMillis(),
+                networkTime.getUncertaintyMillis());
+
+        // A new periodic check should be scheduled.
+        mFakeEnvironment.assertHasNoImmediateCallback();
+
+        mFakeEnvironment.assertHasScheduledAsyncCallback(
+                mFakeEnvironment.elapsedRealtimeMillis() + NTP_REFRESH_INTERVAL_MILLIS);
+
+        int arbitraryIterationCount = 3;
+        for (int i = 0; i < arbitraryIterationCount; i++) {
+            // Advance by the amount needed for the scheduled work to run. That work should query
+            // and inject.
+            mFakeEnvironment.simulateTimeAdvancing(NTP_REFRESH_INTERVAL_MILLIS);
+
+            // All query/injections are async, so we have to simulate the async work taking place,
+            // causing a query, time injection and a re-schedule.
+            verify(mMockInjectTimeCallback).injectTime(
+                    networkTime.getUnixEpochTime().getUnixEpochTimeMillis(),
+                    networkTime.getUnixEpochTime().getElapsedRealtimeMillis(),
+                    networkTime.getUncertaintyMillis());
+
+            // A new periodic check should be scheduled.
+            mFakeEnvironment.assertHasScheduledAsyncCallback(
+                    mFakeEnvironment.elapsedRealtimeMillis() + NTP_REFRESH_INTERVAL_MILLIS);
+            mFakeEnvironment.assertHasNoImmediateCallback();
+        }
+    }
+
+    @Test
+    public void networkTimeAvailableBehavior() {
+        // Set the elapsed realtime clock to an arbitrary start value.
+        mFakeEnvironment.pokeElapsedRealtimeMillis(12345L);
+
+        // No periodic time injections. This call causes a time query, but no time is available yet.
+        mTimeDetectorNetworkTimeHelper.setPeriodicTimeInjectionMode(false);
+
+        // All query/injections are async, so we have to simulate the async work taking place.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+        mFakeEnvironment.assertHasImmediateCallback();
+        mFakeEnvironment.simulateTimeAdvancing(1);
+
+        // No time available, so no injection.
+        verifyNoMoreInteractions(mMockInjectTimeCallback);
+
+        // No periodic check should be scheduled.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+
+        // Time passes...
+        mFakeEnvironment.simulateTimeAdvancing(NTP_REFRESH_INTERVAL_MILLIS / 2);
+
+        // A network time becomes available: This should cause the registered listener to trigger
+        // and cause time to be injected.
+        NetworkTimeSuggestion networkTime = ARBITRARY_NETWORK_TIME;
+        mFakeEnvironment.simulateLatestNetworkTimeChange(networkTime);
+
+        // All query/injections are async, so we have to simulate the async work taking place,
+        // causing a query, time injection and a re-schedule.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+        mFakeEnvironment.assertHasImmediateCallback();
+        mFakeEnvironment.simulateTimeAdvancing(1);
+        verify(mMockInjectTimeCallback).injectTime(
+                networkTime.getUnixEpochTime().getUnixEpochTimeMillis(),
+                networkTime.getUnixEpochTime().getElapsedRealtimeMillis(),
+                networkTime.getUncertaintyMillis());
+
+        // No periodic check should be scheduled.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+        mFakeEnvironment.assertHasNoImmediateCallback();
+    }
+
+    @Test
+    public void networkConnectivityAvailableBehavior() {
+        // Set the elapsed realtime clock to an arbitrary start value.
+        mFakeEnvironment.pokeElapsedRealtimeMillis(12345L);
+
+        // No periodic time injections. This call causes a time query, but no time is available yet.
+        mTimeDetectorNetworkTimeHelper.setPeriodicTimeInjectionMode(false);
+
+        // All query/injections are async, so we have to simulate the async work taking place.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+        mFakeEnvironment.assertHasImmediateCallback();
+        mFakeEnvironment.simulateTimeAdvancing(1);
+
+        // No time available, so no injection.
+        verifyNoMoreInteractions(mMockInjectTimeCallback);
+
+        // No periodic check should be scheduled.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+
+        // Time passes...
+        mFakeEnvironment.simulateTimeAdvancing(NTP_REFRESH_INTERVAL_MILLIS / 2);
+
+        NetworkTimeSuggestion networkTime = ARBITRARY_NETWORK_TIME;
+        mFakeEnvironment.pokeLatestNetworkTime(networkTime);
+
+        // Simulate location code noticing that connectivity has changed and notifying the helper.
+        mTimeDetectorNetworkTimeHelper.onNetworkAvailable();
+
+        // All query/injections are async, so we have to simulate the async work taking place,
+        // causing a query, time injection and a re-schedule.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+        mFakeEnvironment.assertHasImmediateCallback();
+        mFakeEnvironment.simulateTimeAdvancing(1);
+        verify(mMockInjectTimeCallback).injectTime(
+                networkTime.getUnixEpochTime().getUnixEpochTimeMillis(),
+                networkTime.getUnixEpochTime().getElapsedRealtimeMillis(),
+                networkTime.getUncertaintyMillis());
+
+        // No periodic check should be scheduled.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+        mFakeEnvironment.assertHasNoImmediateCallback();
+    }
+
+    @Test
+    public void oldTimesNotInjected() {
+        NetworkTimeSuggestion networkTime = ARBITRARY_NETWORK_TIME;
+        mFakeEnvironment.pokeLatestNetworkTime(networkTime);
+
+        int millisElapsedSinceNetworkTimeReceived = MAX_NETWORK_TIME_AGE_MILLIS;
+        long currentElapsedRealtimeMillis =
+                networkTime.getUnixEpochTime().getElapsedRealtimeMillis()
+                        + millisElapsedSinceNetworkTimeReceived;
+        mFakeEnvironment.pokeElapsedRealtimeMillis(currentElapsedRealtimeMillis);
+
+        mTimeDetectorNetworkTimeHelper.setPeriodicTimeInjectionMode(true);
+
+        // All injections are async, so we have to simulate the async work taking place.
+        mFakeEnvironment.assertHasNoScheduledAsyncCallback();
+        mFakeEnvironment.assertHasImmediateCallback();
+
+        // The age of the network time will now be MAX_NETWORK_TIME_AGE_MILLIS + 1, which is too
+        // old to inject.
+        mFakeEnvironment.simulateTimeAdvancing(1);
+
+        // Old network times should not be injected.
+        verify(mMockInjectTimeCallback, never()).injectTime(anyLong(), anyLong(), anyInt());
+
+        // Check whether the scheduled async is set up / not set up for the periodic request.
+        mFakeEnvironment.assertHasScheduledAsyncCallback(
+                mFakeEnvironment.elapsedRealtimeMillis() + NTP_REFRESH_INTERVAL_MILLIS);
+    }
+
+    /** A fake implementation of {@link Environment} for use by this test. */
+    private static class FakeEnvironment implements Environment {
+
+        private StateChangeListener mNetworkTimeUpdateListener;
+
+        private long mCurrentElapsedRealtimeMillis;
+        private NetworkTimeSuggestion mLatestNetworkTime;
+
+        private TimeDetectorNetworkTimeHelper mImmediateAsyncCallback;
+        private String mImmediateAsyncCallbackReason;
+
+        private TimeDetectorNetworkTimeHelper mScheduledAsyncCallback;
+        private long mScheduledAsyncRunnableTimeMillis;
+
+        @Override
+        public long elapsedRealtimeMillis() {
+            return mCurrentElapsedRealtimeMillis;
+        }
+
+        @Override
+        public NetworkTimeSuggestion getLatestNetworkTime() {
+            return mLatestNetworkTime;
+        }
+
+        @Override
+        public void setNetworkTimeUpdateListener(StateChangeListener stateChangeListener) {
+            mNetworkTimeUpdateListener = stateChangeListener;
+        }
+
+        @Override
+        public void requestImmediateTimeQueryCallback(TimeDetectorNetworkTimeHelper helper,
+                String reason) {
+            if (mImmediateAsyncCallback != null) {
+                fail("Only one immediate callback expected at a time, found reason: "
+                        + mImmediateAsyncCallbackReason);
+            }
+            mImmediateAsyncCallback = helper;
+            mImmediateAsyncCallbackReason = reason;
+        }
+
+        @Override
+        public void requestDelayedTimeQueryCallback(
+                TimeDetectorNetworkTimeHelper instance, long delayMillis) {
+            mScheduledAsyncCallback = instance;
+            mScheduledAsyncRunnableTimeMillis = mCurrentElapsedRealtimeMillis + delayMillis;
+        }
+
+        @Override
+        public void clearDelayedTimeQueryCallback() {
+            mScheduledAsyncCallback = null;
+            mScheduledAsyncRunnableTimeMillis = -1;
+        }
+
+        void pokeLatestNetworkTime(NetworkTimeSuggestion networkTime) {
+            mLatestNetworkTime = networkTime;
+        }
+
+        void pokeElapsedRealtimeMillis(long currentElapsedRealtimeMillis) {
+            mCurrentElapsedRealtimeMillis = currentElapsedRealtimeMillis;
+        }
+
+        void simulateLatestNetworkTimeChange(NetworkTimeSuggestion networkTime) {
+            mLatestNetworkTime = networkTime;
+            mNetworkTimeUpdateListener.onChange();
+        }
+
+        void simulateTimeAdvancing(long durationMillis) {
+            mCurrentElapsedRealtimeMillis += durationMillis;
+
+            if (mImmediateAsyncCallback != null) {
+                TimeDetectorNetworkTimeHelper helper = mImmediateAsyncCallback;
+                String reason = mImmediateAsyncCallbackReason;
+                mImmediateAsyncCallback = null;
+                mImmediateAsyncCallbackReason = null;
+                helper.queryAndInjectNetworkTime(reason);
+            }
+
+            if (mScheduledAsyncCallback != null
+                    && mCurrentElapsedRealtimeMillis >= mScheduledAsyncRunnableTimeMillis) {
+                TimeDetectorNetworkTimeHelper helper = mScheduledAsyncCallback;
+                mScheduledAsyncCallback = null;
+                mScheduledAsyncRunnableTimeMillis = -1;
+                helper.delayedQueryAndInjectNetworkTime();
+            }
+        }
+
+        void assertHasNetworkTimeChangeListener() {
+            assertNotNull(mNetworkTimeUpdateListener);
+        }
+
+        void assertHasImmediateCallback() {
+            assertNotNull(mImmediateAsyncCallback);
+        }
+
+        void assertHasNoImmediateCallback() {
+            assertNull(mImmediateAsyncCallback);
+        }
+
+        void assertHasScheduledAsyncCallback(long expectedScheduledAsyncRunnableTimeMillis) {
+            assertNotNull(mScheduledAsyncCallback);
+            assertEquals(expectedScheduledAsyncRunnableTimeMillis,
+                    mScheduledAsyncRunnableTimeMillis);
+        }
+
+        void assertHasNoScheduledAsyncCallback() {
+            assertNull(mScheduledAsyncCallback);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java b/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java
index 704b06b..87aa272 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java
@@ -24,6 +24,8 @@
 import android.app.timedetector.TelephonyTimeSuggestion;
 import android.util.IndentingPrintWriter;
 
+import com.android.server.timezonedetector.StateChangeListener;
+
 /**
  * A fake implementation of {@link com.android.server.timedetector.TimeDetectorStrategy} for use
  * in tests.
@@ -31,6 +33,7 @@
 public class FakeTimeDetectorStrategy implements TimeDetectorStrategy {
     // State
     private TimeState mTimeState;
+    private NetworkTimeSuggestion mLatestNetworkTimeSuggestion;
 
     @Override
     public TimeState getTimeState() {
@@ -62,8 +65,12 @@
     }
 
     @Override
+    public void addNetworkTimeUpdateListener(StateChangeListener networkSuggestionUpdateListener) {
+    }
+
+    @Override
     public NetworkTimeSuggestion getLatestNetworkSuggestion() {
-        return null;
+        return mLatestNetworkTimeSuggestion;
     }
 
     @Override
@@ -81,4 +88,8 @@
     @Override
     public void dump(IndentingPrintWriter pw, String[] args) {
     }
+
+    void setLatestNetworkTime(NetworkTimeSuggestion networkTimeSuggestion) {
+        mLatestNetworkTimeSuggestion = networkTimeSuggestion;
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
index 0b339ad..5a0867f 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
@@ -54,6 +54,7 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.location.gnss.TimeDetectorNetworkTimeHelper;
 import com.android.server.timezonedetector.TestCallerIdentityInjector;
 import com.android.server.timezonedetector.TestHandler;
 
@@ -420,21 +421,35 @@
 
     @Test
     public void testGetLatestNetworkSuggestion() {
-        NtpTrustedTime.TimeResult latestNetworkTime = new NtpTrustedTime.TimeResult(
-                1234L, 54321L, 999, InetSocketAddress.createUnresolved("test.timeserver", 123));
-        when(mMockNtpTrustedTime.getCachedTimeResult())
-                .thenReturn(latestNetworkTime);
-        UnixEpochTime expectedUnixEpochTime = new UnixEpochTime(
-                latestNetworkTime.getElapsedRealtimeMillis(), latestNetworkTime.getTimeMillis());
-        NetworkTimeSuggestion expected = new NetworkTimeSuggestion(
-                expectedUnixEpochTime, latestNetworkTime.getUncertaintyMillis());
-        assertEquals(expected, mTimeDetectorService.getLatestNetworkSuggestion());
+        if (TimeDetectorNetworkTimeHelper.isInUse()) {
+            NetworkTimeSuggestion latestNetworkTime = createNetworkTimeSuggestion();
+            mFakeTimeDetectorStrategySpy.setLatestNetworkTime(latestNetworkTime);
+
+            assertEquals(latestNetworkTime, mTimeDetectorService.getLatestNetworkSuggestion());
+        } else {
+            NtpTrustedTime.TimeResult latestNetworkTime = new NtpTrustedTime.TimeResult(
+                    1234L, 54321L, 999, InetSocketAddress.createUnresolved("test.timeserver", 123));
+            when(mMockNtpTrustedTime.getCachedTimeResult())
+                    .thenReturn(latestNetworkTime);
+            UnixEpochTime expectedUnixEpochTime = new UnixEpochTime(
+                    latestNetworkTime.getElapsedRealtimeMillis(),
+                    latestNetworkTime.getTimeMillis());
+            NetworkTimeSuggestion expected = new NetworkTimeSuggestion(
+                    expectedUnixEpochTime, latestNetworkTime.getUncertaintyMillis());
+            assertEquals(expected, mTimeDetectorService.getLatestNetworkSuggestion());
+        }
     }
 
     @Test
     public void testGetLatestNetworkSuggestion_noTimeAvailable() {
-        when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null);
-        assertNull(mTimeDetectorService.getLatestNetworkSuggestion());
+        if (TimeDetectorNetworkTimeHelper.isInUse()) {
+            mFakeTimeDetectorStrategySpy.setLatestNetworkTime(null);
+
+            assertNull(mTimeDetectorService.getLatestNetworkSuggestion());
+        } else {
+            when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null);
+            assertNull(mTimeDetectorService.getLatestNetworkSuggestion());
+        }
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
index 37da2a2..4df21e0 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
@@ -41,6 +41,7 @@
 import com.android.server.SystemClockTime.TimeConfidence;
 import com.android.server.timedetector.TimeDetectorStrategy.Origin;
 import com.android.server.timezonedetector.StateChangeListener;
+import com.android.server.timezonedetector.TestStateChangeListener;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -51,6 +52,8 @@
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 
 import junitparams.JUnitParamsRunner;
@@ -863,8 +866,11 @@
                 new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED)
                         .setOriginPriorities(ORIGIN_NETWORK)
                         .build();
-        Script script = new Script().simulateConfigurationInternalChange(configInternal)
-                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
+        TestStateChangeListener networkTimeUpdateListener = new TestStateChangeListener();
+        Script script = new Script()
+                .simulateConfigurationInternalChange(configInternal)
+                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
+                .addNetworkTimeUpdateListener(networkTimeUpdateListener);
 
         NetworkTimeSuggestion timeSuggestion =
                 script.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME);
@@ -877,6 +883,11 @@
                 .assertLatestNetworkSuggestion(timeSuggestion)
                 .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
                 .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
+
+        // Confirm the network time update listener is notified of a change.
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
+        script.simulateAsyncRunnableExecution();
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(1);
     }
 
     @Test
@@ -885,7 +896,10 @@
                 new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED)
                         .setOriginPriorities(ORIGIN_NETWORK)
                         .build();
-        Script script = new Script().simulateConfigurationInternalChange(configInternal);
+        TestStateChangeListener networkTimeUpdateListener = new TestStateChangeListener();
+        Script script = new Script()
+                .simulateConfigurationInternalChange(configInternal)
+                .addNetworkTimeUpdateListener(networkTimeUpdateListener);
 
         NetworkTimeSuggestion timeSuggestion =
                 script.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME);
@@ -894,6 +908,11 @@
                 .simulateNetworkTimeSuggestion(timeSuggestion)
                 .assertLatestNetworkSuggestion(timeSuggestion)
                 .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Confirm the network time update listener is notified of a change.
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
+        script.simulateAsyncRunnableExecution();
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(1);
     }
 
     @Test
@@ -902,7 +921,11 @@
                 new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED)
                         .setOriginPriorities(ORIGIN_NETWORK, ORIGIN_EXTERNAL)
                         .build();
-        Script script = new Script().simulateConfigurationInternalChange(configInternal);
+
+        TestStateChangeListener networkTimeUpdateListener = new TestStateChangeListener();
+        Script script = new Script()
+                .simulateConfigurationInternalChange(configInternal)
+                .addNetworkTimeUpdateListener(networkTimeUpdateListener);
 
         // Create two different time suggestions for the current elapsedRealtimeMillis.
         ExternalTimeSuggestion externalTimeSuggestion =
@@ -927,6 +950,11 @@
             script.simulateNetworkTimeSuggestion(networkTimeSuggestion)
                     .assertLatestNetworkSuggestion(networkTimeSuggestion)
                     .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
+
+            // Confirm the network time update listener is notified of a change.
+            networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
+            script.simulateAsyncRunnableExecution();
+            networkTimeUpdateListener.assertNotificationsReceivedAndReset(1);
         }
 
         // Clear the network time. This should cause the device to change back to the external time,
@@ -937,6 +965,11 @@
             script.simulateClearLatestNetworkSuggestion()
                     .assertLatestNetworkSuggestion(null)
                     .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
+
+            // Confirm network time update listeners are asynchronously notified of a change.
+            networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
+            script.simulateAsyncRunnableExecution();
+            networkTimeUpdateListener.assertNotificationsReceivedAndReset(1);
         }
     }
 
@@ -947,8 +980,11 @@
                         .setOriginPriorities(ORIGIN_NETWORK)
                         .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
                         .build();
-        Script script = new Script().simulateConfigurationInternalChange(configInternal)
-                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
+        TestStateChangeListener networkTimeUpdateListener = new TestStateChangeListener();
+        Script script = new Script()
+                .simulateConfigurationInternalChange(configInternal)
+                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
+                .addNetworkTimeUpdateListener(networkTimeUpdateListener);
 
         Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1);
         NetworkTimeSuggestion timeSuggestion =
@@ -957,6 +993,11 @@
                 .assertLatestNetworkSuggestion(null)
                 .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
                 .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Confirm the network time update listener is not notified of a change.
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
+        script.simulateAsyncRunnableExecution();
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
     }
 
     @Test
@@ -966,8 +1007,10 @@
                         .setOriginPriorities(ORIGIN_NETWORK)
                         .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
                         .build();
+        TestStateChangeListener networkTimeUpdateListener = new TestStateChangeListener();
         Script script = new Script().simulateConfigurationInternalChange(configInternal)
-                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
+                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
+                .addNetworkTimeUpdateListener(networkTimeUpdateListener);
 
         Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1);
         NetworkTimeSuggestion timeSuggestion =
@@ -976,6 +1019,11 @@
                 .assertLatestNetworkSuggestion(timeSuggestion)
                 .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
                 .verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli());
+
+        // Confirm the network time update listener is notified of a change.
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
+        script.simulateAsyncRunnableExecution();
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(1);
     }
 
     @Test
@@ -985,8 +1033,10 @@
                         .setOriginPriorities(ORIGIN_NETWORK)
                         .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
                         .build();
+        TestStateChangeListener networkTimeUpdateListener = new TestStateChangeListener();
         Script script = new Script().simulateConfigurationInternalChange(configInternal)
-                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
+                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
+                .addNetworkTimeUpdateListener(networkTimeUpdateListener);
 
         Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1);
         NetworkTimeSuggestion timeSuggestion =
@@ -995,6 +1045,11 @@
                 .assertLatestNetworkSuggestion(null)
                 .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
                 .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Confirm the network time update listener is not notified of a change.
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
+        script.simulateAsyncRunnableExecution();
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
     }
 
     @Test
@@ -1004,8 +1059,10 @@
                         .setOriginPriorities(ORIGIN_NETWORK)
                         .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
                         .build();
+        TestStateChangeListener networkTimeUpdateListener = new TestStateChangeListener();
         Script script = new Script().simulateConfigurationInternalChange(configInternal)
-                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
+                .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
+                .addNetworkTimeUpdateListener(networkTimeUpdateListener);
 
         Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1);
         NetworkTimeSuggestion timeSuggestion =
@@ -1014,6 +1071,11 @@
                 .assertLatestNetworkSuggestion(timeSuggestion)
                 .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
                 .verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli());
+
+        // Confirm the network time update listener is notified of a change.
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
+        script.simulateAsyncRunnableExecution();
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(1);
     }
 
     @Test
@@ -1800,7 +1862,10 @@
                 new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED)
                         .setOriginPriorities(ORIGIN_TELEPHONY)
                         .build();
-        Script script = new Script().simulateConfigurationInternalChange(configInternal);
+        TestStateChangeListener networkTimeUpdateListener = new TestStateChangeListener();
+        Script script = new Script()
+                .simulateConfigurationInternalChange(configInternal)
+                .addNetworkTimeUpdateListener(networkTimeUpdateListener);
 
         NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(
                 ARBITRARY_TEST_TIME);
@@ -1809,6 +1874,11 @@
                 .assertLatestNetworkSuggestion(timeSuggestion)
                 .assertLatestNetworkSuggestion(timeSuggestion)
                 .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Confirm the network time update listener is notified of a change.
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(0);
+        script.simulateAsyncRunnableExecution();
+        networkTimeUpdateListener.assertNotificationsReceivedAndReset(1);
     }
 
     @Test
@@ -1867,6 +1937,8 @@
      */
     private static class FakeEnvironment implements TimeDetectorStrategyImpl.Environment {
 
+        private final List<Runnable> mAsyncRunnables = new ArrayList<>();
+
         private ConfigurationInternal mConfigurationInternal;
         private boolean mWakeLockAcquired;
         private long mElapsedRealtimeMillis;
@@ -1951,8 +2023,20 @@
             // No-op for tests
         }
 
+        @Override
+        public void runAsync(Runnable runnable) {
+            mAsyncRunnables.add(runnable);
+        }
+
         // Methods below are for managing the fake's behavior.
 
+        void runAsyncRunnables() {
+            for (Runnable runnable : mAsyncRunnables) {
+                runnable.run();
+            }
+            mAsyncRunnables.clear();
+        }
+
         void simulateConfigurationInternalChange(ConfigurationInternal configurationInternal) {
             mConfigurationInternal = configurationInternal;
             mConfigurationInternalChangeListener.onChange();
@@ -1992,7 +2076,7 @@
             assertEquals(expectedSystemClockMillis, mSystemClockMillis);
         }
 
-        public void verifySystemClockConfidenceLatest(@TimeConfidence int expectedConfidence) {
+        void verifySystemClockConfidenceLatest(@TimeConfidence int expectedConfidence) {
             assertEquals(expectedConfidence, mSystemClockConfidence);
         }
 
@@ -2118,6 +2202,17 @@
             return this;
         }
 
+        /** Calls {@link TimeDetectorStrategy#addNetworkTimeUpdateListener(StateChangeListener)}. */
+        Script addNetworkTimeUpdateListener(StateChangeListener listener) {
+            mTimeDetectorStrategy.addNetworkTimeUpdateListener(listener);
+            return this;
+        }
+
+        Script simulateAsyncRunnableExecution() {
+            mFakeEnvironment.runAsyncRunnables();
+            return this;
+        }
+
         Script verifySystemClockWasNotSetAndResetCallTracking() {
             mFakeEnvironment.verifySystemClockNotSet();
             mFakeEnvironment.resetCallTracking();
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TestStateChangeListener.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TestStateChangeListener.java
new file mode 100644
index 0000000..9cbf0a3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TestStateChangeListener.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 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.timezonedetector;
+
+import static org.junit.Assert.assertEquals;
+
+public class TestStateChangeListener implements StateChangeListener {
+
+    private int mNotificationsReceived;
+
+    @Override
+    public void onChange() {
+        mNotificationsReceived++;
+    }
+
+    public void assertNotificationsReceivedAndReset(int expectedCount) {
+        assertNotificationsReceived(expectedCount);
+        resetNotificationsReceivedCount();
+    }
+
+    private void resetNotificationsReceivedCount() {
+        mNotificationsReceived = 0;
+    }
+
+    private void assertNotificationsReceived(int expectedCount) {
+        assertEquals(expectedCount, mNotificationsReceived);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
index 590aba9..03d406f 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
@@ -2001,26 +2001,4 @@
         return new TelephonyTestCase(matchType, quality, expectedScore);
     }
 
-    private static class TestStateChangeListener implements StateChangeListener {
-
-        private int mNotificationsReceived;
-
-        @Override
-        public void onChange() {
-            mNotificationsReceived++;
-        }
-
-        public void assertNotificationsReceivedAndReset(int expectedCount) {
-            assertNotificationsReceived(expectedCount);
-            resetNotificationsReceivedCount();
-        }
-
-        private void resetNotificationsReceivedCount() {
-            mNotificationsReceived = 0;
-        }
-
-        private void assertNotificationsReceived(int expectedCount) {
-            assertEquals(expectedCount, mNotificationsReceived);
-        }
-    }
 }