diff options
15 files changed, 1038 insertions, 63 deletions
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 3a2514604600..f5114b7aab91 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 @@ 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 * the looper passed to {@link #create(Context, Looper, InjectTimeCallback)}. @@ -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. * * <p>Called by {@link GnssLocationProvider} when the device establishes a data network - * connection. + * connection. This call should be removed eventually because it should be handled by the {@link + * NetworkTimeHelper} implementation itself, but has been retained for compatibility while + * switching implementations. */ abstract void onNetworkAvailable(); @@ -82,4 +98,5 @@ abstract class NetworkTimeHelper { * Dumps internal state during bugreports useful for debugging. */ abstract void dump(@NonNull PrintWriter pw); + } 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 000000000000..15366d35fe41 --- /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 5801920864b4..fc960d83dc3b 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 5df5cbc3535b..4b65c554bf2e 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 @@ 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 * 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 af168f81b119..7e96a4358a28 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; @@ -87,6 +88,19 @@ public class TimeDetectorInternalImpl implements TimeDetectorInternal { } @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 a9dcff49d03c..0da967a3bc00 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 dbd717253028..11cec6663a37 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,11 +105,19 @@ 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 d679bbee611f..b293bacfdc0d 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<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 @@ 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 b117cae5c97b..850b5b686efe 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2095,6 +2095,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(); @@ -2108,14 +2116,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 000000000000..3e2e46c520c0 --- /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 704b06b7e7fd..87aa2725491b 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() { @@ -62,8 +65,12 @@ public class FakeTimeDetectorStrategy implements TimeDetectorStrategy { } @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 0b339ad52eda..5a0867f8f584 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 37da2a28a892..4df21e00d61c 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<Runnable> 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 000000000000..9cbf0a34f8fe --- /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 590aba9bc536..03d406f94696 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); - } - } } |