diff options
| author | 2019-12-12 04:08:06 -0800 | |
|---|---|---|
| committer | 2019-12-12 04:08:06 -0800 | |
| commit | 1602545f282d9466bbfd61471ccd507bf823e5c2 (patch) | |
| tree | 9cedf59cb88579d89edf015efc7e7f2cafe4c588 | |
| parent | 31f969ba66b8062cff0bd4307b18aee804e0356d (diff) | |
| parent | 51c61e8bdba4dac4499b25eb0361fe60a65d4d43 (diff) | |
Merge "Handle multiple phoneIds in time detection"
am: 51c61e8bdb
Change-Id: I6b792aba6ca4ca2f6b43256080e6dff230aca370
4 files changed, 682 insertions, 343 deletions
diff --git a/core/java/android/app/timedetector/ManualTimeSuggestion.java b/core/java/android/app/timedetector/ManualTimeSuggestion.java index 471606da4d75..55f92be14cd0 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<Long> 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 dd02af7a3ac7..4a89a1245473 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<Long> utcTime) { + public Builder setUtcTime(@Nullable TimestampedValue<Long> 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 4e8ba078a73c..5a31bbc859c8 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. * * <p>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; - - // Information about the last time signal received: Used when toggling auto-time. - @Nullable private TimestampedValue<Long> mLastAutoSystemClockTime; - private boolean mLastAutoSystemClockTimeSendNetworkBroadcast; + // 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<Long> mLastAutoSystemClockTimeSet; - // System clock state. - @Nullable private TimestampedValue<Long> 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<Integer, LinkedList<PhoneTimeSuggestion>> 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<Long> newUtcTime = suggestion.getUtcTime(); + + // 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); + } - // Empty suggestions are just ignored as we don't currently keep track of suggestion origin. + @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<Long> 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 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; + } } @Override - public synchronized void suggestManualTime(ManualTimeSuggestion timeSuggestion) { - final TimestampedValue<Long> newUtcTime = timeSuggestion.getUtcTime(); - setSystemClockIfRequired(ORIGIN_MANUAL, newUtcTime, timeSuggestion); + public synchronized void dump(@NonNull PrintWriter pw, @Nullable String[] args) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + ipw.println("TimeDetectorStrategy:"); + ipw.increaseIndent(); // level 1 + + 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<Integer, LinkedList<PhoneTimeSuggestion>> 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(); } - private static boolean validateNewPhoneSuggestion(@NonNull PhoneTimeSuggestion newSuggestion, - @Nullable PhoneTimeSuggestion lastSuggestion) { - - 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); + @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<PhoneTimeSuggestion> 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<Long> 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<Long> 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<PhoneTimeSuggestion> 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<Long> 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<Long> 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<Long> 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; - - 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(); + @GuardedBy("this") + private void setSystemClockUnderWakeLock( + int origin, @NonNull TimestampedValue<Long> newTime, @NonNull Object cause) { + + 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<Long> 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<PhoneTimeSuggestion> 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 7a0a28dfbd16..52e7d8197842 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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,84 +693,51 @@ 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; - } - - long getInitialRealTimeMillis() { - return mInitialDeviceRealtimeMillis; - } - - long getInitialSystemClockMillis() { - return mInitialDeviceSystemClockMillis; - } - - long getActualTimeMillis() { - return mActualTimeMillis; - } - - PhoneTimeSuggestion createPhoneTimeSuggestionForActual(int phoneId) { - TimestampedValue<Long> time = new TimestampedValue<>( - mInitialDeviceRealtimeMillis, mActualTimeMillis); - return createPhoneTimeSuggestion(phoneId, time); - } - - ManualTimeSuggestion createManualTimeSuggestionForActual() { - TimestampedValue<Long> time = new TimestampedValue<>( - mInitialDeviceRealtimeMillis, mActualTimeMillis); - return new ManualTimeSuggestion(time); + /** + * 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; } - 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); + /** + * 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(); + } + + /** + * Generates a ManualTimeSuggestion using the current elapsed realtime clock for the + * reference time. + */ + ManualTimeSuggestion generateManualTimeSuggestion(long timeMillis) { + TimestampedValue<Long> utcTime = + new TimestampedValue<>(mFakeCallback.peekElapsedRealtimeMillis(), timeMillis); + return new ManualTimeSuggestion(utcTime); + } + + /** + * Generates a PhoneTimeSuggestion using the current elapsed realtime clock for the + * reference time. + */ + PhoneTimeSuggestion generatePhoneTimeSuggestion(int phoneId, Long timeMillis) { + TimestampedValue<Long> time = null; + if (timeMillis != null) { + time = new TimestampedValue<>(peekElapsedRealtimeMillis(), timeMillis); } + return createPhoneTimeSuggestion(phoneId, time); } } @@ -645,12 +748,6 @@ public class SimpleTimeDetectorStrategyTest { .build(); } - private ManualTimeSuggestion createManualTimeSuggestion(long timeMillis) { - TimestampedValue<Long> 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")); |