From 312da9b9fcb6593c52d8782552a7d050675fe534 Mon Sep 17 00:00:00 2001 From: Neil Fuller Date: Tue, 26 Nov 2019 13:07:09 +0000 Subject: [PATCH] Handle multiple phoneIds in time detection Android has dual sim devices, which means multiple "phone" time signals. NITZ is generally trusted and considered "good enough" in the absence of better signals, but it is provided by carriers and it's not unusual for the signals to be incorrect by minutes. We don't want the device system clock just flicking to the latest signal received. The changes to the SimpleTimeDetectorStrategy try to balance recency and provide some consistency / determinism. See comments in the code for details. The time zone detection works in a similar way, particularly with respect to choosing the lowest phoneId in the event of a tie. There will be a follow-up change to remove the word "Simple" from SimpleTimeDetectorStrategy as it no longer applies. Test: atest services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java Test: atest android.app.timedetector Bug: 140712361 Change-Id: I228aff8709eabfcec910af22f7ab08fee32d566a --- .../timedetector/ManualTimeSuggestion.java | 1 + .../app/timedetector/PhoneTimeSuggestion.java | 7 +- .../SimpleTimeDetectorStrategy.java | 484 +++++++++++----- .../SimpleTimeDetectorStrategyTest.java | 521 +++++++++++------- 4 files changed, 676 insertions(+), 337 deletions(-) diff --git a/core/java/android/app/timedetector/ManualTimeSuggestion.java b/core/java/android/app/timedetector/ManualTimeSuggestion.java index 471606da4d755..55f92be14cd0d 100644 --- a/core/java/android/app/timedetector/ManualTimeSuggestion.java +++ b/core/java/android/app/timedetector/ManualTimeSuggestion.java @@ -56,6 +56,7 @@ public final class ManualTimeSuggestion implements Parcelable { public ManualTimeSuggestion(@NonNull TimestampedValue utcTime) { mUtcTime = Objects.requireNonNull(utcTime); + Objects.requireNonNull(utcTime.getValue()); } private static ManualTimeSuggestion createFromParcel(Parcel in) { diff --git a/core/java/android/app/timedetector/PhoneTimeSuggestion.java b/core/java/android/app/timedetector/PhoneTimeSuggestion.java index dd02af7a3ac7b..4a89a12454736 100644 --- a/core/java/android/app/timedetector/PhoneTimeSuggestion.java +++ b/core/java/android/app/timedetector/PhoneTimeSuggestion.java @@ -166,7 +166,12 @@ public final class PhoneTimeSuggestion implements Parcelable { } /** Returns the builder for call chaining. */ - public Builder setUtcTime(TimestampedValue utcTime) { + public Builder setUtcTime(@Nullable TimestampedValue utcTime) { + if (utcTime != null) { + // utcTime can be null, but the value it holds cannot. + Objects.requireNonNull(utcTime.getValue()); + } + mUtcTime = utcTime; return this; } diff --git a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java index 4e8ba078a73cc..5a31bbc859c8c 100644 --- a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java +++ b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java @@ -23,21 +23,26 @@ import android.app.AlarmManager; import android.app.timedetector.ManualTimeSuggestion; import android.app.timedetector.PhoneTimeSuggestion; import android.content.Intent; +import android.util.ArrayMap; import android.util.LocalLog; import android.util.Slog; import android.util.TimestampedValue; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.util.IndentingPrintWriter; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.LinkedList; +import java.util.Map; /** - * An implementation of TimeDetectorStrategy that passes only NITZ suggestions to - * {@link AlarmManager}. + * An implementation of TimeDetectorStrategy that passes phone and manual suggestions to + * {@link AlarmManager}. When there are multiple phone sources, the one with the lowest ID is used + * unless the data becomes too stale. * *

Most public methods are marked synchronized to ensure thread safety around internal state. */ @@ -46,6 +51,17 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { private static final boolean DBG = false; private static final String LOG_TAG = "SimpleTimeDetectorStrategy"; + /** A score value used to indicate "no score", either due to validation failure or age. */ + private static final int PHONE_INVALID_SCORE = -1; + /** The number of buckets phone suggestions can be put in by age. */ + private static final int PHONE_BUCKET_COUNT = 24; + /** Each bucket is this size. All buckets are equally sized. */ + @VisibleForTesting + static final int PHONE_BUCKET_SIZE_MILLIS = 60 * 60 * 1000; + /** Phone suggestions older than this value are considered too old. */ + @VisibleForTesting + static final long PHONE_MAX_AGE_MILLIS = PHONE_BUCKET_COUNT * PHONE_BUCKET_SIZE_MILLIS; + @IntDef({ ORIGIN_PHONE, ORIGIN_MANUAL }) @Retention(RetentionPolicy.SOURCE) public @interface Origin {} @@ -61,10 +77,13 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { /** * 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 automatically. + * there is something other than this class setting the system clock. */ private static final long SYSTEM_CLOCK_PARANOIA_THRESHOLD_MILLIS = 2 * 1000; + /** The number of previous phone suggestions to keep for each ID (for use during debugging). */ + private static final int KEEP_SUGGESTION_HISTORY_SIZE = 30; + // A log for changes made to the system clock and why. @NonNull private final LocalLog mTimeChangesLog = new LocalLog(30, false /* useLocalTimestamps */); @@ -72,15 +91,22 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { // @NonNull after initialize() private Callback mCallback; - // Last phone suggestion. - @Nullable private PhoneTimeSuggestion mLastPhoneSuggestion; + // Used to store the last time the system clock state was set automatically. It is used to + // detect (and log) issues with the realtime clock or whether the clock is being set without + // going through this strategy code. + @GuardedBy("this") + @Nullable + private TimestampedValue mLastAutoSystemClockTimeSet; - // Information about the last time signal received: Used when toggling auto-time. - @Nullable private TimestampedValue mLastAutoSystemClockTime; - private boolean mLastAutoSystemClockTimeSendNetworkBroadcast; - - // System clock state. - @Nullable private TimestampedValue mLastAutoSystemClockTimeSet; + /** + * A mapping from phoneId to a linked list of time suggestions (the "first" being the latest). + * We typically expect one or two entries in this Map: devices will have a small number + * of telephony devices and phoneIds are assumed to be stable. The LinkedList associated with + * the ID will not exceed {@link #KEEP_SUGGESTION_HISTORY_SIZE} in size. + */ + @GuardedBy("this") + private ArrayMap> mSuggestionByPhoneId = + new ArrayMap<>(); @Override public void initialize(@NonNull Callback callback) { @@ -88,66 +114,297 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { } @Override - public synchronized void suggestPhoneTime(@NonNull PhoneTimeSuggestion timeSuggestion) { - // NITZ logic + public synchronized void suggestManualTime(ManualTimeSuggestion suggestion) { + final TimestampedValue newUtcTime = suggestion.getUtcTime(); - // Empty suggestions are just ignored as we don't currently keep track of suggestion origin. + // We can validate the suggestion against the reference time clock. + long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis(); + if (elapsedRealtimeMillis < newUtcTime.getReferenceTimeMillis()) { + // elapsedRealtime clock went backwards? + Slog.w(LOG_TAG, "New reference time is in the future? Ignoring." + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + ", timeSuggestion=" + suggestion); + return; + } + + String cause = "Manual time suggestion received: suggestion=" + suggestion; + setSystemClockIfRequired(ORIGIN_MANUAL, newUtcTime, cause); + } + + @Override + public synchronized void suggestPhoneTime(@NonNull PhoneTimeSuggestion timeSuggestion) { + // Empty time suggestion means that telephony network connectivity has been lost. + // The passage of time is relentless, and we don't expect our users to use a time machine, + // so we can continue relying on previous suggestions when we lose connectivity. This is + // unlike time zone, where a user may lose connectivity when boarding a flight and where we + // do want to "forget" old signals. Suggestions that are too old are discarded later in the + // detection algorithm. if (timeSuggestion.getUtcTime() == null) { return; } - boolean timeSuggestionIsValid = - validateNewPhoneSuggestion(timeSuggestion, mLastPhoneSuggestion); - if (!timeSuggestionIsValid) { + // Perform validation / input filtering and record the validated suggestion against the + // phoneId. + if (!validateAndStorePhoneSuggestion(timeSuggestion)) { 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 phone suggestions. - mLastPhoneSuggestion = timeSuggestion; - // System clock update logic. - final TimestampedValue newUtcTime = timeSuggestion.getUtcTime(); - setSystemClockIfRequired(ORIGIN_PHONE, newUtcTime, timeSuggestion); + // Now perform auto time detection. The new suggestion may be used to modify the system + // clock. + String reason = "New phone time suggested. timeSuggestion=" + timeSuggestion; + doAutoTimeDetection(reason); } @Override - public synchronized void suggestManualTime(ManualTimeSuggestion timeSuggestion) { - final TimestampedValue newUtcTime = timeSuggestion.getUtcTime(); - setSystemClockIfRequired(ORIGIN_MANUAL, newUtcTime, timeSuggestion); + public synchronized void handleAutoTimeDetectionChanged() { + boolean enabled = mCallback.isAutoTimeDetectionEnabled(); + // When automatic time detection is enabled we update the system clock instantly if we can. + // Conversely, when automatic time detection is disabled we leave the clock as it is. + if (enabled) { + String reason = "Auto time zone detection setting enabled."; + doAutoTimeDetection(reason); + } else { + // CLOCK_PARANOIA: We are losing "control" of the system clock so we cannot predict what + // it should be in future. + mLastAutoSystemClockTimeSet = null; + } } - private static boolean validateNewPhoneSuggestion(@NonNull PhoneTimeSuggestion newSuggestion, - @Nullable PhoneTimeSuggestion lastSuggestion) { + @Override + public synchronized void dump(@NonNull PrintWriter pw, @Nullable String[] args) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + ipw.println("TimeDetectorStrategy:"); + ipw.increaseIndent(); // level 1 - if (lastSuggestion != null) { - long referenceTimeDifference = TimestampedValue.referenceTimeDifference( - newSuggestion.getUtcTime(), lastSuggestion.getUtcTime()); - if (referenceTimeDifference < 0 || referenceTimeDifference > Integer.MAX_VALUE) { - // Out of order or bogus. - Slog.w(LOG_TAG, "Bad NITZ signal received." - + " referenceTimeDifference=" + referenceTimeDifference - + " lastSuggestion=" + lastSuggestion - + " newSuggestion=" + newSuggestion); + ipw.println("mLastAutoSystemClockTimeSet=" + mLastAutoSystemClockTimeSet); + + ipw.println("Time change log:"); + ipw.increaseIndent(); // level 2 + mTimeChangesLog.dump(ipw); + ipw.decreaseIndent(); // level 2 + + ipw.println("Phone suggestion history:"); + ipw.increaseIndent(); // level 2 + for (Map.Entry> entry + : mSuggestionByPhoneId.entrySet()) { + ipw.println("Phone " + entry.getKey()); + + ipw.increaseIndent(); // level 3 + for (PhoneTimeSuggestion suggestion : entry.getValue()) { + ipw.println(suggestion); + } + ipw.decreaseIndent(); // level 3 + } + ipw.decreaseIndent(); // level 2 + + ipw.decreaseIndent(); // level 1 + ipw.flush(); + } + + @GuardedBy("this") + private boolean validateAndStorePhoneSuggestion(@NonNull PhoneTimeSuggestion timeSuggestion) { + if (timeSuggestion.getUtcTime().getValue() == null) { + Slog.w(LOG_TAG, "Suggestion utcTime contains null value" + + " timeSuggestion=" + timeSuggestion); + return false; + } + + int phoneId = timeSuggestion.getPhoneId(); + LinkedList phoneSuggestions = mSuggestionByPhoneId.get(phoneId); + if (phoneSuggestions == null) { + // The first time we've seen this phoneId. + phoneSuggestions = new LinkedList<>(); + mSuggestionByPhoneId.put(phoneId, phoneSuggestions); + } else if (phoneSuggestions.isEmpty()) { + Slog.w(LOG_TAG, "Suggestions unexpectedly empty when adding" + + " timeSuggestion=" + timeSuggestion); + } + + if (!phoneSuggestions.isEmpty()) { + PhoneTimeSuggestion previousSuggestion = phoneSuggestions.getFirst(); + + // We can log / discard suggestions with obvious issues with the reference time clock. + long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis(); + TimestampedValue newTime = timeSuggestion.getUtcTime(); + if (elapsedRealtimeMillis < newTime.getReferenceTimeMillis()) { + // elapsedRealtime clock went backwards? + Slog.w(LOG_TAG, "New reference time is in the future?" + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + ", timeSuggestion=" + timeSuggestion); + // There's probably nothing useful we can do: elsewhere we assume that reference + // times are in the past so just stop here. return false; } + + if (previousSuggestion.getUtcTime() != null) { + long referenceTimeDifference = TimestampedValue.referenceTimeDifference( + timeSuggestion.getUtcTime(), previousSuggestion.getUtcTime()); + if (referenceTimeDifference < 0) { + // The reference time is before the previously received suggestion. Ignore it. + Slog.w(LOG_TAG, "Out of order phone suggestion received." + + " referenceTimeDifference=" + referenceTimeDifference + + " lastSuggestion=" + previousSuggestion + + " newSuggestion=" + timeSuggestion); + return false; + } + } + } + + // Store the latest suggestion. + phoneSuggestions.addFirst(timeSuggestion); + if (phoneSuggestions.size() > KEEP_SUGGESTION_HISTORY_SIZE) { + phoneSuggestions.removeLast(); } return true; } + @GuardedBy("this") + private void doAutoTimeDetection(@NonNull String detectionReason) { + if (!mCallback.isAutoTimeDetectionEnabled()) { + // Avoid doing unnecessary work with this (race-prone) check. + return; + } + + PhoneTimeSuggestion bestPhoneSuggestion = findBestPhoneSuggestion(); + + // Work out what to do with the best suggestion. + if (bestPhoneSuggestion == null) { + // There is no good phone suggestion. + if (DBG) { + Slog.d(LOG_TAG, "Could not determine time: No best phone suggestion." + + " detectionReason=" + detectionReason); + } + return; + } + + final TimestampedValue newUtcTime = bestPhoneSuggestion.getUtcTime(); + String cause = "Found good suggestion." + + ", bestPhoneSuggestion=" + bestPhoneSuggestion + + ", detectionReason=" + detectionReason; + setSystemClockIfRequired(ORIGIN_PHONE, newUtcTime, cause); + } + + @GuardedBy("this") + @Nullable + private PhoneTimeSuggestion findBestPhoneSuggestion() { + long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis(); + + // Phone time suggestions are assumed to be derived from NITZ or NITZ-like signals. These + // have a number of limitations: + // 1) No guarantee of accuracy ("accuracy of the time information is in the order of + // minutes") [1] + // 2) No guarantee of regular signals ("dependent on the handset crossing radio network + // boundaries") [1] + // + // [1] https://en.wikipedia.org/wiki/NITZ + // + // Generally, when there are suggestions from multiple phoneIds they should usually + // approximately agree. In cases where signals *are* inaccurate we don't want to vacillate + // between signals from two phoneIds. However, it is known for NITZ signals to be incorrect + // occasionally, which means we also don't want to stick forever with one phoneId. Without + // cross-referencing across sources (e.g. the current device time, NTP), or doing some kind + // of statistical analysis of consistency within and across phoneIds, we can't know which + // suggestions are more correct. + // + // For simplicity, we try to value recency, then consistency of phoneId. + // + // The heuristic works as follows: + // Recency: The most recent suggestion from each phone is scored. The score is based on a + // discrete age bucket, i.e. so signals received around the same time will be in the same + // bucket, thus applying a loose reference time ordering. The suggestion with the highest + // score is used. + // Consistency: If there a multiple suggestions with the same score, the suggestion with the + // lowest phoneId is always taken. + // + // In the trivial case with a single ID this will just mean that the latest received + // suggestion is used. + + PhoneTimeSuggestion bestSuggestion = null; + int bestScore = PHONE_INVALID_SCORE; + for (int i = 0; i < mSuggestionByPhoneId.size(); i++) { + Integer phoneId = mSuggestionByPhoneId.keyAt(i); + LinkedList phoneSuggestions = mSuggestionByPhoneId.valueAt(i); + if (phoneSuggestions == null) { + // Unexpected - map is missing a value. + Slog.w(LOG_TAG, "Suggestions unexpectedly missing for phoneId." + + " phoneId=" + phoneId); + continue; + } + + PhoneTimeSuggestion candidateSuggestion = phoneSuggestions.getFirst(); + if (candidateSuggestion == null) { + // Unexpected - null suggestions should never be stored. + Slog.w(LOG_TAG, "Latest suggestion unexpectedly null for phoneId." + + " phoneId=" + phoneId); + continue; + } else if (candidateSuggestion.getUtcTime() == null) { + // Unexpected - we do not store empty suggestions. + Slog.w(LOG_TAG, "Latest suggestion unexpectedly empty. " + + " candidateSuggestion=" + candidateSuggestion); + continue; + } + + int candidateScore = scorePhoneSuggestion(elapsedRealtimeMillis, candidateSuggestion); + if (candidateScore == PHONE_INVALID_SCORE) { + // Expected: This means the suggestion is obviously invalid or just too old. + continue; + } + + // Higher scores are better. + if (bestSuggestion == null || bestScore < candidateScore) { + bestSuggestion = candidateSuggestion; + bestScore = candidateScore; + } else if (bestScore == candidateScore) { + // Tie! Use the suggestion with the lowest phoneId. + int candidatePhoneId = candidateSuggestion.getPhoneId(); + int bestPhoneId = bestSuggestion.getPhoneId(); + if (candidatePhoneId < bestPhoneId) { + bestSuggestion = candidateSuggestion; + } + } + } + return bestSuggestion; + } + + private static int scorePhoneSuggestion( + long elapsedRealtimeMillis, @NonNull PhoneTimeSuggestion timeSuggestion) { + // The score is based on the age since receipt. Suggestions are bucketed so two + // suggestions in the same bucket from different phoneIds are scored the same. + TimestampedValue utcTime = timeSuggestion.getUtcTime(); + long referenceTimeMillis = utcTime.getReferenceTimeMillis(); + if (referenceTimeMillis > elapsedRealtimeMillis) { + // Future times are ignored. They imply the reference time was wrong, or the elapsed + // realtime clock has gone backwards, neither of which are supportable situations. + Slog.w(LOG_TAG, "Existing suggestion found to be in the future. " + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + ", timeSuggestion=" + timeSuggestion); + return PHONE_INVALID_SCORE; + } + + long ageMillis = elapsedRealtimeMillis - referenceTimeMillis; + + // Any suggestion > MAX_AGE_MILLIS is treated as too old. Although time is relentless and + // predictable, the accuracy of the reference time clock may be poor over long periods which + // would lead to errors creeping in. Also, in edge cases where a bad suggestion has been + // made and never replaced, it could also mean that the time detection code remains + // opinionated using a bad invalid suggestion. This caps that edge case at MAX_AGE_MILLIS. + if (ageMillis > PHONE_MAX_AGE_MILLIS) { + return PHONE_INVALID_SCORE; + } + + // Turn the age into a discrete value: 0 <= bucketIndex < MAX_AGE_HOURS. + int bucketIndex = (int) (ageMillis / PHONE_BUCKET_SIZE_MILLIS); + + // We want the lowest bucket index to have the highest score. 0 > score >= BUCKET_COUNT. + return PHONE_BUCKET_COUNT - bucketIndex; + } + @GuardedBy("this") private void setSystemClockIfRequired( - @Origin int origin, TimestampedValue time, Object cause) { - // Historically, Android has sent a TelephonyIntents.ACTION_NETWORK_SET_TIME broadcast only - // when setting the time using NITZ. - boolean sendNetworkBroadcast = origin == ORIGIN_PHONE; + @Origin int origin, @NonNull TimestampedValue time, @NonNull String cause) { boolean isOriginAutomatic = isOriginAutomatic(origin); if (isOriginAutomatic) { - // Store the last auto time candidate we've seen in all cases so we can set the system - // clock when/if time detection is off but later enabled. - mLastAutoSystemClockTime = time; - mLastAutoSystemClockTimeSendNetworkBroadcast = sendNetworkBroadcast; - if (!mCallback.isAutoTimeDetectionEnabled()) { if (DBG) { Slog.d(LOG_TAG, "Auto time detection is not enabled." @@ -171,30 +428,7 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { mCallback.acquireWakeLock(); try { - long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis(); - long actualTimeMillis = mCallback.systemClockMillis(); - - if (isOriginAutomatic) { - // CLOCK_PARANOIA : Check to see if this class owns the clock or if something else - // may be setting the clock. - if (mLastAutoSystemClockTimeSet != null) { - long expectedTimeMillis = TimeDetectorStrategy.getTimeAt( - mLastAutoSystemClockTimeSet, elapsedRealtimeMillis); - long absSystemClockDifference = Math.abs(expectedTimeMillis - actualTimeMillis); - if (absSystemClockDifference > SYSTEM_CLOCK_PARANOIA_THRESHOLD_MILLIS) { - 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." - + " elapsedRealtimeMillis=" + elapsedRealtimeMillis - + " expectedTimeMillis=" + expectedTimeMillis - + " actualTimeMillis=" + actualTimeMillis); - } - } - } - - adjustAndSetDeviceSystemClock( - time, sendNetworkBroadcast, elapsedRealtimeMillis, actualTimeMillis, cause); + setSystemClockUnderWakeLock(origin, time, cause); } finally { mCallback.releaseWakeLock(); } @@ -204,61 +438,33 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { return origin == ORIGIN_PHONE; } - @Override - 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. - final boolean sendNetworkBroadcast = mLastAutoSystemClockTimeSendNetworkBroadcast; + @GuardedBy("this") + private void setSystemClockUnderWakeLock( + int origin, @NonNull TimestampedValue newTime, @NonNull Object cause) { - mCallback.acquireWakeLock(); - try { - long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis(); - long actualTimeMillis = mCallback.systemClockMillis(); - - final String reason = "Automatic time detection enabled."; - adjustAndSetDeviceSystemClock(mLastAutoSystemClockTime, sendNetworkBroadcast, - elapsedRealtimeMillis, actualTimeMillis, reason); - } finally { - mCallback.releaseWakeLock(); + long elapsedRealtimeMillis = mCallback.elapsedRealtimeMillis(); + boolean isOriginAutomatic = isOriginAutomatic(origin); + long actualSystemClockMillis = mCallback.systemClockMillis(); + if (isOriginAutomatic) { + // CLOCK_PARANOIA : Check to see if this class owns the clock or if something else + // may be setting the clock. + if (mLastAutoSystemClockTimeSet != null) { + long expectedTimeMillis = TimeDetectorStrategy.getTimeAt( + mLastAutoSystemClockTimeSet, elapsedRealtimeMillis); + long absSystemClockDifference = + Math.abs(expectedTimeMillis - actualSystemClockMillis); + if (absSystemClockDifference > SYSTEM_CLOCK_PARANOIA_THRESHOLD_MILLIS) { + 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." + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + " expectedTimeMillis=" + expectedTimeMillis + + " actualTimeMillis=" + actualSystemClockMillis + + " cause=" + cause); } } - } else { - // CLOCK_PARANOIA: We are losing "control" of the system clock so we cannot predict what - // it should be in future. - mLastAutoSystemClockTimeSet = null; } - } - - @Override - public synchronized void dump(@NonNull PrintWriter pw, @Nullable String[] args) { - IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); - ipw.println("TimeDetectorStrategy:"); - ipw.increaseIndent(); // level 1 - - ipw.println("mLastPhoneSuggestion=" + mLastPhoneSuggestion); - ipw.println("mLastAutoSystemClockTimeSet=" + mLastAutoSystemClockTimeSet); - ipw.println("mLastAutoSystemClockTime=" + mLastAutoSystemClockTime); - ipw.println("mLastAutoSystemClockTimeSendNetworkBroadcast=" - + mLastAutoSystemClockTimeSendNetworkBroadcast); - - - ipw.println("Time change log:"); - ipw.increaseIndent(); // level 2 - mTimeChangesLog.dump(ipw); - ipw.decreaseIndent(); // level 2 - - ipw.decreaseIndent(); // level 1 - ipw.flush(); - } - - @GuardedBy("this") - private void adjustAndSetDeviceSystemClock( - TimestampedValue newTime, boolean sendNetworkBroadcast, - long elapsedRealtimeMillis, long actualSystemClockMillis, Object cause) { // Adjust for the time that has elapsed since the signal was received. long newSystemClockMillis = TimeDetectorStrategy.getTimeAt(newTime, elapsedRealtimeMillis); @@ -290,10 +496,17 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { } mTimeChangesLog.log(logMsg); - // CLOCK_PARANOIA : Record the last time this class set the system clock. - mLastAutoSystemClockTimeSet = newTime; + // CLOCK_PARANOIA : Record the last time this class set the system clock due to an auto-time + // signal, or clear the record it is being done manually. + if (isOriginAutomatic(origin)) { + mLastAutoSystemClockTimeSet = newTime; + } else { + mLastAutoSystemClockTimeSet = null; + } - if (sendNetworkBroadcast) { + // Historically, Android has sent a TelephonyIntents.ACTION_NETWORK_SET_TIME broadcast only + // when setting the time using NITZ. + if (origin == ORIGIN_PHONE) { // 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); @@ -302,4 +515,27 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { mCallback.sendStickyBroadcast(intent); } } + + /** + * Returns the current best phone suggestion. Not intended for general use: it is used during + * tests to check strategy behavior. + */ + @VisibleForTesting + @Nullable + public synchronized PhoneTimeSuggestion findBestPhoneSuggestionForTests() { + return findBestPhoneSuggestion(); + } + + /** + * A method used to inspect state during tests. Not intended for general use. + */ + @VisibleForTesting + @Nullable + public synchronized PhoneTimeSuggestion getLatestPhoneSuggestion(int phoneId) { + LinkedList suggestions = mSuggestionByPhoneId.get(phoneId); + if (suggestions == null) { + return null; + } + return suggestions.getFirst(); + } } diff --git a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java index 7a0a28dfbd16c..52e7d81978421 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java @@ -42,11 +42,12 @@ import java.time.Duration; @RunWith(AndroidJUnit4.class) public class SimpleTimeDetectorStrategyTest { - 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 static final TimestampedValue ARBITRARY_CLOCK_INITIALIZATION_INFO = + new TimestampedValue<>( + 123456789L /* realtimeClockMillis */, + createUtcTime(1977, 1, 1, 12, 0, 0)); + + private static final long ARBITRARY_TEST_TIME_MILLIS = createUtcTime(2018, 1, 1, 12, 0, 0); private static final int ARBITRARY_PHONE_ID = 123456; @@ -61,116 +62,211 @@ public class SimpleTimeDetectorStrategyTest { @Test public void testSuggestPhoneTime_autoTimeEnabled() { - Scenario scenario = SCENARIO_1; - mScript.pokeFakeClocks(scenario) - .pokeTimeDetectionEnabled(true); + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true); + int phoneId = ARBITRARY_PHONE_ID; + long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS; PhoneTimeSuggestion timeSuggestion = - scenario.createPhoneTimeSuggestionForActual(ARBITRARY_PHONE_ID); - final int clockIncrement = 1000; - long expectSystemClockMillis = scenario.getActualTimeMillis() + clockIncrement; + mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis); + int clockIncrement = 1000; + long expectedSystemClockMillis = testTimeMillis + clockIncrement; mScript.simulateTimePassing(clockIncrement) .simulatePhoneTimeSuggestion(timeSuggestion) .verifySystemClockWasSetAndResetCallTracking( - expectSystemClockMillis, true /* expectNetworkBroadcast */); + expectedSystemClockMillis, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, timeSuggestion); } @Test public void testSuggestPhoneTime_emptySuggestionIgnored() { - Scenario scenario = SCENARIO_1; - mScript.pokeFakeClocks(scenario) - .pokeTimeDetectionEnabled(true); - - PhoneTimeSuggestion timeSuggestion = createPhoneTimeSuggestion(ARBITRARY_PHONE_ID, null); + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true); + int phoneId = ARBITRARY_PHONE_ID; + PhoneTimeSuggestion timeSuggestion = + mScript.generatePhoneTimeSuggestion(phoneId, null); mScript.simulatePhoneTimeSuggestion(timeSuggestion) - .verifySystemClockWasNotSetAndResetCallTracking(); + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, null); } @Test public void testSuggestPhoneTime_systemClockThreshold() { - Scenario scenario = SCENARIO_1; - final int systemClockUpdateThresholdMillis = 1000; - mScript.pokeFakeClocks(scenario) + int systemClockUpdateThresholdMillis = 1000; + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) .pokeThresholds(systemClockUpdateThresholdMillis) - .pokeTimeDetectionEnabled(true); - - PhoneTimeSuggestion timeSuggestion1 = - scenario.createPhoneTimeSuggestionForActual(ARBITRARY_PHONE_ID); - TimestampedValue utcTime1 = timeSuggestion1.getUtcTime(); + .pokeAutoTimeDetectionEnabled(true); 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()); + int phoneId = ARBITRARY_PHONE_ID; // Send the first time signal. It should be used. - mScript.simulatePhoneTimeSuggestion(timeSuggestion1) - .verifySystemClockWasSetAndResetCallTracking( - expectSystemClockMillis1, true /* expectNetworkBroadcast */); + { + long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS; + PhoneTimeSuggestion timeSuggestion1 = + mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis); + TimestampedValue utcTime1 = timeSuggestion1.getUtcTime(); + + // Increment the the device clocks to simulate the passage of time. + mScript.simulateTimePassing(clockIncrement); + + long expectedSystemClockMillis1 = + TimeDetectorStrategy.getTimeAt(utcTime1, mScript.peekElapsedRealtimeMillis()); + + mScript.simulatePhoneTimeSuggestion(timeSuggestion1) + .verifySystemClockWasSetAndResetCallTracking( + expectedSystemClockMillis1, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, timeSuggestion1); + } // 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); - PhoneTimeSuggestion timeSuggestion2 = - createPhoneTimeSuggestion(ARBITRARY_PHONE_ID, utcTime2); - mScript.simulateTimePassing(clockIncrement) - .simulatePhoneTimeSuggestion(timeSuggestion2) - .verifySystemClockWasNotSetAndResetCallTracking(); + // stored, but not used to set the system clock. + { + int underThresholdMillis = systemClockUpdateThresholdMillis - 1; + PhoneTimeSuggestion timeSuggestion2 = mScript.generatePhoneTimeSuggestion( + phoneId, mScript.peekSystemClockMillis() + underThresholdMillis); + mScript.simulateTimePassing(clockIncrement) + .simulatePhoneTimeSuggestion(timeSuggestion2) + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, timeSuggestion2); + } // 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); + { + PhoneTimeSuggestion timeSuggestion3 = mScript.generatePhoneTimeSuggestion( + phoneId, + mScript.peekSystemClockMillis() + systemClockUpdateThresholdMillis); + mScript.simulateTimePassing(clockIncrement); + + long expectedSystemClockMillis3 = + TimeDetectorStrategy.getTimeAt(timeSuggestion3.getUtcTime(), + mScript.peekElapsedRealtimeMillis()); + + mScript.simulatePhoneTimeSuggestion(timeSuggestion3) + .verifySystemClockWasSetAndResetCallTracking( + expectedSystemClockMillis3, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, timeSuggestion3); + } + } + + @Test + public void testSuggestPhoneTime_multiplePhoneIdsAndBucketing() { + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true); + + // There are 2 phones in this test. Phone 2 has a different idea of the current time. + // phone1Id < phone2Id (which is important because the strategy uses the lowest ID when + // multiple phone suggestions are available. + int phone1Id = ARBITRARY_PHONE_ID; + int phone2Id = ARBITRARY_PHONE_ID + 1; + long phone1TimeMillis = ARBITRARY_TEST_TIME_MILLIS; + long phone2TimeMillis = phone1TimeMillis + 60000; + + final int clockIncrement = 999; + + // Make a suggestion with phone2Id. + { + PhoneTimeSuggestion phone2TimeSuggestion = + mScript.generatePhoneTimeSuggestion(phone2Id, phone2TimeMillis); + mScript.simulateTimePassing(clockIncrement); + + long expectedSystemClockMillis = phone2TimeMillis + clockIncrement; + + mScript.simulatePhoneTimeSuggestion(phone2TimeSuggestion) + .verifySystemClockWasSetAndResetCallTracking( + expectedSystemClockMillis, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phone1Id, null) + .assertLatestPhoneSuggestion(phone2Id, phone2TimeSuggestion); + } - PhoneTimeSuggestion timeSuggestion3 = - createPhoneTimeSuggestion(ARBITRARY_PHONE_ID, utcTime3); mScript.simulateTimePassing(clockIncrement); - long expectSystemClockMillis3 = - TimeDetectorStrategy.getTimeAt(utcTime3, mScript.peekElapsedRealtimeMillis()); + // Now make a different suggestion with phone1Id. + { + PhoneTimeSuggestion phone1TimeSuggestion = + mScript.generatePhoneTimeSuggestion(phone1Id, phone1TimeMillis); + mScript.simulateTimePassing(clockIncrement); - mScript.simulatePhoneTimeSuggestion(timeSuggestion3) - .verifySystemClockWasSetAndResetCallTracking( - expectSystemClockMillis3, true /* expectNetworkBroadcast */); + long expectedSystemClockMillis = phone1TimeMillis + clockIncrement; + + mScript.simulatePhoneTimeSuggestion(phone1TimeSuggestion) + .verifySystemClockWasSetAndResetCallTracking( + expectedSystemClockMillis, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phone1Id, phone1TimeSuggestion); + + } + + mScript.simulateTimePassing(clockIncrement); + + // Make another suggestion with phone2Id. It should be stored but not used because the + // phone1Id suggestion will still "win". + { + PhoneTimeSuggestion phone2TimeSuggestion = + mScript.generatePhoneTimeSuggestion(phone2Id, phone2TimeMillis); + mScript.simulateTimePassing(clockIncrement); + + mScript.simulatePhoneTimeSuggestion(phone2TimeSuggestion) + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phone2Id, phone2TimeSuggestion); + } + + // Let enough time pass that phone1Id's suggestion should now be too old. + mScript.simulateTimePassing(SimpleTimeDetectorStrategy.PHONE_BUCKET_SIZE_MILLIS); + + // Make another suggestion with phone2Id. It should be used because the phoneId1 + // is in an older "bucket". + { + PhoneTimeSuggestion phone2TimeSuggestion = + mScript.generatePhoneTimeSuggestion(phone2Id, phone2TimeMillis); + mScript.simulateTimePassing(clockIncrement); + + long expectedSystemClockMillis = phone2TimeMillis + clockIncrement; + + mScript.simulatePhoneTimeSuggestion(phone2TimeSuggestion) + .verifySystemClockWasSetAndResetCallTracking( + expectedSystemClockMillis, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phone2Id, phone2TimeSuggestion); + } } @Test public void testSuggestPhoneTime_autoTimeDisabled() { - Scenario scenario = SCENARIO_1; - mScript.pokeFakeClocks(scenario) - .pokeTimeDetectionEnabled(false); + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(false); + int phoneId = ARBITRARY_PHONE_ID; PhoneTimeSuggestion timeSuggestion = - scenario.createPhoneTimeSuggestionForActual(ARBITRARY_PHONE_ID); - mScript.simulatePhoneTimeSuggestion(timeSuggestion) - .verifySystemClockWasNotSetAndResetCallTracking(); + mScript.generatePhoneTimeSuggestion(phoneId, ARBITRARY_TEST_TIME_MILLIS); + mScript.simulateTimePassing(1000) + .simulatePhoneTimeSuggestion(timeSuggestion) + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, timeSuggestion); } @Test public void testSuggestPhoneTime_invalidNitzReferenceTimesIgnored() { - Scenario scenario = SCENARIO_1; final int systemClockUpdateThreshold = 2000; - mScript.pokeFakeClocks(scenario) + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) .pokeThresholds(systemClockUpdateThreshold) - .pokeTimeDetectionEnabled(true); + .pokeAutoTimeDetectionEnabled(true); + + long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS; + int phoneId = ARBITRARY_PHONE_ID; + PhoneTimeSuggestion timeSuggestion1 = - scenario.createPhoneTimeSuggestionForActual(ARBITRARY_PHONE_ID); + mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis); TimestampedValue utcTime1 = timeSuggestion1.getUtcTime(); - // Initialize the strategy / device with a time set from NITZ. + // Initialize the strategy / device with a time set from a phone suggestion. mScript.simulateTimePassing(100); long expectedSystemClockMillis1 = TimeDetectorStrategy.getTimeAt(utcTime1, mScript.peekElapsedRealtimeMillis()); mScript.simulatePhoneTimeSuggestion(timeSuggestion1) .verifySystemClockWasSetAndResetCallTracking( - expectedSystemClockMillis1, true /* expectNetworkBroadcast */); + expectedSystemClockMillis1, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, timeSuggestion1); // The UTC time increment should be larger than the system clock update threshold so we // know it shouldn't be ignored for other reasons. @@ -182,9 +278,10 @@ public class SimpleTimeDetectorStrategyTest { TimestampedValue utcTime2 = new TimestampedValue<>( referenceTimeBeforeLastSignalMillis, validUtcTimeMillis); PhoneTimeSuggestion timeSuggestion2 = - createPhoneTimeSuggestion(ARBITRARY_PHONE_ID, utcTime2); + createPhoneTimeSuggestion(phoneId, utcTime2); mScript.simulatePhoneTimeSuggestion(timeSuggestion2) - .verifySystemClockWasNotSetAndResetCallTracking(); + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, timeSuggestion1); // Now supply a new signal that has an obviously bogus reference time : substantially in the // future. @@ -193,9 +290,10 @@ public class SimpleTimeDetectorStrategyTest { TimestampedValue utcTime3 = new TimestampedValue<>( referenceTimeInFutureMillis, validUtcTimeMillis); PhoneTimeSuggestion timeSuggestion3 = - createPhoneTimeSuggestion(ARBITRARY_PHONE_ID, utcTime3); + createPhoneTimeSuggestion(phoneId, utcTime3); mScript.simulatePhoneTimeSuggestion(timeSuggestion3) - .verifySystemClockWasNotSetAndResetCallTracking(); + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, timeSuggestion1); // Just to prove validUtcTimeMillis is valid. long validReferenceTimeMillis = utcTime1.getReferenceTimeMillis() + 100; @@ -204,23 +302,25 @@ public class SimpleTimeDetectorStrategyTest { long expectedSystemClockMillis4 = TimeDetectorStrategy.getTimeAt(utcTime4, mScript.peekElapsedRealtimeMillis()); PhoneTimeSuggestion timeSuggestion4 = - createPhoneTimeSuggestion(ARBITRARY_PHONE_ID, utcTime4); + createPhoneTimeSuggestion(phoneId, utcTime4); mScript.simulatePhoneTimeSuggestion(timeSuggestion4) .verifySystemClockWasSetAndResetCallTracking( - expectedSystemClockMillis4, true /* expectNetworkBroadcast */); + expectedSystemClockMillis4, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, timeSuggestion4); } @Test public void testSuggestPhoneTime_timeDetectionToggled() { - Scenario scenario = SCENARIO_1; final int clockIncrementMillis = 100; final int systemClockUpdateThreshold = 2000; - mScript.pokeFakeClocks(scenario) + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) .pokeThresholds(systemClockUpdateThreshold) - .pokeTimeDetectionEnabled(false); + .pokeAutoTimeDetectionEnabled(false); + int phoneId = ARBITRARY_PHONE_ID; + long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS; PhoneTimeSuggestion timeSuggestion1 = - scenario.createPhoneTimeSuggestionForActual(ARBITRARY_PHONE_ID); + mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis); TimestampedValue utcTime1 = timeSuggestion1.getUtcTime(); // Simulate time passing. @@ -229,7 +329,8 @@ public class SimpleTimeDetectorStrategyTest { // Simulate the time signal being received. It should not be used because auto time // detection is off but it should be recorded. mScript.simulatePhoneTimeSuggestion(timeSuggestion1) - .verifySystemClockWasNotSetAndResetCallTracking(); + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, timeSuggestion1); // Simulate more time passing. mScript.simulateTimePassing(clockIncrementMillis); @@ -240,64 +341,95 @@ public class SimpleTimeDetectorStrategyTest { // Turn on auto time detection. mScript.simulateAutoTimeDetectionToggle() .verifySystemClockWasSetAndResetCallTracking( - expectedSystemClockMillis1, true /* expectNetworkBroadcast */); + expectedSystemClockMillis1, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, timeSuggestion1); // Turn off auto time detection. mScript.simulateAutoTimeDetectionToggle() - .verifySystemClockWasNotSetAndResetCallTracking(); + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, timeSuggestion1); // 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); - PhoneTimeSuggestion timeSuggestion2 = - createPhoneTimeSuggestion(ARBITRARY_PHONE_ID, utcTime2); + PhoneTimeSuggestion timeSuggestion2 = mScript.generatePhoneTimeSuggestion( + phoneId, mScript.peekSystemClockMillis() + systemClockUpdateThreshold); // Simulate more time passing. mScript.simulateTimePassing(clockIncrementMillis); - long expectedSystemClockMillis2 = - TimeDetectorStrategy.getTimeAt(utcTime2, mScript.peekElapsedRealtimeMillis()); + long expectedSystemClockMillis2 = TimeDetectorStrategy.getTimeAt( + timeSuggestion2.getUtcTime(), mScript.peekElapsedRealtimeMillis()); // The new time, though valid, should not be set in the system clock because auto time is // disabled. mScript.simulatePhoneTimeSuggestion(timeSuggestion2) - .verifySystemClockWasNotSetAndResetCallTracking(); + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, timeSuggestion2); // Turn on auto time detection. mScript.simulateAutoTimeDetectionToggle() .verifySystemClockWasSetAndResetCallTracking( - expectedSystemClockMillis2, true /* expectNetworkBroadcast */); + expectedSystemClockMillis2, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, timeSuggestion2); + } + + @Test + public void testSuggestPhoneTime_maxSuggestionAge() { + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true); + + int phoneId = ARBITRARY_PHONE_ID; + long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS; + PhoneTimeSuggestion phoneSuggestion = + mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis); + int clockIncrementMillis = 1000; + + mScript.simulateTimePassing(clockIncrementMillis) + .simulatePhoneTimeSuggestion(phoneSuggestion) + .verifySystemClockWasSetAndResetCallTracking( + testTimeMillis + clockIncrementMillis, true /* expectedNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, phoneSuggestion); + + // Look inside and check what the strategy considers the current best phone suggestion. + assertEquals(phoneSuggestion, mScript.peekBestPhoneSuggestion()); + + // Simulate time passing, long enough that phoneSuggestion is now too old. + mScript.simulateTimePassing(SimpleTimeDetectorStrategy.PHONE_MAX_AGE_MILLIS); + + // Look inside and check what the strategy considers the current best phone suggestion. It + // should still be the, it's just no longer used. + assertNull(mScript.peekBestPhoneSuggestion()); + mScript.assertLatestPhoneSuggestion(phoneId, phoneSuggestion); } @Test public void testSuggestManualTime_autoTimeDisabled() { - Scenario scenario = SCENARIO_1; - mScript.pokeFakeClocks(scenario) - .pokeTimeDetectionEnabled(false); + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(false); - ManualTimeSuggestion timeSuggestion = scenario.createManualTimeSuggestionForActual(); + long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS; + ManualTimeSuggestion timeSuggestion = mScript.generateManualTimeSuggestion(testTimeMillis); final int clockIncrement = 1000; - long expectSystemClockMillis = scenario.getActualTimeMillis() + clockIncrement; + long expectedSystemClockMillis = testTimeMillis + clockIncrement; mScript.simulateTimePassing(clockIncrement) .simulateManualTimeSuggestion(timeSuggestion) .verifySystemClockWasSetAndResetCallTracking( - expectSystemClockMillis, false /* expectNetworkBroadcast */); + expectedSystemClockMillis, false /* expectNetworkBroadcast */); } @Test public void testSuggestManualTime_retainsAutoSignal() { - Scenario scenario = SCENARIO_1; - // Configure the start state. - mScript.pokeFakeClocks(scenario) - .pokeTimeDetectionEnabled(true); + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true); + + int phoneId = ARBITRARY_PHONE_ID; // Simulate a phone suggestion. + long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS; PhoneTimeSuggestion phoneTimeSuggestion = - scenario.createPhoneTimeSuggestionForActual(ARBITRARY_PHONE_ID); + mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis); long expectedAutoClockMillis = phoneTimeSuggestion.getUtcTime().getValue(); final int clockIncrement = 1000; @@ -307,7 +439,8 @@ public class SimpleTimeDetectorStrategyTest { mScript.simulatePhoneTimeSuggestion(phoneTimeSuggestion) .verifySystemClockWasSetAndResetCallTracking( - expectedAutoClockMillis, true /* expectNetworkBroadcast */); + expectedAutoClockMillis, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, phoneTimeSuggestion); // Simulate the passage of time. mScript.simulateTimePassing(clockIncrement); @@ -315,20 +448,22 @@ public class SimpleTimeDetectorStrategyTest { // Switch to manual. mScript.simulateAutoTimeDetectionToggle() - .verifySystemClockWasNotSetAndResetCallTracking(); + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, phoneTimeSuggestion); // Simulate the passage of time. mScript.simulateTimePassing(clockIncrement); expectedAutoClockMillis += clockIncrement; - // Simulate a manual suggestion 1 day different from the auto suggestion. - long manualTimeMillis = SCENARIO_1.getActualTimeMillis() + ONE_DAY_MILLIS; + long manualTimeMillis = testTimeMillis + ONE_DAY_MILLIS; long expectedManualClockMillis = manualTimeMillis; - ManualTimeSuggestion manualTimeSuggestion = createManualTimeSuggestion(manualTimeMillis); + ManualTimeSuggestion manualTimeSuggestion = + mScript.generateManualTimeSuggestion(manualTimeMillis); mScript.simulateManualTimeSuggestion(manualTimeSuggestion) .verifySystemClockWasSetAndResetCallTracking( - expectedManualClockMillis, false /* expectNetworkBroadcast */); + expectedManualClockMillis, false /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, phoneTimeSuggestion); // Simulate the passage of time. mScript.simulateTimePassing(clockIncrement); @@ -338,11 +473,13 @@ public class SimpleTimeDetectorStrategyTest { mScript.simulateAutoTimeDetectionToggle(); mScript.verifySystemClockWasSetAndResetCallTracking( - expectedAutoClockMillis, true /* expectNetworkBroadcast */); + expectedAutoClockMillis, true /* expectNetworkBroadcast */) + .assertLatestPhoneSuggestion(phoneId, phoneTimeSuggestion); // Switch back to manual - nothing should happen to the clock. mScript.simulateAutoTimeDetectionToggle() - .verifySystemClockWasNotSetAndResetCallTracking(); + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestPhoneSuggestion(phoneId, phoneTimeSuggestion); } /** @@ -350,11 +487,11 @@ public class SimpleTimeDetectorStrategyTest { */ @Test public void testSuggestManualTime_autoTimeEnabled() { - Scenario scenario = SCENARIO_1; - mScript.pokeFakeClocks(scenario) - .pokeTimeDetectionEnabled(true); + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true); - ManualTimeSuggestion timeSuggestion = scenario.createManualTimeSuggestionForActual(); + ManualTimeSuggestion timeSuggestion = + mScript.generateManualTimeSuggestion(ARBITRARY_TEST_TIME_MILLIS); final int clockIncrement = 1000; mScript.simulateTimePassing(clockIncrement) @@ -367,7 +504,7 @@ public class SimpleTimeDetectorStrategyTest { * like the real thing should, it also asserts preconditions. */ private static class FakeCallback implements TimeDetectorStrategy.Callback { - private boolean mTimeDetectionEnabled; + private boolean mAutoTimeDetectionEnabled; private boolean mWakeLockAcquired; private long mElapsedRealtimeMillis; private long mSystemClockMillis; @@ -384,7 +521,7 @@ public class SimpleTimeDetectorStrategyTest { @Override public boolean isAutoTimeDetectionEnabled() { - return mTimeDetectionEnabled; + return mAutoTimeDetectionEnabled; } @Override @@ -397,7 +534,6 @@ public class SimpleTimeDetectorStrategyTest { @Override public long elapsedRealtimeMillis() { - assertWakeLockAcquired(); return mElapsedRealtimeMillis; } @@ -428,57 +564,57 @@ public class SimpleTimeDetectorStrategyTest { // Methods below are for managing the fake's behavior. - public void pokeSystemClockUpdateThreshold(int thresholdMillis) { + void pokeSystemClockUpdateThreshold(int thresholdMillis) { mSystemClockUpdateThresholdMillis = thresholdMillis; } - public void pokeElapsedRealtimeMillis(long elapsedRealtimeMillis) { + void pokeElapsedRealtimeMillis(long elapsedRealtimeMillis) { mElapsedRealtimeMillis = elapsedRealtimeMillis; } - public void pokeSystemClockMillis(long systemClockMillis) { + void pokeSystemClockMillis(long systemClockMillis) { mSystemClockMillis = systemClockMillis; } - public void pokeAutoTimeDetectionEnabled(boolean enabled) { - mTimeDetectionEnabled = enabled; + void pokeAutoTimeDetectionEnabled(boolean enabled) { + mAutoTimeDetectionEnabled = enabled; } - public long peekElapsedRealtimeMillis() { + long peekElapsedRealtimeMillis() { return mElapsedRealtimeMillis; } - public long peekSystemClockMillis() { + long peekSystemClockMillis() { return mSystemClockMillis; } - public void simulateTimePassing(int incrementMillis) { + void simulateTimePassing(long incrementMillis) { mElapsedRealtimeMillis += incrementMillis; mSystemClockMillis += incrementMillis; } - public void simulateAutoTimeZoneDetectionToggle() { - mTimeDetectionEnabled = !mTimeDetectionEnabled; + void simulateAutoTimeZoneDetectionToggle() { + mAutoTimeDetectionEnabled = !mAutoTimeDetectionEnabled; } - public void verifySystemClockNotSet() { + void verifySystemClockNotSet() { assertFalse(mSystemClockWasSet); } - public void verifySystemClockWasSet(long expectSystemClockMillis) { + void verifySystemClockWasSet(long expectedSystemClockMillis) { assertTrue(mSystemClockWasSet); - assertEquals(expectSystemClockMillis, mSystemClockMillis); + assertEquals(expectedSystemClockMillis, mSystemClockMillis); } - public void verifyIntentWasBroadcast() { + void verifyIntentWasBroadcast() { assertTrue(mBroadcastSent != null); } - public void verifyIntentWasNotBroadcast() { + void verifyIntentWasNotBroadcast() { assertNull(mBroadcastSent); } - public void resetCallTracking() { + void resetCallTracking() { mSystemClockWasSet = false; mBroadcastSent = null; } @@ -495,23 +631,23 @@ public class SimpleTimeDetectorStrategyTest { private class Script { private final FakeCallback mFakeCallback; - private final SimpleTimeDetectorStrategy mSimpleTimeDetectorStrategy; + private final SimpleTimeDetectorStrategy mTimeDetectorStrategy; Script() { mFakeCallback = new FakeCallback(); - mSimpleTimeDetectorStrategy = new SimpleTimeDetectorStrategy(); - mSimpleTimeDetectorStrategy.initialize(mFakeCallback); + mTimeDetectorStrategy = new SimpleTimeDetectorStrategy(); + mTimeDetectorStrategy.initialize(mFakeCallback); } - Script pokeTimeDetectionEnabled(boolean enabled) { + Script pokeAutoTimeDetectionEnabled(boolean enabled) { mFakeCallback.pokeAutoTimeDetectionEnabled(enabled); return this; } - Script pokeFakeClocks(Scenario scenario) { - mFakeCallback.pokeElapsedRealtimeMillis(scenario.getInitialRealTimeMillis()); - mFakeCallback.pokeSystemClockMillis(scenario.getInitialSystemClockMillis()); + Script pokeFakeClocks(TimestampedValue timeInfo) { + mFakeCallback.pokeElapsedRealtimeMillis(timeInfo.getReferenceTimeMillis()); + mFakeCallback.pokeSystemClockMillis(timeInfo.getValue()); return this; } @@ -529,23 +665,23 @@ public class SimpleTimeDetectorStrategyTest { } Script simulatePhoneTimeSuggestion(PhoneTimeSuggestion timeSuggestion) { - mSimpleTimeDetectorStrategy.suggestPhoneTime(timeSuggestion); + mTimeDetectorStrategy.suggestPhoneTime(timeSuggestion); return this; } Script simulateManualTimeSuggestion(ManualTimeSuggestion timeSuggestion) { - mSimpleTimeDetectorStrategy.suggestManualTime(timeSuggestion); + mTimeDetectorStrategy.suggestManualTime(timeSuggestion); return this; } Script simulateAutoTimeDetectionToggle() { mFakeCallback.simulateAutoTimeZoneDetectionToggle(); - mSimpleTimeDetectorStrategy.handleAutoTimeDetectionChanged(); + mTimeDetectorStrategy.handleAutoTimeDetectionChanged(); return this; } - Script simulateTimePassing(int clockIncrement) { - mFakeCallback.simulateTimePassing(clockIncrement); + Script simulateTimePassing(long clockIncrementMillis) { + mFakeCallback.simulateTimePassing(clockIncrementMillis); return this; } @@ -557,85 +693,52 @@ public class SimpleTimeDetectorStrategyTest { } Script verifySystemClockWasSetAndResetCallTracking( - long expectSystemClockMillis, boolean expectNetworkBroadcast) { - mFakeCallback.verifySystemClockWasSet(expectSystemClockMillis); + long expectedSystemClockMillis, boolean expectNetworkBroadcast) { + mFakeCallback.verifySystemClockWasSet(expectedSystemClockMillis); if (expectNetworkBroadcast) { 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; + /** + * White box test info: Asserts the latest suggestion for the phone ID is as expected. + */ + Script assertLatestPhoneSuggestion(int phoneId, PhoneTimeSuggestion expected) { + assertEquals(expected, mTimeDetectorStrategy.getLatestPhoneSuggestion(phoneId)); + return this; } - long getInitialRealTimeMillis() { - return mInitialDeviceRealtimeMillis; + /** + * White box test info: Returns the phone suggestion that would be used, if any, given the + * current elapsed real time clock. + */ + PhoneTimeSuggestion peekBestPhoneSuggestion() { + return mTimeDetectorStrategy.findBestPhoneSuggestionForTests(); } - long getInitialSystemClockMillis() { - return mInitialDeviceSystemClockMillis; + /** + * Generates a ManualTimeSuggestion using the current elapsed realtime clock for the + * reference time. + */ + ManualTimeSuggestion generateManualTimeSuggestion(long timeMillis) { + TimestampedValue utcTime = + new TimestampedValue<>(mFakeCallback.peekElapsedRealtimeMillis(), timeMillis); + return new ManualTimeSuggestion(utcTime); } - long getActualTimeMillis() { - return mActualTimeMillis; - } - - PhoneTimeSuggestion createPhoneTimeSuggestionForActual(int phoneId) { - TimestampedValue time = new TimestampedValue<>( - mInitialDeviceRealtimeMillis, mActualTimeMillis); + /** + * Generates a PhoneTimeSuggestion using the current elapsed realtime clock for the + * reference time. + */ + PhoneTimeSuggestion generatePhoneTimeSuggestion(int phoneId, Long timeMillis) { + TimestampedValue time = null; + if (timeMillis != null) { + time = new TimestampedValue<>(peekElapsedRealtimeMillis(), timeMillis); + } return createPhoneTimeSuggestion(phoneId, time); } - - ManualTimeSuggestion createManualTimeSuggestionForActual() { - TimestampedValue time = new TimestampedValue<>( - mInitialDeviceRealtimeMillis, mActualTimeMillis); - return new ManualTimeSuggestion(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 PhoneTimeSuggestion createPhoneTimeSuggestion(int phoneId, @@ -645,12 +748,6 @@ public class SimpleTimeDetectorStrategyTest { .build(); } - private ManualTimeSuggestion createManualTimeSuggestion(long timeMillis) { - TimestampedValue utcTime = - new TimestampedValue<>(mScript.peekElapsedRealtimeMillis(), timeMillis); - return new ManualTimeSuggestion(utcTime); - } - private static long createUtcTime(int year, int monthInYear, int day, int hourOfDay, int minute, int second) { Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("Etc/UTC"));