Merge "Add more behavior to TimeDetectorService" am: b1fc5a4c7e
am: 325ecf7df4

Change-Id: I28d5b3dbd666dc9d097f6fb7c15dc5ba8cf931e9
diff --git a/core/java/android/util/TimestampedValue.java b/core/java/android/util/TimestampedValue.java
index 75fa18d..1289e4d 100644
--- a/core/java/android/util/TimestampedValue.java
+++ b/core/java/android/util/TimestampedValue.java
@@ -126,4 +126,12 @@
         dest.writeLong(timestampedValue.mReferenceTimeMillis);
         dest.writeValue(timestampedValue.mValue);
     }
+
+    /**
+     * Returns the difference in milliseconds between two instance's reference times.
+     */
+    public static long referenceTimeDifference(
+            @NonNull TimestampedValue<?> one, @NonNull TimestampedValue<?> two) {
+        return one.mReferenceTimeMillis - two.mReferenceTimeMillis;
+    }
 }
diff --git a/core/tests/coretests/src/android/util/TimestampedValueTest.java b/core/tests/coretests/src/android/util/TimestampedValueTest.java
index 7117c1b..03b4abd 100644
--- a/core/tests/coretests/src/android/util/TimestampedValueTest.java
+++ b/core/tests/coretests/src/android/util/TimestampedValueTest.java
@@ -116,4 +116,14 @@
             parcel.recycle();
         }
     }
+
+    @Test
+    public void testReferenceTimeDifference() {
+        TimestampedValue<Long> value1 = new TimestampedValue<>(1000, 123L);
+        assertEquals(0, TimestampedValue.referenceTimeDifference(value1, value1));
+
+        TimestampedValue<Long> value2 = new TimestampedValue<>(1, 321L);
+        assertEquals(999, TimestampedValue.referenceTimeDifference(value1, value2));
+        assertEquals(-999, TimestampedValue.referenceTimeDifference(value2, value1));
+    }
 }
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);
+        }
+    }
 }