diff --git a/core/java/android/util/TimestampedValue.java b/core/java/android/util/TimestampedValue.java index 75fa18db3908b..1289e4db07436 100644 --- a/core/java/android/util/TimestampedValue.java +++ b/core/java/android/util/TimestampedValue.java @@ -126,4 +126,12 @@ public final class TimestampedValue { dest.writeLong(timestampedValue.mReferenceTimeMillis); dest.writeValue(timestampedValue.mValue); } + + /** + * Returns the difference in milliseconds between two instance's reference times. + */ + public static long referenceTimeDifference( + @NonNull TimestampedValue one, @NonNull TimestampedValue two) { + return one.mReferenceTimeMillis - two.mReferenceTimeMillis; + } } diff --git a/core/tests/coretests/src/android/util/TimestampedValueTest.java b/core/tests/coretests/src/android/util/TimestampedValueTest.java index 7117c1b2d950b..03b4abd9b7a30 100644 --- a/core/tests/coretests/src/android/util/TimestampedValueTest.java +++ b/core/tests/coretests/src/android/util/TimestampedValueTest.java @@ -116,4 +116,14 @@ public class TimestampedValueTest { parcel.recycle(); } } + + @Test + public void testReferenceTimeDifference() { + TimestampedValue value1 = new TimestampedValue<>(1000, 123L); + assertEquals(0, TimestampedValue.referenceTimeDifference(value1, value1)); + + TimestampedValue value2 = new TimestampedValue<>(1, 321L); + assertEquals(999, TimestampedValue.referenceTimeDifference(value1, value2)); + assertEquals(-999, TimestampedValue.referenceTimeDifference(value2, value1)); + } } diff --git a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java index e5207cb81495c..7bdc8a32815aa 100644 --- a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java +++ b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java @@ -20,38 +20,213 @@ import android.annotation.NonNull; 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 mLastNitzTime; + + + // Information about the last time signal received: Used when toggling auto-time. + @Nullable private TimestampedValue mLastSystemClockTime; + private boolean mLastSystemClockTimeSendNetworkBroadcast; + + // System clock state. + @Nullable private TimestampedValue mLastSystemClockTimeSet; @Override public void initialize(@NonNull Callback callback) { - mHelper = callback; + mCallback = callback; } @Override public void suggestTime(@NonNull TimeSignal timeSignal) { if (!TimeSignal.SOURCE_ID_NITZ.equals(timeSignal.getSourceId())) { - Slog.w(TAG, "Ignoring signal from unknown source: " + timeSignal); + Slog.w(TAG, "Ignoring signal from unsupported source: " + timeSignal); return; } - mHelper.setTime(timeSignal.getUtcTime()); + // NITZ logic + + TimestampedValue 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 newUtcTime = newNitzUtcTime; + setSystemClockIfRequired(newUtcTime, sendNetworkBroadcast); + } + + private static boolean validateNewNitzTime(TimestampedValue newNitzUtcTime, + TimestampedValue 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 time, boolean sendNetworkBroadcast) { + + // Store the last candidate we've seen in all cases so we can set the system clock + // when/if time detection is enabled. + mLastSystemClockTime = time; + mLastSystemClockTimeSendNetworkBroadcast = sendNetworkBroadcast; + + if (!mCallback.isTimeDetectionEnabled()) { + Slog.d(TAG, "setSystemClockIfRequired: Time detection is not enabled. time=" + time); + return; + } + + mCallback.acquireWakeLock(); + try { + long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis(); + long actualTimeMillis = mCallback.systemClockMillis(); + + // CLOCK_PARANOIA : Check to see if this class owns the clock or if something else + // may be setting the clock. + if (mLastSystemClockTimeSet != null) { + long expectedTimeMillis = TimeDetectorStrategy.getTimeAt( + mLastSystemClockTimeSet, elapsedRealtimeMillis); + long absSystemClockDifference = Math.abs(expectedTimeMillis - actualTimeMillis); + if (absSystemClockDifference > SYSTEM_CLOCK_PARANOIA_THRESHOLD_MILLIS) { + Slog.w(TAG, "System clock has not tracked elapsed real time clock. A clock may" + + " be inaccurate or something unexpectedly set the system clock." + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + " expectedTimeMillis=" + expectedTimeMillis + + " actualTimeMillis=" + actualTimeMillis); + } + } + + final String reason = "New time signal"; + adjustAndSetDeviceSystemClock( + time, sendNetworkBroadcast, elapsedRealtimeMillis, actualTimeMillis, reason); + } finally { + mCallback.releaseWakeLock(); + } } @Override - public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @Nullable String[] args) { - // No state to dump. + public void handleAutoTimeDetectionToggle(boolean enabled) { + // If automatic time detection is enabled we update the system clock instantly if we can. + // Conversely, if automatic time detection is disabled we leave the clock as it is. + if (enabled) { + if (mLastSystemClockTime != null) { + // Only send the network broadcast if the last candidate would have caused one. + final boolean sendNetworkBroadcast = mLastSystemClockTimeSendNetworkBroadcast; + + mCallback.acquireWakeLock(); + try { + long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis(); + long actualTimeMillis = mCallback.systemClockMillis(); + + final String reason = "Automatic time detection enabled."; + adjustAndSetDeviceSystemClock(mLastSystemClockTime, sendNetworkBroadcast, + elapsedRealtimeMillis, actualTimeMillis, reason); + } finally { + mCallback.releaseWakeLock(); + } + } + } else { + // CLOCK_PARANOIA: We are losing "control" of the system clock so we cannot predict what + // it should be in future. + mLastSystemClockTimeSet = null; + } + } + + @Override + public void dump(@NonNull PrintWriter pw, @Nullable String[] args) { + pw.println("mLastNitzTime=" + mLastNitzTime); + pw.println("mLastSystemClockTimeSet=" + mLastSystemClockTimeSet); + pw.println("mLastSystemClockTime=" + mLastSystemClockTime); + pw.println("mLastSystemClockTimeSendNetworkBroadcast=" + + mLastSystemClockTimeSendNetworkBroadcast); + } + + private void adjustAndSetDeviceSystemClock( + TimestampedValue newTime, boolean sendNetworkBroadcast, + long elapsedRealtimeMillis, long actualSystemClockMillis, String reason) { + + // Adjust for the time that has elapsed since the signal was received. + long newSystemClockMillis = TimeDetectorStrategy.getTimeAt(newTime, elapsedRealtimeMillis); + + // Check if the new signal would make sufficient difference to the system clock. If it's + // below the threshold then ignore it. + long absTimeDifference = Math.abs(newSystemClockMillis - actualSystemClockMillis); + long systemClockUpdateThreshold = mCallback.systemClockUpdateThresholdMillis(); + if (absTimeDifference < systemClockUpdateThreshold) { + Slog.d(TAG, "adjustAndSetDeviceSystemClock: Not setting system clock. New time and" + + " system clock are close enough." + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + " newTime=" + newTime + + " reason=" + reason + + " systemClockUpdateThreshold=" + systemClockUpdateThreshold + + " absTimeDifference=" + absTimeDifference); + return; + } + + Slog.d(TAG, "Setting system clock using time=" + newTime + + " reason=" + reason + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + " newTimeMillis=" + newSystemClockMillis); + mCallback.setSystemClock(newSystemClockMillis); + + // CLOCK_PARANOIA : Record the last time this class set the system clock. + mLastSystemClockTimeSet = newTime; + + if (sendNetworkBroadcast) { + // Send a broadcast that telephony code used to send after setting the clock. + // TODO Remove this broadcast as soon as there are no remaining listeners. + Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra("time", newSystemClockMillis); + mCallback.sendStickyBroadcast(intent); + } } } diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java index 0ec24d8cfedb3..9c830003cab43 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorService.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorService.java @@ -20,24 +20,29 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.timedetector.ITimeDetectorService; import android.app.timedetector.TimeSignal; +import android.content.ContentResolver; import android.content.Context; +import android.database.ContentObserver; import android.os.Binder; +import android.provider.Settings; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; +import com.android.server.FgThread; import com.android.server.SystemService; +import com.android.server.timedetector.TimeDetectorStrategy.Callback; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Objects; public final class TimeDetectorService extends ITimeDetectorService.Stub { - private static final String TAG = "timedetector.TimeDetectorService"; public static class Lifecycle extends SystemService { - public Lifecycle(Context context) { + public Lifecycle(@NonNull Context context) { super(context); } @@ -51,31 +56,65 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub { } } - private final Context mContext; - private final TimeDetectorStrategy mTimeDetectorStrategy; + @NonNull private final Context mContext; + @NonNull private final Callback mCallback; - private static TimeDetectorService create(Context context) { - TimeDetectorStrategy timeDetector = new SimpleTimeDetectorStrategy(); - timeDetector.initialize(new TimeDetectorStrategyCallbackImpl(context)); - return new TimeDetectorService(context, timeDetector); + // The lock used when call the strategy to ensure thread safety. + @NonNull private final Object mStrategyLock = new Object(); + + @GuardedBy("mStrategyLock") + @NonNull private final TimeDetectorStrategy mTimeDetectorStrategy; + + private static TimeDetectorService create(@NonNull Context context) { + final TimeDetectorStrategy timeDetector = new SimpleTimeDetectorStrategy(); + final TimeDetectorStrategyCallbackImpl callback = + new TimeDetectorStrategyCallbackImpl(context); + timeDetector.initialize(callback); + + TimeDetectorService timeDetectorService = + new TimeDetectorService(context, callback, timeDetector); + + // Wire up event listening. + ContentResolver contentResolver = context.getContentResolver(); + contentResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true, + new ContentObserver(FgThread.getHandler()) { + public void onChange(boolean selfChange) { + timeDetectorService.handleAutoTimeDetectionToggle(); + } + }); + + return timeDetectorService; } @VisibleForTesting - public TimeDetectorService(@NonNull Context context, + public TimeDetectorService(@NonNull Context context, @NonNull Callback callback, @NonNull TimeDetectorStrategy timeDetectorStrategy) { mContext = Objects.requireNonNull(context); + mCallback = Objects.requireNonNull(callback); mTimeDetectorStrategy = Objects.requireNonNull(timeDetectorStrategy); } @Override public void suggestTime(@NonNull TimeSignal timeSignal) { enforceSetTimePermission(); + Objects.requireNonNull(timeSignal); - long callerIdToken = Binder.clearCallingIdentity(); + long idToken = Binder.clearCallingIdentity(); try { - mTimeDetectorStrategy.suggestTime(timeSignal); + synchronized (mStrategyLock) { + mTimeDetectorStrategy.suggestTime(timeSignal); + } } finally { - Binder.restoreCallingIdentity(callerIdToken); + Binder.restoreCallingIdentity(idToken); + } + } + + @VisibleForTesting + public void handleAutoTimeDetectionToggle() { + synchronized (mStrategyLock) { + final boolean timeDetectionEnabled = mCallback.isTimeDetectionEnabled(); + mTimeDetectorStrategy.handleAutoTimeDetectionToggle(timeDetectionEnabled); } } @@ -84,7 +123,9 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub { @Nullable String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; - mTimeDetectorStrategy.dump(fd, pw, args); + synchronized (mStrategyLock) { + mTimeDetectorStrategy.dump(pw, args); + } } private void enforceSetTimePermission() { diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java index 5cb2eed0932e8..e050865d96c74 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java @@ -19,26 +19,66 @@ package com.android.server.timedetector; 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 time); + + /** + * The absolute threshold below which the system clock need not be updated. i.e. if setting + * the system clock would adjust it by less than this (either backwards or forwards) then it + * need not be set. + */ + int systemClockUpdateThresholdMillis(); + + /** Returns true if automatic time detection is enabled. */ + boolean isTimeDetectionEnabled(); + + /** Acquire a suitable wake lock. Must be followed by {@link #releaseWakeLock()} */ + void acquireWakeLock(); + + /** Returns the elapsedRealtimeMillis clock value. The WakeLock must be held. */ + long elapsedRealtimeMillis(); + + /** Returns the system clock value. The WakeLock must be held. */ + long systemClockMillis(); + + /** Sets the device system clock. The WakeLock must be held. */ + void setSystemClock(long newTimeMillis); + + /** Release the wake lock acquired by a call to {@link #acquireWakeLock()}. */ + void releaseWakeLock(); + + /** Send the supplied intent as a stick broadcast. */ + void sendStickyBroadcast(@NonNull Intent intent); } + /** Initialize the strategy. */ void initialize(@NonNull Callback callback); + + /** Process the suggested time. */ void suggestTime(@NonNull TimeSignal timeSignal); - void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @Nullable String[] args); + + /** Handle the auto-time setting being toggled on or off. */ + void handleAutoTimeDetectionToggle(boolean enabled); + + /** Dump debug information. */ + void dump(@NonNull PrintWriter pw, @Nullable String[] args); // Utility methods below are to be moved to a better home when one becomes more obvious. diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyCallbackImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyCallbackImpl.java index 568d73aed50da..77b9e62810866 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyCallbackImpl.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyCallbackImpl.java @@ -18,41 +18,108 @@ package com.android.server.timedetector; 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 time) { - mWakeLock.acquire(); + public int systemClockUpdateThresholdMillis() { + return mSystemClockUpdateThresholdMillis; + } + + @Override + public boolean isTimeDetectionEnabled() { try { - long elapsedRealtimeMillis = SystemClock.elapsedRealtime(); - long currentTimeMillis = TimeDetectorStrategy.getTimeAt(time, elapsedRealtimeMillis); - Slog.d(TAG, "Setting system clock using time=" + time - + ", elapsedRealtimeMillis=" + elapsedRealtimeMillis); - mAlarmManager.setTime(currentTimeMillis); - } finally { - mWakeLock.release(); + return Settings.Global.getInt(mContentResolver, Settings.Global.AUTO_TIME) != 0; + } catch (Settings.SettingNotFoundException snfe) { + return true; + } + } + + @Override + public void acquireWakeLock() { + if (mWakeLock.isHeld()) { + Slog.wtf(TAG, "WakeLock " + mWakeLock + " already held"); + } + mWakeLock.acquire(); + } + + @Override + public long elapsedRealtimeMillis() { + checkWakeLockHeld(); + return SystemClock.elapsedRealtime(); + } + + @Override + public long systemClockMillis() { + checkWakeLockHeld(); + return System.currentTimeMillis(); + } + + @Override + public void setSystemClock(long newTimeMillis) { + checkWakeLockHeld(); + mAlarmManager.setTime(newTimeMillis); + } + + @Override + public void releaseWakeLock() { + checkWakeLockHeld(); + mWakeLock.release(); + } + + @Override + public void sendStickyBroadcast(@NonNull Intent intent) { + mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + } + + private void checkWakeLockHeld() { + if (!mWakeLock.isHeld()) { + Slog.wtf(TAG, "WakeLock " + mWakeLock + " not held"); } } } diff --git a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java index e4b3b131b4093..62f1433f7907b 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeZoneDetectorStrategyTest.java @@ -16,12 +16,18 @@ package com.android.server.timedetector; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import android.app.timedetector.TimeSignal; +import android.content.Intent; +import android.icu.util.Calendar; +import android.icu.util.GregorianCalendar; +import android.icu.util.TimeZone; import android.support.test.runner.AndroidJUnit4; import android.util.TimestampedValue; @@ -32,37 +38,476 @@ import org.junit.runner.RunWith; @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 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 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 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 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 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 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 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 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 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 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 utcTime = createUtcTime(); - TimeSignal timeSignal = new TimeSignal("unknown", utcTime); - mSimpleTimeZoneDetectorStrategy.suggestTime(timeSignal); + Scenario scenario = SCENARIO_1; + mScript.pokeFakeClocks(scenario) + .pokeTimeDetectionEnabled(true); - verify(mMockCallback, never()).setTime(any()); + TimeSignal timeSignal = scenario.createTimeSignalForActual("unknown"); + mScript.simulateTimeSignalReceived(timeSignal) + .verifySystemClockWasNotSetAndResetCallTracking(); } - private static TimestampedValue createUtcTime() { - return new TimestampedValue<>(321L, 123456L); + /** + * A fake implementation of TimeDetectorStrategy.Callback. Besides tracking changes and behaving + * like the real thing should, it also asserts preconditions. + */ + private static class FakeCallback implements TimeDetectorStrategy.Callback { + private boolean mTimeDetectionEnabled; + private boolean mWakeLockAcquired; + private long mElapsedRealtimeMillis; + private long mSystemClockMillis; + private int mSystemClockUpdateThresholdMillis = 2000; + + // Tracking operations. + private boolean mSystemClockWasSet; + private Intent mBroadcastSent; + + @Override + public int systemClockUpdateThresholdMillis() { + return mSystemClockUpdateThresholdMillis; + } + + @Override + public boolean isTimeDetectionEnabled() { + return mTimeDetectionEnabled; + } + + @Override + public void acquireWakeLock() { + if (mWakeLockAcquired) { + fail("Wake lock already acquired"); + } + mWakeLockAcquired = true; + } + + @Override + public long elapsedRealtimeMillis() { + assertWakeLockAcquired(); + return mElapsedRealtimeMillis; + } + + @Override + public long systemClockMillis() { + assertWakeLockAcquired(); + return mSystemClockMillis; + } + + @Override + public void setSystemClock(long newTimeMillis) { + assertWakeLockAcquired(); + mSystemClockWasSet = true; + mSystemClockMillis = newTimeMillis; + } + + @Override + public void releaseWakeLock() { + assertWakeLockAcquired(); + mWakeLockAcquired = false; + } + + @Override + public void sendStickyBroadcast(Intent intent) { + assertNotNull(intent); + mBroadcastSent = intent; + } + + // Methods below are for managing the fake's behavior. + + public void pokeSystemClockUpdateThreshold(int thresholdMillis) { + mSystemClockUpdateThresholdMillis = thresholdMillis; + } + + public void pokeElapsedRealtimeMillis(long elapsedRealtimeMillis) { + mElapsedRealtimeMillis = elapsedRealtimeMillis; + } + + public void pokeSystemClockMillis(long systemClockMillis) { + mSystemClockMillis = systemClockMillis; + } + + public void pokeTimeDetectionEnabled(boolean enabled) { + mTimeDetectionEnabled = enabled; + } + + public long peekElapsedRealtimeMillis() { + return mElapsedRealtimeMillis; + } + + public long peekSystemClockMillis() { + return mSystemClockMillis; + } + + public void simulateTimePassing(int incrementMillis) { + mElapsedRealtimeMillis += incrementMillis; + mSystemClockMillis += incrementMillis; + } + + public void verifySystemClockNotSet() { + assertFalse(mSystemClockWasSet); + } + + public void verifySystemClockWasSet(long expectSystemClockMillis) { + assertTrue(mSystemClockWasSet); + assertEquals(expectSystemClockMillis, mSystemClockMillis); + } + + public void verifyIntentWasBroadcast() { + assertTrue(mBroadcastSent != null); + } + + public void verifyIntentWasNotBroadcast() { + assertNull(mBroadcastSent); + } + + public void resetCallTracking() { + mSystemClockWasSet = false; + mBroadcastSent = null; + } + + private void assertWakeLockAcquired() { + assertTrue("The operation must be performed only after acquiring the wakelock", + mWakeLockAcquired); + } + } + + /** + * A fluent helper class for tests. + */ + private class Script { + + private final FakeCallback mFakeCallback; + private final SimpleTimeDetectorStrategy mSimpleTimeDetectorStrategy; + + public Script() { + mFakeCallback = new FakeCallback(); + mSimpleTimeDetectorStrategy = new SimpleTimeDetectorStrategy(); + mSimpleTimeDetectorStrategy.initialize(mFakeCallback); + + } + + Script pokeTimeDetectionEnabled(boolean enabled) { + mFakeCallback.pokeTimeDetectionEnabled(enabled); + return this; + } + + Script pokeFakeClocks(Scenario scenario) { + mFakeCallback.pokeElapsedRealtimeMillis(scenario.getInitialRealTimeMillis()); + mFakeCallback.pokeSystemClockMillis(scenario.getInitialSystemClockMillis()); + return this; + } + + Script pokeThresholds(int systemClockUpdateThreshold) { + mFakeCallback.pokeSystemClockUpdateThreshold(systemClockUpdateThreshold); + return this; + } + + long peekElapsedRealtimeMillis() { + return mFakeCallback.peekElapsedRealtimeMillis(); + } + + long peekSystemClockMillis() { + return mFakeCallback.peekSystemClockMillis(); + } + + Script simulateTimeSignalReceived(TimeSignal timeSignal) { + mSimpleTimeDetectorStrategy.suggestTime(timeSignal); + return this; + } + + Script simulateAutoTimeDetectionToggle() { + boolean enabled = !mFakeCallback.isTimeDetectionEnabled(); + mFakeCallback.pokeTimeDetectionEnabled(enabled); + mSimpleTimeDetectorStrategy.handleAutoTimeDetectionToggle(enabled); + return this; + } + + Script simulateTimePassing(int clockIncrement) { + mFakeCallback.simulateTimePassing(clockIncrement); + return this; + } + + Script verifySystemClockWasNotSetAndResetCallTracking() { + mFakeCallback.verifySystemClockNotSet(); + mFakeCallback.verifyIntentWasNotBroadcast(); + mFakeCallback.resetCallTracking(); + return this; + } + + Script verifySystemClockWasSetAndResetCallTracking(long expectSystemClockMillis) { + mFakeCallback.verifySystemClockWasSet(expectSystemClockMillis); + mFakeCallback.verifyIntentWasBroadcast(); + mFakeCallback.resetCallTracking(); + return this; + } + } + + /** + * A starting scenario used during tests. Describes a fictional "physical" reality. + */ + private static class Scenario { + + private final long mInitialDeviceSystemClockMillis; + private final long mInitialDeviceRealtimeMillis; + private final long mActualTimeMillis; + + Scenario(long initialDeviceSystemClock, long elapsedRealtime, long timeMillis) { + mInitialDeviceSystemClockMillis = initialDeviceSystemClock; + mActualTimeMillis = timeMillis; + mInitialDeviceRealtimeMillis = elapsedRealtime; + } + + long getInitialRealTimeMillis() { + return mInitialDeviceRealtimeMillis; + } + + long getInitialSystemClockMillis() { + return mInitialDeviceSystemClockMillis; + } + + long getActualTimeMillis() { + return mActualTimeMillis; + } + + TimeSignal createTimeSignalForActual(String sourceId) { + TimestampedValue time = new TimestampedValue<>( + mInitialDeviceRealtimeMillis, mActualTimeMillis); + return new TimeSignal(sourceId, time); + } + + static class Builder { + + private long mInitialDeviceSystemClockMillis; + private long mInitialDeviceRealtimeMillis; + private long mActualTimeMillis; + + Builder setInitialDeviceSystemClockUtc(int year, int monthInYear, int day, + int hourOfDay, int minute, int second) { + mInitialDeviceSystemClockMillis = createUtcTime(year, monthInYear, day, hourOfDay, + minute, second); + return this; + } + + Builder setInitialDeviceRealtimeMillis(long realtimeMillis) { + mInitialDeviceRealtimeMillis = realtimeMillis; + return this; + } + + Builder setActualTimeUtc(int year, int monthInYear, int day, int hourOfDay, + int minute, int second) { + mActualTimeMillis = + createUtcTime(year, monthInYear, day, hourOfDay, minute, second); + return this; + } + + Scenario build() { + return new Scenario(mInitialDeviceSystemClockMillis, mInitialDeviceRealtimeMillis, + mActualTimeMillis); + } + } + } + + private static long createUtcTime(int year, int monthInYear, int day, int hourOfDay, int minute, + int second) { + Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("Etc/UTC")); + cal.clear(); + cal.set(year, monthInYear - 1, day, hourOfDay, minute, second); + return cal.getTimeInMillis(); } } diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java index 22dea92cc0b23..ed74cd7b3e53d 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java @@ -16,6 +16,9 @@ package com.android.server.timedetector; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -23,36 +26,40 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import android.app.timedetector.TimeSignal; import android.content.Context; +import android.content.pm.PackageManager; import android.support.test.runner.AndroidJUnit4; import android.util.TimestampedValue; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import com.android.server.timedetector.TimeDetectorStrategy.Callback; + +import java.io.PrintWriter; + @RunWith(AndroidJUnit4.class) public class TimeDetectorServiceTest { - private TimeDetectorService mTimeDetectorService; - private Context mMockContext; - private TimeDetectorStrategy mMockTimeDetectorStrategy; + private StubbedTimeDetectorStrategy mStubbedTimeDetectorStrategy; + private Callback mMockCallback; + + private TimeDetectorService mTimeDetectorService; @Before public void setUp() { mMockContext = mock(Context.class); - mMockTimeDetectorStrategy = mock(TimeDetectorStrategy.class); - mTimeDetectorService = new TimeDetectorService(mMockContext, mMockTimeDetectorStrategy); - } + mMockCallback = mock(Callback.class); + mStubbedTimeDetectorStrategy = new StubbedTimeDetectorStrategy(); - @After - public void tearDown() { - verifyNoMoreInteractions(mMockContext, mMockTimeDetectorStrategy); + mTimeDetectorService = new TimeDetectorService( + mMockContext, mMockCallback, + mStubbedTimeDetectorStrategy); } @Test(expected=SecurityException.class) @@ -78,11 +85,86 @@ public class TimeDetectorServiceTest { 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 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); + } + } }