Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 77d50097 authored by Neil Fuller's avatar Neil Fuller Committed by Gerrit Code Review
Browse files

Merge "Make TimeDetectorService / Strategy consistent"

parents a57d5599 40cf2957
Loading
Loading
Loading
Loading
+58 −26
Original line number Diff line number Diff line
@@ -23,10 +23,13 @@ import android.app.AlarmManager;
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 @@ import java.lang.annotation.RetentionPolicy;

/**
 * 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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
     */
    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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
    }

    @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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
    }

    @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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
                    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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
        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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
            mLastAutoSystemClockTimeSendNetworkBroadcast = sendNetworkBroadcast;

            if (!mCallback.isAutoTimeDetectionEnabled()) {
                Slog.d(TAG, "setSystemClockIfRequired: Auto time detection is not enabled."
                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."
                if (DBG) {
                    Slog.d(LOG_TAG, "Auto time detection is enabled."
                            + " time=" + time
                            + ", cause=" + cause);
                }
                return;
            }
        }
@@ -167,7 +179,7 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
                            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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
    }

    @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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
    }

    @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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy {
        long absTimeDifference = Math.abs(newSystemClockMillis - actualSystemClockMillis);
        long systemClockUpdateThreshold = mCallback.systemClockUpdateThresholdMillis();
        if (absTimeDifference < systemClockUpdateThreshold) {
            Slog.d(TAG, "adjustAndSetDeviceSystemClock: Not setting system clock. New time and"
            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;
+15 −38
Original line number Diff line number Diff line
@@ -24,10 +24,9 @@ import android.app.timedetector.PhoneTimeSuggestion;
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.io.PrintWriter;
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 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
        }
    }

    @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 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
    }

    @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 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
        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 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
        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,10 +118,8 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
            @Nullable String[] args) {
        if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;

        synchronized (mStrategyLock) {
        mTimeDetectorStrategy.dump(pw, args);
    }
    }

    private void enforceSuggestPhoneTimePermission() {
        mContext.enforceCallingPermission(android.Manifest.permission.SET_TIME, "set time");
+6 −4
Original line number Diff line number Diff line
@@ -27,12 +27,14 @@ import java.io.PrintWriter;

/**
 * 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 @@ public interface TimeDetectorStrategy {
    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);
+10 −7
Original line number Diff line number Diff line
@@ -40,7 +40,7 @@ import org.junit.runner.RunWith;
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 @@ public class SimpleTimeZoneDetectorStrategyTest {
            mSystemClockMillis = systemClockMillis;
        }

        public void pokeTimeDetectionEnabled(boolean enabled) {
        public void pokeAutoTimeDetectionEnabled(boolean enabled) {
            mTimeDetectionEnabled = enabled;
        }

@@ -457,6 +457,10 @@ public class SimpleTimeZoneDetectorStrategyTest {
            mSystemClockMillis += incrementMillis;
        }

        public void simulateAutoTimeZoneDetectionToggle() {
            mTimeDetectionEnabled = !mTimeDetectionEnabled;
        }

        public void verifySystemClockNotSet() {
            assertFalse(mSystemClockWasSet);
        }
@@ -493,7 +497,7 @@ public class SimpleTimeZoneDetectorStrategyTest {
        private final FakeCallback mFakeCallback;
        private final SimpleTimeDetectorStrategy mSimpleTimeDetectorStrategy;

        public Script() {
        Script() {
            mFakeCallback = new FakeCallback();
            mSimpleTimeDetectorStrategy = new SimpleTimeDetectorStrategy();
            mSimpleTimeDetectorStrategy.initialize(mFakeCallback);
@@ -501,7 +505,7 @@ public class SimpleTimeZoneDetectorStrategyTest {
        }

        Script pokeTimeDetectionEnabled(boolean enabled) {
            mFakeCallback.pokeTimeDetectionEnabled(enabled);
            mFakeCallback.pokeAutoTimeDetectionEnabled(enabled);
            return this;
        }

@@ -535,9 +539,8 @@ public class SimpleTimeZoneDetectorStrategyTest {
        }

        Script simulateAutoTimeDetectionToggle() {
            boolean enabled = !mFakeCallback.isAutoTimeDetectionEnabled();
            mFakeCallback.pokeTimeDetectionEnabled(enabled);
            mSimpleTimeDetectorStrategy.handleAutoTimeDetectionToggle(enabled);
            mFakeCallback.simulateAutoTimeZoneDetectionToggle();
            mSimpleTimeDetectorStrategy.handleAutoTimeDetectionChanged();
            return this;
        }

+76 −35
Original line number Diff line number Diff line
@@ -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.ManualTimeSuggestion;
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 @@ public class TimeDetectorServiceTest {
    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);
        mTestHandler.assertTotalMessagesEnqueued(1);

        verify(mMockContext)
                .enforceCallingPermission(eq(android.Manifest.permission.SET_TIME), anyString());
        verify(mMockContext).enforceCallingPermission(
                eq(android.Manifest.permission.SET_TIME),
                anyString());

        mTestHandler.waitForEmptyQueue();
        mStubbedTimeDetectorStrategy.verifySuggestPhoneTimeCalled(phoneTimeSuggestion);
    }

    @Test
    public void testSuggestManualTime() {
    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 @@ public class TimeDetectorServiceTest {
    }

    @Test
    public void testAutoTimeDetectionToggle() {
        when(mMockCallback.isAutoTimeDetectionEnabled()).thenReturn(true);

    public void testAutoTimeDetectionToggle() throws Exception {
        mTimeDetectorService.handleAutoTimeDetectionToggle();

        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(true);

        when(mMockCallback.isAutoTimeDetectionEnabled()).thenReturn(false);
        mTestHandler.assertTotalMessagesEnqueued(1);
        mTestHandler.waitForEmptyQueue();
        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled();

        mTimeDetectorService.handleAutoTimeDetectionToggle();

        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled(false);
        mTestHandler.assertTotalMessagesEnqueued(2);
        mTestHandler.waitForEmptyQueue();
        mStubbedTimeDetectorStrategy.verifyHandleAutoTimeDetectionToggleCalled();
    }

    private static PhoneTimeSuggestion createPhoneTimeSuggestion() {
@@ -147,7 +156,7 @@ public class TimeDetectorServiceTest {
        // Call tracking.
        private PhoneTimeSuggestion mLastPhoneSuggestion;
        private ManualTimeSuggestion mLastManualSuggestion;
        private Boolean mLastAutoTimeDetectionToggle;
        private boolean mLastAutoTimeDetectionToggleCalled;
        private boolean mDumpCalled;

        @Override
@@ -167,9 +176,9 @@ public class TimeDetectorServiceTest {
        }

        @Override
        public void handleAutoTimeDetectionToggle(boolean enabled) {
        public void handleAutoTimeDetectionChanged() {
            resetCallTracking();
            mLastAutoTimeDetectionToggle = enabled;
            mLastAutoTimeDetectionToggleCalled = true;
        }

        @Override
@@ -181,7 +190,7 @@ public class TimeDetectorServiceTest {
        void resetCallTracking() {
            mLastPhoneSuggestion = null;
            mLastManualSuggestion = null;
            mLastAutoTimeDetectionToggle = null;
            mLastAutoTimeDetectionToggleCalled = false;
            mDumpCalled = false;
        }

@@ -193,13 +202,45 @@ public class TimeDetectorServiceTest {
            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);
            }
        }
    }
}