From bd64b8d54323772d050270420e33acc424e6c805 Mon Sep 17 00:00:00 2001 From: Neil Fuller Date: Tue, 13 Dec 2022 15:05:38 +0000 Subject: [PATCH] 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 --- .../location/gnss/NetworkTimeHelper.java | 21 +- .../gnss/TimeDetectorNetworkTimeHelper.java | 339 +++++++++++++++ .../server/timedetector/EnvironmentImpl.java | 5 + .../timedetector/TimeDetectorInternal.java | 20 +- .../TimeDetectorInternalImpl.java | 14 + .../timedetector/TimeDetectorService.java | 19 +- .../timedetector/TimeDetectorStrategy.java | 11 +- .../TimeDetectorStrategyImpl.java | 26 ++ .../java/com/android/server/SystemServer.java | 16 +- .../TimeDetectorNetworkTimeHelperTest.java | 399 ++++++++++++++++++ .../FakeTimeDetectorStrategy.java | 13 +- .../timedetector/TimeDetectorServiceTest.java | 37 +- .../TimeDetectorStrategyImplTest.java | 117 ++++- .../TestStateChangeListener.java | 42 ++ .../TimeZoneDetectorStrategyImplTest.java | 22 - 15 files changed, 1038 insertions(+), 63 deletions(-) create mode 100644 services/core/java/com/android/server/location/gnss/TimeDetectorNetworkTimeHelper.java create mode 100644 services/robotests/src/com/android/server/location/gnss/TimeDetectorNetworkTimeHelperTest.java create mode 100644 services/tests/servicestests/src/com/android/server/timezonedetector/TestStateChangeListener.java 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 3a2514604600f..f5114b7aab913 100644 --- a/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java +++ b/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java @@ -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,7 +55,13 @@ abstract class NetworkTimeHelper { 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 @@ abstract class NetworkTimeHelper { * Notifies that network connectivity has been established. * *

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); + } 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 0000000000000..15366d35fe41a --- /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 5801920864b48..fc960d83dc3bf 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 @@ 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); + } } diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorInternal.java b/services/core/java/com/android/server/timedetector/TimeDetectorInternal.java index 5df5cbc3535b4..4b65c554bf2ed 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,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 diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorInternalImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorInternalImpl.java index af168f81b119e..7e96a4358a284 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.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); diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java index a9dcff49d03c3..0da967a3bc006 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.annotations.VisibleForTesting; 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 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub // 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 dbd7172530284..11cec6663a376 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 android.util.IndentingPrintWriter; 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,10 +105,18 @@ public interface TimeDetectorStrategy extends Dumpable { /** * 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 d679bbee611f1..b293bacfdc0d6 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.ManualTimeSuggestion; 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 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { private final ReferenceWithHistory mLastExternalSuggestion = new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); + @GuardedBy("this") + private final ArraySet 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 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { * 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 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { 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 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { 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 final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { 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 5c5442d0b9c32..8de01ac0ba5cc 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2086,6 +2086,14 @@ public final class SystemServer implements Dumpable { 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(); @@ -2099,14 +2107,6 @@ public final class SystemServer implements Dumpable { } 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 0000000000000..3e2e46c520c00 --- /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 704b06b7e7fd2..87aa2725491bb 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.ManualTimeSuggestion; 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 @@ import android.util.IndentingPrintWriter; public class FakeTimeDetectorStrategy implements TimeDetectorStrategy { // State private TimeState mTimeState; + private NetworkTimeSuggestion mLatestNetworkTimeSuggestion; @Override public TimeState getTimeState() { @@ -61,9 +64,13 @@ public class FakeTimeDetectorStrategy implements TimeDetectorStrategy { public void suggestNetworkTime(NetworkTimeSuggestion suggestion) { } + @Override + public void addNetworkTimeUpdateListener(StateChangeListener networkSuggestionUpdateListener) { + } + @Override public NetworkTimeSuggestion getLatestNetworkSuggestion() { - return null; + return mLatestNetworkTimeSuggestion; } @Override @@ -81,4 +88,8 @@ public class FakeTimeDetectorStrategy implements TimeDetectorStrategy { @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 0b339ad52eda3..5a0867f8f584b 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 android.util.NtpTrustedTime; 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 @@ public class TimeDetectorServiceTest { @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 37da2a28a892f..4df21e00d61cd 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 android.os.TimestampedValue; 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.Duration; 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 @@ public class TimeDetectorStrategyImplTest { 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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { 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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { 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 @@ public class TimeDetectorStrategyImplTest { 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 @@ public class TimeDetectorStrategyImplTest { 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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { 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 @@ public class TimeDetectorStrategyImplTest { .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 @@ public class TimeDetectorStrategyImplTest { */ private static class FakeEnvironment implements TimeDetectorStrategyImpl.Environment { + private final List mAsyncRunnables = new ArrayList<>(); + private ConfigurationInternal mConfigurationInternal; private boolean mWakeLockAcquired; private long mElapsedRealtimeMillis; @@ -1951,8 +2023,20 @@ public class TimeDetectorStrategyImplTest { // 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 @@ public class TimeDetectorStrategyImplTest { assertEquals(expectedSystemClockMillis, mSystemClockMillis); } - public void verifySystemClockConfidenceLatest(@TimeConfidence int expectedConfidence) { + void verifySystemClockConfidenceLatest(@TimeConfidence int expectedConfidence) { assertEquals(expectedConfidence, mSystemClockConfidence); } @@ -2118,6 +2202,17 @@ public class TimeDetectorStrategyImplTest { 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 0000000000000..9cbf0a34f8fe5 --- /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 590aba9bc536e..03d406f946964 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 @@ public class TimeZoneDetectorStrategyImplTest { 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); - } - } }