Merge "Make TimeDetectorService / Strategy consistent"
diff --git a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
index 3c79b23..0b970bf 100644
--- a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
+++ b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
@@ -23,10 +23,13 @@
 import android.app.timedetector.ManualTimeSuggestion;
 import android.app.timedetector.PhoneTimeSuggestion;
 import android.content.Intent;
+import android.util.LocalLog;
 import android.util.Slog;
 import android.util.TimestampedValue;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.util.IndentingPrintWriter;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -34,13 +37,14 @@
 
 /**
  * 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).
+ * {@link AlarmManager}.
+ *
+ * <p>Most public methods are marked synchronized to ensure thread safety around internal state.
  */
-// @NotThreadSafe
 public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
 
-    private final static String TAG = "timedetector.SimpleTimeDetectorStrategy";
+    private static final boolean DBG = false;
+    private static final String LOG_TAG = "SimpleTimeDetectorStrategy";
 
     @IntDef({ ORIGIN_PHONE, ORIGIN_MANUAL })
     @Retention(RetentionPolicy.SOURCE)
@@ -61,6 +65,9 @@
      */
     private static final long SYSTEM_CLOCK_PARANOIA_THRESHOLD_MILLIS = 2 * 1000;
 
+    // A log for changes made to the system clock and why.
+    @NonNull private final LocalLog mTimeChangesLog = new LocalLog(30);
+
     // @NonNull after initialize()
     private Callback mCallback;
 
@@ -80,7 +87,7 @@
     }
 
     @Override
-    public void suggestPhoneTime(@NonNull PhoneTimeSuggestion timeSuggestion) {
+    public synchronized void suggestPhoneTime(@NonNull PhoneTimeSuggestion timeSuggestion) {
         // NITZ logic
 
         // Empty suggestions are just ignored as we don't currently keep track of suggestion origin.
@@ -103,7 +110,7 @@
     }
 
     @Override
-    public void suggestManualTime(ManualTimeSuggestion timeSuggestion) {
+    public synchronized void suggestManualTime(ManualTimeSuggestion timeSuggestion) {
         final TimestampedValue<Long> newUtcTime = timeSuggestion.getUtcTime();
         setSystemClockIfRequired(ORIGIN_MANUAL, newUtcTime, timeSuggestion);
     }
@@ -116,7 +123,7 @@
                     newSuggestion.getUtcTime(), lastSuggestion.getUtcTime());
             if (referenceTimeDifference < 0 || referenceTimeDifference > Integer.MAX_VALUE) {
                 // Out of order or bogus.
-                Slog.w(TAG, "validateNewNitzTime: Bad NITZ signal received."
+                Slog.w(LOG_TAG, "Bad NITZ signal received."
                         + " referenceTimeDifference=" + referenceTimeDifference
                         + " lastSuggestion=" + lastSuggestion
                         + " newSuggestion=" + newSuggestion);
@@ -126,6 +133,7 @@
         return true;
     }
 
+    @GuardedBy("this")
     private void setSystemClockIfRequired(
             @Origin int origin, TimestampedValue<Long> time, Object cause) {
         // Historically, Android has sent a TelephonyIntents.ACTION_NETWORK_SET_TIME broadcast only
@@ -140,16 +148,20 @@
             mLastAutoSystemClockTimeSendNetworkBroadcast = sendNetworkBroadcast;
 
             if (!mCallback.isAutoTimeDetectionEnabled()) {
-                Slog.d(TAG, "setSystemClockIfRequired: Auto time detection is not enabled."
-                        + " time=" + time
-                        + ", cause=" + cause);
+                if (DBG) {
+                    Slog.d(LOG_TAG, "Auto time detection is not enabled."
+                            + " time=" + time
+                            + ", cause=" + cause);
+                }
                 return;
             }
         } else {
             if (mCallback.isAutoTimeDetectionEnabled()) {
-                Slog.d(TAG, "setSystemClockIfRequired: Auto time detection is enabled."
-                        + " time=" + time
-                        + ", cause=" + cause);
+                if (DBG) {
+                    Slog.d(LOG_TAG, "Auto time detection is enabled."
+                            + " time=" + time
+                            + ", cause=" + cause);
+                }
                 return;
             }
         }
@@ -167,7 +179,7 @@
                             mLastAutoSystemClockTimeSet, elapsedRealtimeMillis);
                     long absSystemClockDifference = Math.abs(expectedTimeMillis - actualTimeMillis);
                     if (absSystemClockDifference > SYSTEM_CLOCK_PARANOIA_THRESHOLD_MILLIS) {
-                        Slog.w(TAG,
+                        Slog.w(LOG_TAG,
                                 "System clock has not tracked elapsed real time clock. A clock may"
                                         + " be inaccurate or something unexpectedly set the system"
                                         + " clock."
@@ -190,9 +202,10 @@
     }
 
     @Override
-    public void handleAutoTimeDetectionToggle(boolean enabled) {
+    public synchronized void handleAutoTimeDetectionChanged() {
         // 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.
+        boolean enabled = mCallback.isAutoTimeDetectionEnabled();
         if (enabled) {
             if (mLastAutoSystemClockTime != null) {
                 // Only send the network broadcast if the last candidate would have caused one.
@@ -218,14 +231,27 @@
     }
 
     @Override
-    public void dump(@NonNull PrintWriter pw, @Nullable String[] args) {
+    public synchronized void dump(@NonNull PrintWriter pw, @Nullable String[] args) {
         pw.println("mLastPhoneSuggestion=" + mLastPhoneSuggestion);
         pw.println("mLastAutoSystemClockTimeSet=" + mLastAutoSystemClockTimeSet);
         pw.println("mLastAutoSystemClockTime=" + mLastAutoSystemClockTime);
         pw.println("mLastAutoSystemClockTimeSendNetworkBroadcast="
                 + mLastAutoSystemClockTimeSendNetworkBroadcast);
+
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+
+        ipw.println("TimeDetectorStrategyImpl logs:");
+        ipw.increaseIndent(); // level 1
+
+        ipw.println("Time change log:");
+        ipw.increaseIndent(); // level 2
+        mTimeChangesLog.dump(ipw);
+        ipw.decreaseIndent(); // level 2
+
+        ipw.decreaseIndent(); // level 1
     }
 
+    @GuardedBy("this")
     private void adjustAndSetDeviceSystemClock(
             TimestampedValue<Long> newTime, boolean sendNetworkBroadcast,
             long elapsedRealtimeMillis, long actualSystemClockMillis, Object cause) {
@@ -238,21 +264,27 @@
         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
-                    + " cause=" + cause
-                    + " systemClockUpdateThreshold=" + systemClockUpdateThreshold
-                    + " absTimeDifference=" + absTimeDifference);
+            if (DBG) {
+                Slog.d(LOG_TAG, "Not setting system clock. New time and"
+                        + " system clock are close enough."
+                        + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+                        + " newTime=" + newTime
+                        + " cause=" + cause
+                        + " systemClockUpdateThreshold=" + systemClockUpdateThreshold
+                        + " absTimeDifference=" + absTimeDifference);
+            }
             return;
         }
 
-        Slog.d(TAG, "Setting system clock using time=" + newTime
+        mCallback.setSystemClock(newSystemClockMillis);
+        String logMsg = "Set system clock using time=" + newTime
                 + " cause=" + cause
                 + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
-                + " newTimeMillis=" + newSystemClockMillis);
-        mCallback.setSystemClock(newSystemClockMillis);
+                + " newSystemClockMillis=" + newSystemClockMillis;
+        if (DBG) {
+            Slog.d(LOG_TAG, logMsg);
+        }
+        mTimeChangesLog.log(logMsg);
 
         // CLOCK_PARANOIA : Record the last time this class set the system clock.
         mLastAutoSystemClockTimeSet = newTime;
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java
index 7f5b403..34400ff 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorService.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorService.java
@@ -24,10 +24,9 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
-import android.os.Binder;
+import android.os.Handler;
 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;
@@ -39,7 +38,7 @@
 import java.util.Objects;
 
 public final class TimeDetectorService extends ITimeDetectorService.Stub {
-    private static final String TAG = "timedetector.TimeDetectorService";
+    private static final String TAG = "TimeDetectorService";
 
     public static class Lifecycle extends SystemService {
 
@@ -57,29 +56,25 @@
         }
     }
 
+    @NonNull private final Handler mHandler;
     @NonNull private final Context mContext;
     @NonNull private final Callback mCallback;
-
-    // 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);
+        TimeDetectorStrategy timeDetector = new SimpleTimeDetectorStrategy();
+        TimeDetectorStrategyCallbackImpl callback = new TimeDetectorStrategyCallbackImpl(context);
         timeDetector.initialize(callback);
 
+        Handler handler = FgThread.getHandler();
         TimeDetectorService timeDetectorService =
-                new TimeDetectorService(context, callback, timeDetector);
+                new TimeDetectorService(context, handler, callback, timeDetector);
 
         // Wire up event listening.
         ContentResolver contentResolver = context.getContentResolver();
         contentResolver.registerContentObserver(
                 Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
-                new ContentObserver(FgThread.getHandler()) {
+                new ContentObserver(handler) {
                     public void onChange(boolean selfChange) {
                         timeDetectorService.handleAutoTimeDetectionToggle();
                     }
@@ -89,9 +84,10 @@
     }
 
     @VisibleForTesting
-    public TimeDetectorService(@NonNull Context context, @NonNull Callback callback,
-            @NonNull TimeDetectorStrategy timeDetectorStrategy) {
+    public TimeDetectorService(@NonNull Context context, @NonNull Handler handler,
+            @NonNull Callback callback, @NonNull TimeDetectorStrategy timeDetectorStrategy) {
         mContext = Objects.requireNonNull(context);
+        mHandler = Objects.requireNonNull(handler);
         mCallback = Objects.requireNonNull(callback);
         mTimeDetectorStrategy = Objects.requireNonNull(timeDetectorStrategy);
     }
@@ -101,14 +97,7 @@
         enforceSuggestPhoneTimePermission();
         Objects.requireNonNull(timeSignal);
 
-        long idToken = Binder.clearCallingIdentity();
-        try {
-            synchronized (mStrategyLock) {
-                mTimeDetectorStrategy.suggestPhoneTime(timeSignal);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(idToken);
-        }
+        mHandler.post(() -> mTimeDetectorStrategy.suggestPhoneTime(timeSignal));
     }
 
     @Override
@@ -116,22 +105,12 @@
         enforceSuggestManualTimePermission();
         Objects.requireNonNull(timeSignal);
 
-        long idToken = Binder.clearCallingIdentity();
-        try {
-            synchronized (mStrategyLock) {
-                mTimeDetectorStrategy.suggestManualTime(timeSignal);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(idToken);
-        }
+        mHandler.post(() -> mTimeDetectorStrategy.suggestManualTime(timeSignal));
     }
 
     @VisibleForTesting
     public void handleAutoTimeDetectionToggle() {
-        synchronized (mStrategyLock) {
-            final boolean timeDetectionEnabled = mCallback.isAutoTimeDetectionEnabled();
-            mTimeDetectorStrategy.handleAutoTimeDetectionToggle(timeDetectionEnabled);
-        }
+        mHandler.post(mTimeDetectorStrategy::handleAutoTimeDetectionChanged);
     }
 
     @Override
@@ -139,9 +118,7 @@
             @Nullable String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
-        synchronized (mStrategyLock) {
-            mTimeDetectorStrategy.dump(pw, args);
-        }
+        mTimeDetectorStrategy.dump(pw, args);
     }
 
     private void enforceSuggestPhoneTimePermission() {
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
index b60cebf..32cee2d 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
@@ -27,12 +27,14 @@
 
 /**
  * The interface for classes that implement the time detection algorithm used by the
- * 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).
+ * TimeDetectorService.
+ *
+ * <p>Most calls will be handled by a single thread but that is not true for all calls. For example
+ * {@link #dump(PrintWriter, String[])}) may be called on a different thread so implementations must
+ * handle thread safety.
  *
  * @hide
  */
-// @NotThreadSafe
 public interface TimeDetectorStrategy {
 
     /**
@@ -79,7 +81,7 @@
     void suggestManualTime(@NonNull ManualTimeSuggestion timeSuggestion);
 
     /** Handle the auto-time setting being toggled on or off. */
-    void handleAutoTimeDetectionToggle(boolean enabled);
+    void handleAutoTimeDetectionChanged();
 
     /** Dump debug information. */
     void dump(@NonNull PrintWriter pw, @Nullable String[] args);
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java
rename to services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java
index 317fd4d..8a7edf7 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java
@@ -40,7 +40,7 @@
 import java.time.Duration;
 
 @RunWith(AndroidJUnit4.class)
-public class SimpleTimeZoneDetectorStrategyTest {
+public class SimpleTimeDetectorStrategyTest {
 
     private static final Scenario SCENARIO_1 = new Scenario.Builder()
             .setInitialDeviceSystemClockUtc(1977, 1, 1, 12, 0, 0)
@@ -440,7 +440,7 @@
             mSystemClockMillis = systemClockMillis;
         }
 
-        public void pokeTimeDetectionEnabled(boolean enabled) {
+        public void pokeAutoTimeDetectionEnabled(boolean enabled) {
             mTimeDetectionEnabled = enabled;
         }
 
@@ -457,6 +457,10 @@
             mSystemClockMillis += incrementMillis;
         }
 
+        public void simulateAutoTimeZoneDetectionToggle() {
+            mTimeDetectionEnabled = !mTimeDetectionEnabled;
+        }
+
         public void verifySystemClockNotSet() {
             assertFalse(mSystemClockWasSet);
         }
@@ -493,7 +497,7 @@
         private final FakeCallback mFakeCallback;
         private final SimpleTimeDetectorStrategy mSimpleTimeDetectorStrategy;
 
-        public Script() {
+        Script() {
             mFakeCallback = new FakeCallback();
             mSimpleTimeDetectorStrategy = new SimpleTimeDetectorStrategy();
             mSimpleTimeDetectorStrategy.initialize(mFakeCallback);
@@ -501,7 +505,7 @@
         }
 
         Script pokeTimeDetectionEnabled(boolean enabled) {
-            mFakeCallback.pokeTimeDetectionEnabled(enabled);
+            mFakeCallback.pokeAutoTimeDetectionEnabled(enabled);
             return this;
         }
 
@@ -535,9 +539,8 @@
         }
 
         Script simulateAutoTimeDetectionToggle() {
-            boolean enabled = !mFakeCallback.isAutoTimeDetectionEnabled();
-            mFakeCallback.pokeTimeDetectionEnabled(enabled);
-            mSimpleTimeDetectorStrategy.handleAutoTimeDetectionToggle(enabled);
+            mFakeCallback.simulateAutoTimeZoneDetectionToggle();
+            mSimpleTimeDetectorStrategy.handleAutoTimeDetectionChanged();
             return this;
         }
 
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 4efe771..9951e85 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
@@ -17,13 +17,11 @@
 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;
 import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -32,12 +30,17 @@
 import android.app.timedetector.PhoneTimeSuggestion;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
 import android.util.TimestampedValue;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.timedetector.TimeDetectorStrategy.Callback;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,54 +55,62 @@
     private Callback mMockCallback;
 
     private TimeDetectorService mTimeDetectorService;
+    private HandlerThread mHandlerThread;
+    private TestHandler mTestHandler;
+
 
     @Before
     public void setUp() {
         mMockContext = mock(Context.class);
+
+        // Create a thread + handler for processing the work that the service posts.
+        mHandlerThread = new HandlerThread("TimeDetectorServiceTest");
+        mHandlerThread.start();
+        mTestHandler = new TestHandler(mHandlerThread.getLooper());
+
         mMockCallback = mock(Callback.class);
         mStubbedTimeDetectorStrategy = new StubbedTimeDetectorStrategy();
 
         mTimeDetectorService = new TimeDetectorService(
-                mMockContext, mMockCallback,
+                mMockContext, mTestHandler, mMockCallback,
                 mStubbedTimeDetectorStrategy);
     }
 
-    @Test(expected=SecurityException.class)
-    public void testStubbedCall_withoutPermission() {
-        doThrow(new SecurityException("Mock"))
-                .when(mMockContext).enforceCallingPermission(anyString(), any());
-        PhoneTimeSuggestion phoneTimeSuggestion = createPhoneTimeSuggestion();
-
-        try {
-            mTimeDetectorService.suggestPhoneTime(phoneTimeSuggestion);
-        } finally {
-            verify(mMockContext).enforceCallingPermission(
-                    eq(android.Manifest.permission.SET_TIME), anyString());
-        }
+    @After
+    public void tearDown() throws Exception {
+        mHandlerThread.quit();
+        mHandlerThread.join();
     }
 
     @Test
-    public void testSuggestPhoneTime() {
+    public void testSuggestPhoneTime() throws Exception {
         doNothing().when(mMockContext).enforceCallingPermission(anyString(), any());
 
         PhoneTimeSuggestion phoneTimeSuggestion = createPhoneTimeSuggestion();
         mTimeDetectorService.suggestPhoneTime(phoneTimeSuggestion);
-
-        verify(mMockContext)
-                .enforceCallingPermission(eq(android.Manifest.permission.SET_TIME), anyString());
-        mStubbedTimeDetectorStrategy.verifySuggestPhoneTimeCalled(phoneTimeSuggestion);
-    }
-
-    @Test
-    public void testSuggestManualTime() {
-        doNothing().when(mMockContext).enforceCallingPermission(anyString(), any());
-
-        ManualTimeSuggestion manualTimeSuggestion = createManualTimeSuggestion();
-        mTimeDetectorService.suggestManualTime(manualTimeSuggestion);
+        mTestHandler.assertTotalMessagesEnqueued(1);
 
         verify(mMockContext).enforceCallingPermission(
                 eq(android.Manifest.permission.SET_TIME),
                 anyString());
+
+        mTestHandler.waitForEmptyQueue();
+        mStubbedTimeDetectorStrategy.verifySuggestPhoneTimeCalled(phoneTimeSuggestion);
+    }
+
+    @Test
+    public void testSuggestManualTime() throws Exception {
+        doNothing().when(mMockContext).enforceCallingPermission(anyString(), any());
+
+        ManualTimeSuggestion manualTimeSuggestion = createManualTimeSuggestion();
+        mTimeDetectorService.suggestManualTime(manualTimeSuggestion);
+        mTestHandler.assertTotalMessagesEnqueued(1);
+
+        verify(mMockContext).enforceCallingPermission(
+                eq(android.Manifest.permission.SET_TIME),
+                anyString());
+
+        mTestHandler.waitForEmptyQueue();
         mStubbedTimeDetectorStrategy.verifySuggestManualTimeCalled(manualTimeSuggestion);
     }
 
@@ -115,18 +126,16 @@
     }
 
     @Test
-    public void testAutoTimeDetectionToggle() {
-        when(mMockCallback.isAutoTimeDetectionEnabled()).thenReturn(true);
+    public void testAutoTimeDetectionToggle() throws Exception {
+        mTimeDetectorService.handleAutoTimeDetectionToggle();
+        mTestHandler.assertTotalMessagesEnqueued(1);
+        mTestHandler.waitForEmptyQueue();
+        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled();
 
         mTimeDetectorService.handleAutoTimeDetectionToggle();
-
-        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(true);
-
-        when(mMockCallback.isAutoTimeDetectionEnabled()).thenReturn(false);
-
-        mTimeDetectorService.handleAutoTimeDetectionToggle();
-
-        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(false);
+        mTestHandler.assertTotalMessagesEnqueued(2);
+        mTestHandler.waitForEmptyQueue();
+        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled();
     }
 
     private static PhoneTimeSuggestion createPhoneTimeSuggestion() {
@@ -147,7 +156,7 @@
         // Call tracking.
         private PhoneTimeSuggestion mLastPhoneSuggestion;
         private ManualTimeSuggestion mLastManualSuggestion;
-        private Boolean mLastAutoTimeDetectionToggle;
+        private boolean mLastAutoTimeDetectionToggleCalled;
         private boolean mDumpCalled;
 
         @Override
@@ -167,9 +176,9 @@
         }
 
         @Override
-        public void handleAutoTimeDetectionToggle(boolean enabled) {
+        public void handleAutoTimeDetectionChanged() {
             resetCallTracking();
-            mLastAutoTimeDetectionToggle = enabled;
+            mLastAutoTimeDetectionToggleCalled = true;
         }
 
         @Override
@@ -181,7 +190,7 @@
         void resetCallTracking() {
             mLastPhoneSuggestion = null;
             mLastManualSuggestion = null;
-            mLastAutoTimeDetectionToggle = null;
+            mLastAutoTimeDetectionToggleCalled = false;
             mDumpCalled = false;
         }
 
@@ -193,13 +202,45 @@
             assertEquals(expectedSuggestion, mLastManualSuggestion);
         }
 
-        void verifyHandleAutoTimeDetectionToggleCalled(boolean expectedEnable) {
-            assertNotNull(mLastAutoTimeDetectionToggle);
-            assertEquals(expectedEnable, mLastAutoTimeDetectionToggle);
+        void verifyHandleAutoTimeDetectionToggleCalled() {
+            assertTrue(mLastAutoTimeDetectionToggleCalled);
         }
 
         void verifyDumpCalled() {
             assertTrue(mDumpCalled);
         }
     }
+
+    /**
+     * A Handler that can track posts/sends and wait for work to be completed.
+     */
+    private static class TestHandler extends Handler {
+
+        private int mMessagesSent;
+
+        TestHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
+            mMessagesSent++;
+            return super.sendMessageAtTime(msg, uptimeMillis);
+        }
+
+        /** Asserts the number of messages posted or sent is as expected. */
+        void assertTotalMessagesEnqueued(int expected) {
+            assertEquals(expected, mMessagesSent);
+        }
+
+        /**
+         * Waits for all currently enqueued work due to be processed to be completed before
+         * returning.
+         */
+        void waitForEmptyQueue() throws InterruptedException {
+            while (!getLooper().getQueue().isIdle()) {
+                Thread.sleep(100);
+            }
+        }
+    }
 }