Add more behavior to TimeDetectorService

The client code now "suggests" time updates
to the time detection service. The current implementation
of the time detection service will validate and set the
device system clock as needed. In future it will ignore
these suggestions if better information is available.

Responsibility for sending the
TelephonyIntents.ACTION_NETWORK_SET_TIME intent has
been moved to the time detection service until it can be
removed or replaced.

The telephony code is still responsible for basic rate
limiting but the majority of the decision logic has been
moved to the service.

There is an associated change in telephony code to switch to
using the server.

Bug: 78217059
Test: atest FrameworksServicesTests:com.android.server.timedetector
Test: atest FrameworksCoreTests:android.util.TimestampedValueTest
Change-Id: I4f7a10ac06b2d32da22689e1ddf309e0a2795f30
diff --git a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
index e5207cb..7bdc8a3 100644
--- a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
+++ b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
@@ -20,38 +20,213 @@
 import android.annotation.Nullable;
 import android.app.AlarmManager;
 import android.app.timedetector.TimeSignal;
+import android.content.Intent;
 import android.util.Slog;
+import android.util.TimestampedValue;
 
-import java.io.FileDescriptor;
+import com.android.internal.telephony.TelephonyIntents;
+
 import java.io.PrintWriter;
 
 /**
- * A placeholder implementation of TimeDetectorStrategy that passes NITZ suggestions immediately
- * to {@link AlarmManager}.
+ * An implementation of TimeDetectorStrategy that passes only NITZ suggestions to
+ * {@link AlarmManager}. The TimeDetectorService handles thread safety: all calls to
+ * this class can be assumed to be single threaded (though the thread used may vary).
  */
+// @NotThreadSafe
 public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
 
     private final static String TAG = "timedetector.SimpleTimeDetectorStrategy";
 
-    private Callback mHelper;
+    /**
+     * CLOCK_PARANOIA: The maximum difference allowed between the expected system clock time and the
+     * actual system clock time before a warning is logged. Used to help identify situations where
+     * there is something other than this class setting the system clock.
+     */
+    private static final long SYSTEM_CLOCK_PARANOIA_THRESHOLD_MILLIS = 2 * 1000;
+
+    // @NonNull after initialize()
+    private Callback mCallback;
+
+    // NITZ state.
+    @Nullable private TimestampedValue<Long> mLastNitzTime;
+
+
+    // Information about the last time signal received: Used when toggling auto-time.
+    @Nullable private TimestampedValue<Long> mLastSystemClockTime;
+    private boolean mLastSystemClockTimeSendNetworkBroadcast;
+
+    // System clock state.
+    @Nullable private TimestampedValue<Long> mLastSystemClockTimeSet;
 
     @Override
     public void initialize(@NonNull Callback callback) {
-        mHelper = callback;
+        mCallback = callback;
     }
 
     @Override
     public void suggestTime(@NonNull TimeSignal timeSignal) {
         if (!TimeSignal.SOURCE_ID_NITZ.equals(timeSignal.getSourceId())) {
-            Slog.w(TAG, "Ignoring signal from unknown source: " + timeSignal);
+            Slog.w(TAG, "Ignoring signal from unsupported source: " + timeSignal);
             return;
         }
 
-        mHelper.setTime(timeSignal.getUtcTime());
+        // NITZ logic
+
+        TimestampedValue<Long> newNitzUtcTime = timeSignal.getUtcTime();
+        boolean nitzTimeIsValid = validateNewNitzTime(newNitzUtcTime, mLastNitzTime);
+        if (!nitzTimeIsValid) {
+            return;
+        }
+        // Always store the last NITZ value received, regardless of whether we go on to use it to
+        // update the system clock. This is so that we can validate future NITZ signals.
+        mLastNitzTime = newNitzUtcTime;
+
+        // System clock update logic.
+
+        // Historically, Android has sent a telephony broadcast only when setting the time using
+        // NITZ.
+        final boolean sendNetworkBroadcast =
+                TimeSignal.SOURCE_ID_NITZ.equals(timeSignal.getSourceId());
+
+        final TimestampedValue<Long> newUtcTime = newNitzUtcTime;
+        setSystemClockIfRequired(newUtcTime, sendNetworkBroadcast);
+    }
+
+    private static boolean validateNewNitzTime(TimestampedValue<Long> newNitzUtcTime,
+            TimestampedValue<Long> lastNitzTime) {
+
+        if (lastNitzTime != null) {
+            long referenceTimeDifference =
+                    TimestampedValue.referenceTimeDifference(newNitzUtcTime, lastNitzTime);
+            if (referenceTimeDifference < 0 || referenceTimeDifference > Integer.MAX_VALUE) {
+                // Out of order or bogus.
+                Slog.w(TAG, "validateNewNitzTime: Bad NITZ signal received."
+                        + " referenceTimeDifference=" + referenceTimeDifference
+                        + " lastNitzTime=" + lastNitzTime
+                        + " newNitzUtcTime=" + newNitzUtcTime);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void setSystemClockIfRequired(
+            TimestampedValue<Long> time, boolean sendNetworkBroadcast) {
+
+        // Store the last candidate we've seen in all cases so we can set the system clock
+        // when/if time detection is enabled.
+        mLastSystemClockTime = time;
+        mLastSystemClockTimeSendNetworkBroadcast = sendNetworkBroadcast;
+
+        if (!mCallback.isTimeDetectionEnabled()) {
+            Slog.d(TAG, "setSystemClockIfRequired: Time detection is not enabled. time=" + time);
+            return;
+        }
+
+        mCallback.acquireWakeLock();
+        try {
+            long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis();
+            long actualTimeMillis = mCallback.systemClockMillis();
+
+            // CLOCK_PARANOIA : Check to see if this class owns the clock or if something else
+            // may be setting the clock.
+            if (mLastSystemClockTimeSet != null) {
+                long expectedTimeMillis = TimeDetectorStrategy.getTimeAt(
+                        mLastSystemClockTimeSet, elapsedRealtimeMillis);
+                long absSystemClockDifference = Math.abs(expectedTimeMillis - actualTimeMillis);
+                if (absSystemClockDifference > SYSTEM_CLOCK_PARANOIA_THRESHOLD_MILLIS) {
+                    Slog.w(TAG, "System clock has not tracked elapsed real time clock. A clock may"
+                            + " be inaccurate or something unexpectedly set the system clock."
+                            + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+                            + " expectedTimeMillis=" + expectedTimeMillis
+                            + " actualTimeMillis=" + actualTimeMillis);
+                }
+            }
+
+            final String reason = "New time signal";
+            adjustAndSetDeviceSystemClock(
+                    time, sendNetworkBroadcast, elapsedRealtimeMillis, actualTimeMillis, reason);
+        } finally {
+            mCallback.releaseWakeLock();
+        }
     }
 
     @Override
-    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @Nullable String[] args) {
-        // No state to dump.
+    public void handleAutoTimeDetectionToggle(boolean enabled) {
+        // If automatic time detection is enabled we update the system clock instantly if we can.
+        // Conversely, if automatic time detection is disabled we leave the clock as it is.
+        if (enabled) {
+            if (mLastSystemClockTime != null) {
+                // Only send the network broadcast if the last candidate would have caused one.
+                final boolean sendNetworkBroadcast = mLastSystemClockTimeSendNetworkBroadcast;
+
+                mCallback.acquireWakeLock();
+                try {
+                    long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis();
+                    long actualTimeMillis = mCallback.systemClockMillis();
+
+                    final String reason = "Automatic time detection enabled.";
+                    adjustAndSetDeviceSystemClock(mLastSystemClockTime, sendNetworkBroadcast,
+                            elapsedRealtimeMillis, actualTimeMillis, reason);
+                } finally {
+                    mCallback.releaseWakeLock();
+                }
+            }
+        } else {
+            // CLOCK_PARANOIA: We are losing "control" of the system clock so we cannot predict what
+            // it should be in future.
+            mLastSystemClockTimeSet = null;
+        }
+    }
+
+    @Override
+    public void dump(@NonNull PrintWriter pw, @Nullable String[] args) {
+        pw.println("mLastNitzTime=" + mLastNitzTime);
+        pw.println("mLastSystemClockTimeSet=" + mLastSystemClockTimeSet);
+        pw.println("mLastSystemClockTime=" + mLastSystemClockTime);
+        pw.println("mLastSystemClockTimeSendNetworkBroadcast="
+                + mLastSystemClockTimeSendNetworkBroadcast);
+    }
+
+    private void adjustAndSetDeviceSystemClock(
+            TimestampedValue<Long> newTime, boolean sendNetworkBroadcast,
+            long elapsedRealtimeMillis, long actualSystemClockMillis, String reason) {
+
+        // Adjust for the time that has elapsed since the signal was received.
+        long newSystemClockMillis = TimeDetectorStrategy.getTimeAt(newTime, elapsedRealtimeMillis);
+
+        // Check if the new signal would make sufficient difference to the system clock. If it's
+        // below the threshold then ignore it.
+        long absTimeDifference = Math.abs(newSystemClockMillis - actualSystemClockMillis);
+        long systemClockUpdateThreshold = mCallback.systemClockUpdateThresholdMillis();
+        if (absTimeDifference < systemClockUpdateThreshold) {
+            Slog.d(TAG, "adjustAndSetDeviceSystemClock: Not setting system clock. New time and"
+                    + " system clock are close enough."
+                    + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+                    + " newTime=" + newTime
+                    + " reason=" + reason
+                    + " systemClockUpdateThreshold=" + systemClockUpdateThreshold
+                    + " absTimeDifference=" + absTimeDifference);
+            return;
+        }
+
+        Slog.d(TAG, "Setting system clock using time=" + newTime
+                + " reason=" + reason
+                + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+                + " newTimeMillis=" + newSystemClockMillis);
+        mCallback.setSystemClock(newSystemClockMillis);
+
+        // CLOCK_PARANOIA : Record the last time this class set the system clock.
+        mLastSystemClockTimeSet = newTime;
+
+        if (sendNetworkBroadcast) {
+            // Send a broadcast that telephony code used to send after setting the clock.
+            // TODO Remove this broadcast as soon as there are no remaining listeners.
+            Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME);
+            intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+            intent.putExtra("time", newSystemClockMillis);
+            mCallback.sendStickyBroadcast(intent);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java
index 0ec24d8..9c83000 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorService.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorService.java
@@ -20,24 +20,29 @@
 import android.annotation.Nullable;
 import android.app.timedetector.ITimeDetectorService;
 import android.app.timedetector.TimeSignal;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.database.ContentObserver;
 import android.os.Binder;
+import android.provider.Settings;
 
+import com.android.internal.annotations.GuardedBy;
 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.timedetector.TimeDetectorStrategy.Callback;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Objects;
 
 public final class TimeDetectorService extends ITimeDetectorService.Stub {
-
     private static final String TAG = "timedetector.TimeDetectorService";
 
     public static class Lifecycle extends SystemService {
 
-        public Lifecycle(Context context) {
+        public Lifecycle(@NonNull Context context) {
             super(context);
         }
 
@@ -51,31 +56,65 @@
         }
     }
 
-    private final Context mContext;
-    private final TimeDetectorStrategy mTimeDetectorStrategy;
+    @NonNull private final Context mContext;
+    @NonNull private final Callback mCallback;
 
-    private static TimeDetectorService create(Context context) {
-        TimeDetectorStrategy timeDetector = new SimpleTimeDetectorStrategy();
-        timeDetector.initialize(new TimeDetectorStrategyCallbackImpl(context));
-        return new TimeDetectorService(context, timeDetector);
+    // The lock used when call the strategy to ensure thread safety.
+    @NonNull private final Object mStrategyLock = new Object();
+
+    @GuardedBy("mStrategyLock")
+    @NonNull private final TimeDetectorStrategy mTimeDetectorStrategy;
+
+    private static TimeDetectorService create(@NonNull Context context) {
+        final TimeDetectorStrategy timeDetector = new SimpleTimeDetectorStrategy();
+        final TimeDetectorStrategyCallbackImpl callback =
+                new TimeDetectorStrategyCallbackImpl(context);
+        timeDetector.initialize(callback);
+
+        TimeDetectorService timeDetectorService =
+                new TimeDetectorService(context, callback, timeDetector);
+
+        // Wire up event listening.
+        ContentResolver contentResolver = context.getContentResolver();
+        contentResolver.registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
+                new ContentObserver(FgThread.getHandler()) {
+                    public void onChange(boolean selfChange) {
+                        timeDetectorService.handleAutoTimeDetectionToggle();
+                    }
+                });
+
+        return timeDetectorService;
     }
 
     @VisibleForTesting
-    public TimeDetectorService(@NonNull Context context,
+    public TimeDetectorService(@NonNull Context context, @NonNull Callback callback,
             @NonNull TimeDetectorStrategy timeDetectorStrategy) {
         mContext = Objects.requireNonNull(context);
+        mCallback = Objects.requireNonNull(callback);
         mTimeDetectorStrategy = Objects.requireNonNull(timeDetectorStrategy);
     }
 
     @Override
     public void suggestTime(@NonNull TimeSignal timeSignal) {
         enforceSetTimePermission();
+        Objects.requireNonNull(timeSignal);
 
-        long callerIdToken = Binder.clearCallingIdentity();
+        long idToken = Binder.clearCallingIdentity();
         try {
-            mTimeDetectorStrategy.suggestTime(timeSignal);
+            synchronized (mStrategyLock) {
+                mTimeDetectorStrategy.suggestTime(timeSignal);
+            }
         } finally {
-            Binder.restoreCallingIdentity(callerIdToken);
+            Binder.restoreCallingIdentity(idToken);
+        }
+    }
+
+    @VisibleForTesting
+    public void handleAutoTimeDetectionToggle() {
+        synchronized (mStrategyLock) {
+            final boolean timeDetectionEnabled = mCallback.isTimeDetectionEnabled();
+            mTimeDetectorStrategy.handleAutoTimeDetectionToggle(timeDetectionEnabled);
         }
     }
 
@@ -84,7 +123,9 @@
             @Nullable String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
-        mTimeDetectorStrategy.dump(fd, pw, args);
+        synchronized (mStrategyLock) {
+            mTimeDetectorStrategy.dump(pw, args);
+        }
     }
 
     private void enforceSetTimePermission() {
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
index 5cb2eed..e050865 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
@@ -19,26 +19,66 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.timedetector.TimeSignal;
+import android.content.Intent;
 import android.util.TimestampedValue;
 
-import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
 /**
  * The interface for classes that implement the time detection algorithm used by the
- * TimeDetectorService.
+ * TimeDetectorService. The TimeDetectorService handles thread safety: all calls to implementations
+ * of this interface can be assumed to be single threaded (though the thread used may vary).
  *
  * @hide
  */
+// @NotThreadSafe
 public interface TimeDetectorStrategy {
 
+    /**
+     * The interface used by the strategy to interact with the surrounding service.
+     */
     interface Callback {
-        void setTime(TimestampedValue<Long> time);
+
+        /**
+         * The absolute threshold below which the system clock need not be updated. i.e. if setting
+         * the system clock would adjust it by less than this (either backwards or forwards) then it
+         * need not be set.
+         */
+        int systemClockUpdateThresholdMillis();
+
+        /** Returns true if automatic time detection is enabled. */
+        boolean isTimeDetectionEnabled();
+
+        /** Acquire a suitable wake lock. Must be followed by {@link #releaseWakeLock()} */
+        void acquireWakeLock();
+
+        /** Returns the elapsedRealtimeMillis clock value. The WakeLock must be held. */
+        long elapsedRealtimeMillis();
+
+        /** Returns the system clock value. The WakeLock must be held. */
+        long systemClockMillis();
+
+        /** Sets the device system clock. The WakeLock must be held. */
+        void setSystemClock(long newTimeMillis);
+
+        /** Release the wake lock acquired by a call to {@link #acquireWakeLock()}. */
+        void releaseWakeLock();
+
+        /** Send the supplied intent as a stick broadcast. */
+        void sendStickyBroadcast(@NonNull Intent intent);
     }
 
+    /** Initialize the strategy. */
     void initialize(@NonNull Callback callback);
+
+    /** Process the suggested time. */
     void suggestTime(@NonNull TimeSignal timeSignal);
-    void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @Nullable String[] args);
+
+    /** Handle the auto-time setting being toggled on or off. */
+    void handleAutoTimeDetectionToggle(boolean enabled);
+
+    /** Dump debug information. */
+    void dump(@NonNull PrintWriter pw, @Nullable String[] args);
 
     // Utility methods below are to be moved to a better home when one becomes more obvious.
 
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyCallbackImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyCallbackImpl.java
index 568d73a..77b9e62 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyCallbackImpl.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyCallbackImpl.java
@@ -18,41 +18,108 @@
 
 import android.annotation.NonNull;
 import android.app.AlarmManager;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.os.PowerManager;
 import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
 import android.util.Slog;
-import android.util.TimestampedValue;
+
+import java.util.Objects;
 
 /**
  * The real implementation of {@link TimeDetectorStrategy.Callback} used on device.
  */
-public class TimeDetectorStrategyCallbackImpl implements TimeDetectorStrategy.Callback {
+public final class TimeDetectorStrategyCallbackImpl implements TimeDetectorStrategy.Callback {
 
     private final static String TAG = "timedetector.TimeDetectorStrategyCallbackImpl";
 
-    @NonNull private PowerManager.WakeLock mWakeLock;
-    @NonNull private AlarmManager mAlarmManager;
+    private static final int SYSTEM_CLOCK_UPDATE_THRESHOLD_MILLIS_DEFAULT = 2 * 1000;
 
-    public TimeDetectorStrategyCallbackImpl(Context context) {
+    /**
+     * If a newly calculated system clock time and the current system clock time differs by this or
+     * more the system clock will actually be updated. Used to prevent the system clock being set
+     * for only minor differences.
+     */
+    private final int mSystemClockUpdateThresholdMillis;
+
+    @NonNull private final Context mContext;
+    @NonNull private final ContentResolver mContentResolver;
+    @NonNull private final PowerManager.WakeLock mWakeLock;
+    @NonNull private final AlarmManager mAlarmManager;
+
+    public TimeDetectorStrategyCallbackImpl(@NonNull Context context) {
+        mContext = Objects.requireNonNull(context);
+        mContentResolver = Objects.requireNonNull(context.getContentResolver());
+
         PowerManager powerManager = context.getSystemService(PowerManager.class);
+        mWakeLock = Objects.requireNonNull(
+                powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG));
 
-        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+        mAlarmManager = Objects.requireNonNull(context.getSystemService(AlarmManager.class));
 
-        mAlarmManager = context.getSystemService(AlarmManager.class);
+        mSystemClockUpdateThresholdMillis =
+                SystemProperties.getInt("ro.sys.time_detector_update_diff",
+                        SYSTEM_CLOCK_UPDATE_THRESHOLD_MILLIS_DEFAULT);
     }
 
     @Override
-    public void setTime(TimestampedValue<Long> time) {
-        mWakeLock.acquire();
+    public int systemClockUpdateThresholdMillis() {
+        return mSystemClockUpdateThresholdMillis;
+    }
+
+    @Override
+    public boolean isTimeDetectionEnabled() {
         try {
-            long elapsedRealtimeMillis = SystemClock.elapsedRealtime();
-            long currentTimeMillis = TimeDetectorStrategy.getTimeAt(time, elapsedRealtimeMillis);
-            Slog.d(TAG, "Setting system clock using time=" + time
-                    + ", elapsedRealtimeMillis=" + elapsedRealtimeMillis);
-            mAlarmManager.setTime(currentTimeMillis);
-        } finally {
-            mWakeLock.release();
+            return Settings.Global.getInt(mContentResolver, Settings.Global.AUTO_TIME) != 0;
+        } catch (Settings.SettingNotFoundException snfe) {
+            return true;
+        }
+    }
+
+    @Override
+    public void acquireWakeLock() {
+        if (mWakeLock.isHeld()) {
+            Slog.wtf(TAG, "WakeLock " + mWakeLock + " already held");
+        }
+        mWakeLock.acquire();
+    }
+
+    @Override
+    public long elapsedRealtimeMillis() {
+        checkWakeLockHeld();
+        return SystemClock.elapsedRealtime();
+    }
+
+    @Override
+    public long systemClockMillis() {
+        checkWakeLockHeld();
+        return System.currentTimeMillis();
+    }
+
+    @Override
+    public void setSystemClock(long newTimeMillis) {
+        checkWakeLockHeld();
+        mAlarmManager.setTime(newTimeMillis);
+    }
+
+    @Override
+    public void releaseWakeLock() {
+        checkWakeLockHeld();
+        mWakeLock.release();
+    }
+
+    @Override
+    public void sendStickyBroadcast(@NonNull Intent intent) {
+        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private void checkWakeLockHeld() {
+        if (!mWakeLock.isHeld()) {
+            Slog.wtf(TAG, "WakeLock " + mWakeLock + " not held");
         }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java
index e4b3b13..62f1433 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java
@@ -16,12 +16,18 @@
 
 package com.android.server.timedetector;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.timedetector.TimeSignal;
+import android.content.Intent;
+import android.icu.util.Calendar;
+import android.icu.util.GregorianCalendar;
+import android.icu.util.TimeZone;
 import android.support.test.runner.AndroidJUnit4;
 import android.util.TimestampedValue;
 
@@ -32,37 +38,476 @@
 @RunWith(AndroidJUnit4.class)
 public class SimpleTimeZoneDetectorStrategyTest {
 
-    private TimeDetectorStrategy.Callback mMockCallback;
+    private static final Scenario SCENARIO_1 = new Scenario.Builder()
+            .setInitialDeviceSystemClockUtc(1977, 1, 1, 12, 0, 0)
+            .setInitialDeviceRealtimeMillis(123456789L)
+            .setActualTimeUtc(2018, 1, 1, 12, 0, 0)
+            .build();
 
-    private SimpleTimeDetectorStrategy mSimpleTimeZoneDetectorStrategy;
+    private Script mScript;
 
     @Before
     public void setUp() {
-        mMockCallback = mock(TimeDetectorStrategy.Callback.class);
-        mSimpleTimeZoneDetectorStrategy = new SimpleTimeDetectorStrategy();
-        mSimpleTimeZoneDetectorStrategy.initialize(mMockCallback);
+        mScript = new Script();
     }
 
     @Test
-    public void testSuggestTime_nitz() {
-        TimestampedValue<Long> utcTime = createUtcTime();
-        TimeSignal timeSignal = new TimeSignal(TimeSignal.SOURCE_ID_NITZ, utcTime);
+    public void testSuggestTime_nitz_timeDetectionEnabled() {
+        Scenario scenario = SCENARIO_1;
+        mScript.pokeFakeClocks(scenario)
+                .pokeTimeDetectionEnabled(true);
 
-        mSimpleTimeZoneDetectorStrategy.suggestTime(timeSignal);
+        TimeSignal timeSignal = scenario.createTimeSignalForActual(TimeSignal.SOURCE_ID_NITZ);
+        final int clockIncrement = 1000;
+        long expectSystemClockMillis = scenario.getActualTimeMillis() + clockIncrement;
 
-        verify(mMockCallback).setTime(utcTime);
+        mScript.simulateTimePassing(clockIncrement)
+                .simulateTimeSignalReceived(timeSignal)
+                .verifySystemClockWasSetAndResetCallTracking(expectSystemClockMillis);
+    }
+
+    @Test
+    public void testSuggestTime_systemClockThreshold() {
+        Scenario scenario = SCENARIO_1;
+        final int systemClockUpdateThresholdMillis = 1000;
+        mScript.pokeFakeClocks(scenario)
+                .pokeThresholds(systemClockUpdateThresholdMillis)
+                .pokeTimeDetectionEnabled(true);
+
+        TimeSignal timeSignal1 = scenario.createTimeSignalForActual(TimeSignal.SOURCE_ID_NITZ);
+        TimestampedValue<Long> utcTime1 = timeSignal1.getUtcTime();
+
+        final int clockIncrement = 100;
+        // Increment the the device clocks to simulate the passage of time.
+        mScript.simulateTimePassing(clockIncrement);
+
+        long expectSystemClockMillis1 =
+                TimeDetectorStrategy.getTimeAt(utcTime1, mScript.peekElapsedRealtimeMillis());
+
+        // Send the first time signal. It should be used.
+        mScript.simulateTimeSignalReceived(timeSignal1)
+                .verifySystemClockWasSetAndResetCallTracking(expectSystemClockMillis1);
+
+        // Now send another time signal, but one that is too similar to the last one and should be
+        // ignored.
+        int underThresholdMillis = systemClockUpdateThresholdMillis - 1;
+        TimestampedValue<Long> utcTime2 = new TimestampedValue<>(
+                mScript.peekElapsedRealtimeMillis(),
+                mScript.peekSystemClockMillis() + underThresholdMillis);
+        TimeSignal timeSignal2 = new TimeSignal(TimeSignal.SOURCE_ID_NITZ, utcTime2);
+        mScript.simulateTimePassing(clockIncrement)
+                .simulateTimeSignalReceived(timeSignal2)
+                .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Now send another time signal, but one that is on the threshold and so should be used.
+        TimestampedValue<Long> utcTime3 = new TimestampedValue<>(
+                mScript.peekElapsedRealtimeMillis(),
+                mScript.peekSystemClockMillis() + systemClockUpdateThresholdMillis);
+
+        TimeSignal timeSignal3 = new TimeSignal(TimeSignal.SOURCE_ID_NITZ, utcTime3);
+        mScript.simulateTimePassing(clockIncrement);
+
+        long expectSystemClockMillis3 =
+                TimeDetectorStrategy.getTimeAt(utcTime3, mScript.peekElapsedRealtimeMillis());
+
+        mScript.simulateTimeSignalReceived(timeSignal3)
+                .verifySystemClockWasSetAndResetCallTracking(expectSystemClockMillis3);
+    }
+
+    @Test
+    public void testSuggestTime_nitz_timeDetectionDisabled() {
+        Scenario scenario = SCENARIO_1;
+        mScript.pokeFakeClocks(scenario)
+                .pokeTimeDetectionEnabled(false);
+
+        TimeSignal timeSignal = scenario.createTimeSignalForActual(TimeSignal.SOURCE_ID_NITZ);
+        mScript.simulateTimeSignalReceived(timeSignal)
+                .verifySystemClockWasNotSetAndResetCallTracking();
+    }
+
+    @Test
+    public void testSuggestTime_nitz_invalidNitzReferenceTimesIgnored() {
+        Scenario scenario = SCENARIO_1;
+        final int systemClockUpdateThreshold = 2000;
+        mScript.pokeFakeClocks(scenario)
+                .pokeThresholds(systemClockUpdateThreshold)
+                .pokeTimeDetectionEnabled(true);
+        TimeSignal timeSignal1 = scenario.createTimeSignalForActual(TimeSignal.SOURCE_ID_NITZ);
+        TimestampedValue<Long> utcTime1 = timeSignal1.getUtcTime();
+
+        // Initialize the strategy / device with a time set from NITZ.
+        mScript.simulateTimePassing(100);
+        long expectedSystemClockMillis1 =
+                TimeDetectorStrategy.getTimeAt(utcTime1, mScript.peekElapsedRealtimeMillis());
+        mScript.simulateTimeSignalReceived(timeSignal1)
+                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis1);
+
+        // The UTC time increment should be larger than the system clock update threshold so we
+        // know it shouldn't be ignored for other reasons.
+        long validUtcTimeMillis = utcTime1.getValue() + (2 * systemClockUpdateThreshold);
+
+        // Now supply a new signal that has an obviously bogus reference time : older than the last
+        // one.
+        long referenceTimeBeforeLastSignalMillis = utcTime1.getReferenceTimeMillis() - 1;
+        TimestampedValue<Long> utcTime2 = new TimestampedValue<>(
+                referenceTimeBeforeLastSignalMillis, validUtcTimeMillis);
+        TimeSignal timeSignal2 = new TimeSignal(TimeSignal.SOURCE_ID_NITZ, utcTime2);
+        mScript.simulateTimeSignalReceived(timeSignal2)
+                .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Now supply a new signal that has an obviously bogus reference time : substantially in the
+        // future.
+        long referenceTimeInFutureMillis =
+                utcTime1.getReferenceTimeMillis() + Integer.MAX_VALUE + 1;
+        TimestampedValue<Long> utcTime3 = new TimestampedValue<>(
+                referenceTimeInFutureMillis, validUtcTimeMillis);
+        TimeSignal timeSignal3 = new TimeSignal(TimeSignal.SOURCE_ID_NITZ, utcTime3);
+        mScript.simulateTimeSignalReceived(timeSignal3)
+                .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Just to prove validUtcTimeMillis is valid.
+        long validReferenceTimeMillis = utcTime1.getReferenceTimeMillis() + 100;
+        TimestampedValue<Long> utcTime4 = new TimestampedValue<>(
+                validReferenceTimeMillis, validUtcTimeMillis);
+        long expectedSystemClockMillis4 =
+                TimeDetectorStrategy.getTimeAt(utcTime4, mScript.peekElapsedRealtimeMillis());
+        TimeSignal timeSignal4 = new TimeSignal(TimeSignal.SOURCE_ID_NITZ, utcTime4);
+        mScript.simulateTimeSignalReceived(timeSignal4)
+                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis4);
+    }
+
+    @Test
+    public void testSuggestTime_timeDetectionToggled() {
+        Scenario scenario = SCENARIO_1;
+        final int clockIncrementMillis = 100;
+        final int systemClockUpdateThreshold = 2000;
+        mScript.pokeFakeClocks(scenario)
+                .pokeThresholds(systemClockUpdateThreshold)
+                .pokeTimeDetectionEnabled(false);
+
+        TimeSignal timeSignal1 = scenario.createTimeSignalForActual(TimeSignal.SOURCE_ID_NITZ);
+        TimestampedValue<Long> utcTime1 = timeSignal1.getUtcTime();
+
+        // Simulate time passing.
+        mScript.simulateTimePassing(clockIncrementMillis);
+
+        // Simulate the time signal being received. It should not be used because auto time
+        // detection is off but it should be recorded.
+        mScript.simulateTimeSignalReceived(timeSignal1)
+                .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Simulate more time passing.
+        mScript.simulateTimePassing(clockIncrementMillis);
+
+        long expectedSystemClockMillis1 =
+                TimeDetectorStrategy.getTimeAt(utcTime1, mScript.peekElapsedRealtimeMillis());
+
+        // Turn on auto time detection.
+        mScript.simulateAutoTimeDetectionToggle()
+                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis1);
+
+        // Turn off auto time detection.
+        mScript.simulateAutoTimeDetectionToggle()
+                .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Receive another valid time signal.
+        // It should be on the threshold and accounting for the clock increments.
+        TimestampedValue<Long> utcTime2 = new TimestampedValue<>(
+                mScript.peekElapsedRealtimeMillis(),
+                mScript.peekSystemClockMillis() + systemClockUpdateThreshold);
+        TimeSignal timeSignal2 = new TimeSignal(TimeSignal.SOURCE_ID_NITZ, utcTime2);
+
+        // Simulate more time passing.
+        mScript.simulateTimePassing(clockIncrementMillis);
+
+        long expectedSystemClockMillis2 =
+                TimeDetectorStrategy.getTimeAt(utcTime2, mScript.peekElapsedRealtimeMillis());
+
+        // The new time, though valid, should not be set in the system clock because auto time is
+        // disabled.
+        mScript.simulateTimeSignalReceived(timeSignal2)
+                .verifySystemClockWasNotSetAndResetCallTracking();
+
+        // Turn on auto time detection.
+        mScript.simulateAutoTimeDetectionToggle()
+                .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis2);
     }
 
     @Test
     public void testSuggestTime_unknownSource() {
-        TimestampedValue<Long> utcTime = createUtcTime();
-        TimeSignal timeSignal = new TimeSignal("unknown", utcTime);
-        mSimpleTimeZoneDetectorStrategy.suggestTime(timeSignal);
+        Scenario scenario = SCENARIO_1;
+        mScript.pokeFakeClocks(scenario)
+                .pokeTimeDetectionEnabled(true);
 
-        verify(mMockCallback, never()).setTime(any());
+        TimeSignal timeSignal = scenario.createTimeSignalForActual("unknown");
+        mScript.simulateTimeSignalReceived(timeSignal)
+                .verifySystemClockWasNotSetAndResetCallTracking();
     }
 
-    private static TimestampedValue<Long> createUtcTime() {
-        return new TimestampedValue<>(321L, 123456L);
+    /**
+     * A fake implementation of TimeDetectorStrategy.Callback. Besides tracking changes and behaving
+     * like the real thing should, it also asserts preconditions.
+     */
+    private static class FakeCallback implements TimeDetectorStrategy.Callback {
+        private boolean mTimeDetectionEnabled;
+        private boolean mWakeLockAcquired;
+        private long mElapsedRealtimeMillis;
+        private long mSystemClockMillis;
+        private int mSystemClockUpdateThresholdMillis = 2000;
+
+        // Tracking operations.
+        private boolean mSystemClockWasSet;
+        private Intent mBroadcastSent;
+
+        @Override
+        public int systemClockUpdateThresholdMillis() {
+            return mSystemClockUpdateThresholdMillis;
+        }
+
+        @Override
+        public boolean isTimeDetectionEnabled() {
+            return mTimeDetectionEnabled;
+        }
+
+        @Override
+        public void acquireWakeLock() {
+            if (mWakeLockAcquired) {
+                fail("Wake lock already acquired");
+            }
+            mWakeLockAcquired = true;
+        }
+
+        @Override
+        public long elapsedRealtimeMillis() {
+            assertWakeLockAcquired();
+            return mElapsedRealtimeMillis;
+        }
+
+        @Override
+        public long systemClockMillis() {
+            assertWakeLockAcquired();
+            return mSystemClockMillis;
+        }
+
+        @Override
+        public void setSystemClock(long newTimeMillis) {
+            assertWakeLockAcquired();
+            mSystemClockWasSet = true;
+            mSystemClockMillis = newTimeMillis;
+        }
+
+        @Override
+        public void releaseWakeLock() {
+            assertWakeLockAcquired();
+            mWakeLockAcquired = false;
+        }
+
+        @Override
+        public void sendStickyBroadcast(Intent intent) {
+            assertNotNull(intent);
+            mBroadcastSent = intent;
+        }
+
+        // Methods below are for managing the fake's behavior.
+
+        public void pokeSystemClockUpdateThreshold(int thresholdMillis) {
+            mSystemClockUpdateThresholdMillis = thresholdMillis;
+        }
+
+        public void pokeElapsedRealtimeMillis(long elapsedRealtimeMillis) {
+            mElapsedRealtimeMillis = elapsedRealtimeMillis;
+        }
+
+        public void pokeSystemClockMillis(long systemClockMillis) {
+            mSystemClockMillis = systemClockMillis;
+        }
+
+        public void pokeTimeDetectionEnabled(boolean enabled) {
+            mTimeDetectionEnabled = enabled;
+        }
+
+        public long peekElapsedRealtimeMillis() {
+            return mElapsedRealtimeMillis;
+        }
+
+        public long peekSystemClockMillis() {
+            return mSystemClockMillis;
+        }
+
+        public void simulateTimePassing(int incrementMillis) {
+            mElapsedRealtimeMillis += incrementMillis;
+            mSystemClockMillis += incrementMillis;
+        }
+
+        public void verifySystemClockNotSet() {
+            assertFalse(mSystemClockWasSet);
+        }
+
+        public void verifySystemClockWasSet(long expectSystemClockMillis) {
+            assertTrue(mSystemClockWasSet);
+            assertEquals(expectSystemClockMillis, mSystemClockMillis);
+        }
+
+        public void verifyIntentWasBroadcast() {
+            assertTrue(mBroadcastSent != null);
+        }
+
+        public void verifyIntentWasNotBroadcast() {
+            assertNull(mBroadcastSent);
+        }
+
+        public void resetCallTracking() {
+            mSystemClockWasSet = false;
+            mBroadcastSent = null;
+        }
+
+        private void assertWakeLockAcquired() {
+            assertTrue("The operation must be performed only after acquiring the wakelock",
+                    mWakeLockAcquired);
+        }
+    }
+
+    /**
+     * A fluent helper class for tests.
+     */
+    private class Script {
+
+        private final FakeCallback mFakeCallback;
+        private final SimpleTimeDetectorStrategy mSimpleTimeDetectorStrategy;
+
+        public Script() {
+            mFakeCallback = new FakeCallback();
+            mSimpleTimeDetectorStrategy = new SimpleTimeDetectorStrategy();
+            mSimpleTimeDetectorStrategy.initialize(mFakeCallback);
+
+        }
+
+        Script pokeTimeDetectionEnabled(boolean enabled) {
+            mFakeCallback.pokeTimeDetectionEnabled(enabled);
+            return this;
+        }
+
+        Script pokeFakeClocks(Scenario scenario) {
+            mFakeCallback.pokeElapsedRealtimeMillis(scenario.getInitialRealTimeMillis());
+            mFakeCallback.pokeSystemClockMillis(scenario.getInitialSystemClockMillis());
+            return this;
+        }
+
+        Script pokeThresholds(int systemClockUpdateThreshold) {
+            mFakeCallback.pokeSystemClockUpdateThreshold(systemClockUpdateThreshold);
+            return this;
+        }
+
+        long peekElapsedRealtimeMillis() {
+            return mFakeCallback.peekElapsedRealtimeMillis();
+        }
+
+        long peekSystemClockMillis() {
+            return mFakeCallback.peekSystemClockMillis();
+        }
+
+        Script simulateTimeSignalReceived(TimeSignal timeSignal) {
+            mSimpleTimeDetectorStrategy.suggestTime(timeSignal);
+            return this;
+        }
+
+        Script simulateAutoTimeDetectionToggle() {
+            boolean enabled = !mFakeCallback.isTimeDetectionEnabled();
+            mFakeCallback.pokeTimeDetectionEnabled(enabled);
+            mSimpleTimeDetectorStrategy.handleAutoTimeDetectionToggle(enabled);
+            return this;
+        }
+
+        Script simulateTimePassing(int clockIncrement) {
+            mFakeCallback.simulateTimePassing(clockIncrement);
+            return this;
+        }
+
+        Script verifySystemClockWasNotSetAndResetCallTracking() {
+            mFakeCallback.verifySystemClockNotSet();
+            mFakeCallback.verifyIntentWasNotBroadcast();
+            mFakeCallback.resetCallTracking();
+            return this;
+        }
+
+        Script verifySystemClockWasSetAndResetCallTracking(long expectSystemClockMillis) {
+            mFakeCallback.verifySystemClockWasSet(expectSystemClockMillis);
+            mFakeCallback.verifyIntentWasBroadcast();
+            mFakeCallback.resetCallTracking();
+            return this;
+        }
+    }
+
+    /**
+     * A starting scenario used during tests. Describes a fictional "physical" reality.
+     */
+    private static class Scenario {
+
+        private final long mInitialDeviceSystemClockMillis;
+        private final long mInitialDeviceRealtimeMillis;
+        private final long mActualTimeMillis;
+
+        Scenario(long initialDeviceSystemClock, long elapsedRealtime, long timeMillis) {
+            mInitialDeviceSystemClockMillis = initialDeviceSystemClock;
+            mActualTimeMillis = timeMillis;
+            mInitialDeviceRealtimeMillis = elapsedRealtime;
+        }
+
+        long getInitialRealTimeMillis() {
+            return mInitialDeviceRealtimeMillis;
+        }
+
+        long getInitialSystemClockMillis() {
+            return mInitialDeviceSystemClockMillis;
+        }
+
+        long getActualTimeMillis() {
+            return mActualTimeMillis;
+        }
+
+        TimeSignal createTimeSignalForActual(String sourceId) {
+            TimestampedValue<Long> time = new TimestampedValue<>(
+                    mInitialDeviceRealtimeMillis, mActualTimeMillis);
+            return new TimeSignal(sourceId, time);
+        }
+
+        static class Builder {
+
+            private long mInitialDeviceSystemClockMillis;
+            private long mInitialDeviceRealtimeMillis;
+            private long mActualTimeMillis;
+
+            Builder setInitialDeviceSystemClockUtc(int year, int monthInYear, int day,
+                    int hourOfDay, int minute, int second) {
+                mInitialDeviceSystemClockMillis = createUtcTime(year, monthInYear, day, hourOfDay,
+                        minute, second);
+                return this;
+            }
+
+            Builder setInitialDeviceRealtimeMillis(long realtimeMillis) {
+                mInitialDeviceRealtimeMillis = realtimeMillis;
+                return this;
+            }
+
+            Builder setActualTimeUtc(int year, int monthInYear, int day, int hourOfDay,
+                    int minute, int second) {
+                mActualTimeMillis =
+                        createUtcTime(year, monthInYear, day, hourOfDay, minute, second);
+                return this;
+            }
+
+            Scenario build() {
+                return new Scenario(mInitialDeviceSystemClockMillis, mInitialDeviceRealtimeMillis,
+                        mActualTimeMillis);
+            }
+        }
+    }
+
+    private static long createUtcTime(int year, int monthInYear, int day, int hourOfDay, int minute,
+            int second) {
+        Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("Etc/UTC"));
+        cal.clear();
+        cal.set(year, monthInYear - 1, day, hourOfDay, minute, second);
+        return cal.getTimeInMillis();
     }
 }
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 22dea92..ed74cd7 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.timedetector;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -23,36 +26,40 @@
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
 
 import android.app.timedetector.TimeSignal;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.support.test.runner.AndroidJUnit4;
 import android.util.TimestampedValue;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import com.android.server.timedetector.TimeDetectorStrategy.Callback;
+
+import java.io.PrintWriter;
+
 @RunWith(AndroidJUnit4.class)
 public class TimeDetectorServiceTest {
 
-    private TimeDetectorService mTimeDetectorService;
-
     private Context mMockContext;
-    private TimeDetectorStrategy mMockTimeDetectorStrategy;
+    private StubbedTimeDetectorStrategy mStubbedTimeDetectorStrategy;
+    private Callback mMockCallback;
+
+    private TimeDetectorService mTimeDetectorService;
 
     @Before
     public void setUp() {
         mMockContext = mock(Context.class);
-        mMockTimeDetectorStrategy = mock(TimeDetectorStrategy.class);
-        mTimeDetectorService = new TimeDetectorService(mMockContext, mMockTimeDetectorStrategy);
-    }
+        mMockCallback = mock(Callback.class);
+        mStubbedTimeDetectorStrategy = new StubbedTimeDetectorStrategy();
 
-    @After
-    public void tearDown() {
-        verifyNoMoreInteractions(mMockContext, mMockTimeDetectorStrategy);
+        mTimeDetectorService = new TimeDetectorService(
+                mMockContext, mMockCallback,
+                mStubbedTimeDetectorStrategy);
     }
 
     @Test(expected=SecurityException.class)
@@ -78,11 +85,86 @@
 
         verify(mMockContext)
                 .enforceCallingPermission(eq(android.Manifest.permission.SET_TIME), anyString());
-        verify(mMockTimeDetectorStrategy).suggestTime(timeSignal);
+        mStubbedTimeDetectorStrategy.verifySuggestTimeCalled(timeSignal);
+    }
+
+    @Test
+    public void testDump() {
+        when(mMockContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+        mTimeDetectorService.dump(null, null, null);
+
+        verify(mMockContext).checkCallingOrSelfPermission(eq(android.Manifest.permission.DUMP));
+        mStubbedTimeDetectorStrategy.verifyDumpCalled();
+    }
+
+    @Test
+    public void testAutoTimeDetectionToggle() {
+        when(mMockCallback.isTimeDetectionEnabled()).thenReturn(true);
+
+        mTimeDetectorService.handleAutoTimeDetectionToggle();
+
+        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(true);
+
+        when(mMockCallback.isTimeDetectionEnabled()).thenReturn(false);
+
+        mTimeDetectorService.handleAutoTimeDetectionToggle();
+
+        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(false);
     }
 
     private static TimeSignal createNitzTimeSignal() {
         TimestampedValue<Long> timeValue = new TimestampedValue<>(100L, 1_000_000L);
         return new TimeSignal(TimeSignal.SOURCE_ID_NITZ, timeValue);
     }
+
+    private static class StubbedTimeDetectorStrategy implements TimeDetectorStrategy {
+
+        // Call tracking.
+        private TimeSignal mLastSuggestedTime;
+        private Boolean mLastAutoTimeDetectionToggle;
+        private boolean mDumpCalled;
+
+        @Override
+        public void initialize(Callback ignored) {
+        }
+
+        @Override
+        public void suggestTime(TimeSignal timeSignal) {
+            resetCallTracking();
+            mLastSuggestedTime = timeSignal;
+        }
+
+        @Override
+        public void handleAutoTimeDetectionToggle(boolean enabled) {
+            resetCallTracking();
+            mLastAutoTimeDetectionToggle = enabled;
+        }
+
+        @Override
+        public void dump(PrintWriter pw, String[] args) {
+            resetCallTracking();
+            mDumpCalled = true;
+        }
+
+        void resetCallTracking() {
+            mLastSuggestedTime = null;
+            mLastAutoTimeDetectionToggle = null;
+            mDumpCalled = false;
+        }
+
+        void verifySuggestTimeCalled(TimeSignal expectedSignal) {
+            assertEquals(expectedSignal, mLastSuggestedTime);
+        }
+
+        void verifyHandleAutoTimeDetectionToggleCalled(boolean expectedEnable) {
+            assertNotNull(mLastAutoTimeDetectionToggle);
+            assertEquals(expectedEnable, mLastAutoTimeDetectionToggle);
+        }
+
+        void verifyDumpCalled() {
+            assertTrue(mDumpCalled);
+        }
+    }
 }