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;
+ }
}
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));
+ }
}
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 com.android.internal.telephony.TelephonyIntents;
-import java.io.FileDescriptor;
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;
+ }
+
+ // 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;
}
- mHelper.setTime(timeSignal.getUtcTime());
+ 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 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 FileDescriptor fd, @NonNull PrintWriter pw, @Nullable String[] args) {
- // No state to dump.
+ 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);
+ }
}
}
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);
}
}
}
- private final Context mContext;
- private final TimeDetectorStrategy mTimeDetectorStrategy;
+ @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);
+ 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();
+ }
+ });
- private static TimeDetectorService create(Context context) {
- TimeDetectorStrategy timeDetector = new SimpleTimeDetectorStrategy();
- timeDetector.initialize(new TimeDetectorStrategyCallbackImpl(context));
- return new TimeDetectorService(context, timeDetector);
+ 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);
}
}
@Nullable String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
- mTimeDetectorStrategy.dump(fd, pw, args);
+ synchronized (mStrategyLock) {
+ mTimeDetectorStrategy.dump(pw, args);
+ }
}
private void enforceSetTimePermission() {
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.
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;
+
+ /**
+ * 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());
- public TimeDetectorStrategyCallbackImpl(Context context) {
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");
}
}
}
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;
@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);
+
+ TimeSignal timeSignal = scenario.createTimeSignalForActual("unknown");
+ mScript.simulateTimeSignalReceived(timeSignal)
+ .verifySystemClockWasNotSetAndResetCallTracking();
+ }
+
+ /**
+ * 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;
+ }
- verify(mMockCallback, never()).setTime(any());
+ Scenario build() {
+ return new Scenario(mInitialDeviceSystemClockMillis, mInitialDeviceRealtimeMillis,
+ mActualTimeMillis);
+ }
+ }
}
- private static TimestampedValue<Long> createUtcTime() {
- return new TimestampedValue<>(321L, 123456L);
+ 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();
}
}
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.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)
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);
+ }
+ }
}