Merge "Change NetworkTimeUpdateServiceImpl behavior"

This commit is contained in:
Neil Fuller
2020-01-07 10:07:08 +00:00
committed by Gerrit Code Review
14 changed files with 734 additions and 200 deletions

View File

@@ -17,6 +17,7 @@
package android.app.timedetector;
import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.NetworkTimeSuggestion;
import android.app.timedetector.PhoneTimeSuggestion;
/**
@@ -35,4 +36,5 @@ import android.app.timedetector.PhoneTimeSuggestion;
interface ITimeDetectorService {
void suggestPhoneTime(in PhoneTimeSuggestion timeSuggestion);
void suggestManualTime(in ManualTimeSuggestion timeSuggestion);
void suggestNetworkTime(in NetworkTimeSuggestion timeSuggestion);
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.timedetector;
parcelable NetworkTimeSuggestion;

View File

@@ -0,0 +1,129 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.timedetector;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.TimestampedValue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* A time signal from a network time source like NTP. The value consists of the number of
* milliseconds elapsed since 1/1/1970 00:00:00 UTC and the time according to the elapsed realtime
* clock when that number was established. The elapsed realtime clock is considered accurate but
* volatile, so time signals must not be persisted across device resets.
*
* @hide
*/
public final class NetworkTimeSuggestion implements Parcelable {
public static final @NonNull Creator<NetworkTimeSuggestion> CREATOR =
new Creator<NetworkTimeSuggestion>() {
public NetworkTimeSuggestion createFromParcel(Parcel in) {
return NetworkTimeSuggestion.createFromParcel(in);
}
public NetworkTimeSuggestion[] newArray(int size) {
return new NetworkTimeSuggestion[size];
}
};
@NonNull
private final TimestampedValue<Long> mUtcTime;
@Nullable
private ArrayList<String> mDebugInfo;
public NetworkTimeSuggestion(@NonNull TimestampedValue<Long> utcTime) {
mUtcTime = Objects.requireNonNull(utcTime);
Objects.requireNonNull(utcTime.getValue());
}
private static NetworkTimeSuggestion createFromParcel(Parcel in) {
TimestampedValue<Long> utcTime = in.readParcelable(null /* classLoader */);
NetworkTimeSuggestion suggestion = new NetworkTimeSuggestion(utcTime);
@SuppressWarnings("unchecked")
ArrayList<String> debugInfo = (ArrayList<String>) in.readArrayList(null /* classLoader */);
suggestion.mDebugInfo = debugInfo;
return suggestion;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelable(mUtcTime, 0);
dest.writeList(mDebugInfo);
}
@NonNull
public TimestampedValue<Long> getUtcTime() {
return mUtcTime;
}
@NonNull
public List<String> getDebugInfo() {
return mDebugInfo == null
? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
}
/**
* Associates information with the instance that can be useful for debugging / logging. The
* information is present in {@link #toString()} but is not considered for
* {@link #equals(Object)} and {@link #hashCode()}.
*/
public void addDebugInfo(String... debugInfos) {
if (mDebugInfo == null) {
mDebugInfo = new ArrayList<>();
}
mDebugInfo.addAll(Arrays.asList(debugInfos));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
NetworkTimeSuggestion that = (NetworkTimeSuggestion) o;
return Objects.equals(mUtcTime, that.mUtcTime);
}
@Override
public int hashCode() {
return Objects.hash(mUtcTime);
}
@Override
public String toString() {
return "NetworkTimeSuggestion{"
+ "mUtcTime=" + mUtcTime
+ ", mDebugInfo=" + mDebugInfo
+ '}';
}
}

View File

@@ -85,4 +85,19 @@ public class TimeDetector {
manualTimeSuggestion.addDebugInfo(why);
return manualTimeSuggestion;
}
/**
* Suggests the time according to a network time source like NTP.
*/
@RequiresPermission(android.Manifest.permission.SET_TIME)
public void suggestNetworkTime(NetworkTimeSuggestion timeSuggestion) {
if (DEBUG) {
Log.d(TAG, "suggestNetworkTime called: " + timeSuggestion);
}
try {
mITimeDetectorService.suggestNetworkTime(timeSuggestion);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}

View File

@@ -175,4 +175,22 @@ public class NtpTrustedTime implements TrustedTime {
public long getCachedNtpTimeReference() {
return mCachedNtpElapsedRealtime;
}
/**
* Returns the combination of {@link #getCachedNtpTime()} and {@link
* #getCachedNtpTimeReference()} as a {@link TimestampedValue}. This method is useful when
* passing the time to another component that will adjust for elapsed time.
*
* @throws IllegalStateException if there is no cached value
*/
@UnsupportedAppUsage
public TimestampedValue<Long> getCachedNtpTimeSignal() {
if (!mHasCache) {
throw new IllegalStateException("Missing authoritative time source");
}
if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit");
return new TimestampedValue<>(mCachedNtpElapsedRealtime, mCachedNtpTime);
}
}

View File

@@ -2266,9 +2266,6 @@
<!-- Number of times to try again with the shorter interval, before backing
off until the normal polling interval. A value < 0 indicates infinite. -->
<integer name="config_ntpRetry">3</integer>
<!-- If the time difference is greater than this threshold in milliseconds,
then update the time. -->
<integer name="config_ntpThreshold">5000</integer>
<!-- Timeout to wait for NTP server response in milliseconds. -->
<integer name="config_ntpTimeout">5000</integer>

View File

@@ -489,7 +489,6 @@
<java-symbol type="integer" name="config_ntpPollingInterval" />
<java-symbol type="integer" name="config_ntpPollingIntervalShorter" />
<java-symbol type="integer" name="config_ntpRetry" />
<java-symbol type="integer" name="config_ntpThreshold" />
<java-symbol type="integer" name="config_ntpTimeout" />
<java-symbol type="integer" name="config_shortPressOnPowerBehavior" />
<java-symbol type="integer" name="config_toastDefaultGravity" />

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.timedetector;
import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import android.util.TimestampedValue;
import org.junit.Test;
public class NetworkTimeSuggestionTest {
private static final TimestampedValue<Long> ARBITRARY_TIME =
new TimestampedValue<>(1111L, 2222L);
@Test
public void testEquals() {
NetworkTimeSuggestion one = new NetworkTimeSuggestion(ARBITRARY_TIME);
assertEquals(one, one);
NetworkTimeSuggestion two = new NetworkTimeSuggestion(ARBITRARY_TIME);
assertEquals(one, two);
assertEquals(two, one);
TimestampedValue<Long> differentTime = new TimestampedValue<>(
ARBITRARY_TIME.getReferenceTimeMillis() + 1,
ARBITRARY_TIME.getValue());
NetworkTimeSuggestion three = new NetworkTimeSuggestion(differentTime);
assertNotEquals(one, three);
assertNotEquals(three, one);
// DebugInfo must not be considered in equals().
one.addDebugInfo("Debug info 1");
two.addDebugInfo("Debug info 2");
assertEquals(one, two);
}
@Test
public void testParcelable() {
NetworkTimeSuggestion suggestion = new NetworkTimeSuggestion(ARBITRARY_TIME);
assertRoundTripParcelable(suggestion);
// DebugInfo should also be stored (but is not checked by equals()
suggestion.addDebugInfo("This is debug info");
NetworkTimeSuggestion rtSuggestion = roundTripParcelable(suggestion);
assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo());
}
}

View File

@@ -18,6 +18,8 @@ package com.android.server;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.timedetector.NetworkTimeSuggestion;
import android.app.timedetector.TimeDetector;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -35,10 +37,10 @@ import android.os.Message;
import android.os.PowerManager;
import android.os.SystemClock;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.util.NtpTrustedTime;
import android.util.TimeUtils;
import android.util.TimestampedValue;
import com.android.internal.util.DumpUtils;
@@ -46,21 +48,19 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
/**
* Monitors the network time and updates the system time if it is out of sync
* and there hasn't been any NITZ update from the carrier recently.
* If looking up the network time fails for some reason, it tries a few times with a short
* interval and then resets to checking on longer intervals.
* <p>
* If the user enables AUTO_TIME, it will check immediately for the network time, if NITZ wasn't
* available.
* </p>
* Monitors the network time. If looking up the network time fails for some reason, it tries a few
* times with a short interval and then resets to checking on longer intervals.
*
* <p>When available, the time is always suggested to the {@link
* com.android.server.timedetector.TimeDetectorService} where it may be used to set the device
* system clock, depending on user settings and what other signals are available.
*/
public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeUpdateService {
private static final String TAG = "NetworkTimeUpdateService";
private static final boolean DBG = false;
private static final int EVENT_AUTO_TIME_CHANGED = 1;
private static final int EVENT_AUTO_TIME_ENABLED = 1;
private static final int EVENT_POLL_NETWORK_TIME = 2;
private static final int EVENT_NETWORK_CHANGED = 3;
@@ -69,20 +69,19 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
private static final int POLL_REQUEST = 0;
private static final long NOT_SET = -1;
private long mNitzTimeSetTime = NOT_SET;
private Network mDefaultNetwork = null;
private final Context mContext;
private final NtpTrustedTime mTime;
private final AlarmManager mAlarmManager;
private final TimeDetector mTimeDetector;
private final ConnectivityManager mCM;
private final PendingIntent mPendingPollIntent;
private final PowerManager.WakeLock mWakeLock;
// NTP lookup is done on this thread and handler
private Handler mHandler;
private SettingsObserver mSettingsObserver;
private AutoTimeSettingObserver mAutoTimeSettingObserver;
private NetworkTimeUpdateCallback mNetworkTimeUpdateCallback;
// Normal polling frequency
@@ -91,8 +90,6 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
private final long mPollingIntervalShorterMs;
// Number of times to try again
private final int mTryAgainTimesMax;
// If the time difference is greater than this threshold, then update the time.
private final int mTimeErrorThresholdMs;
// Keeps track of how many quick attempts were made to fetch NTP time.
// During bootup, the network may not have been up yet, or it's taking time for the
// connection to happen.
@@ -102,6 +99,7 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
mContext = context;
mTime = NtpTrustedTime.getInstance(context);
mAlarmManager = mContext.getSystemService(AlarmManager.class);
mTimeDetector = mContext.getSystemService(TimeDetector.class);
mCM = mContext.getSystemService(ConnectivityManager.class);
Intent pollIntent = new Intent(ACTION_POLL, null);
@@ -113,8 +111,6 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
com.android.internal.R.integer.config_ntpPollingIntervalShorter);
mTryAgainTimesMax = mContext.getResources().getInteger(
com.android.internal.R.integer.config_ntpRetry);
mTimeErrorThresholdMs = mContext.getResources().getInteger(
com.android.internal.R.integer.config_ntpThreshold);
mWakeLock = context.getSystemService(PowerManager.class).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, TAG);
@@ -122,7 +118,6 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
@Override
public void systemRunning() {
registerForTelephonyIntents();
registerForAlarms();
HandlerThread thread = new HandlerThread(TAG);
@@ -131,14 +126,9 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
mNetworkTimeUpdateCallback = new NetworkTimeUpdateCallback();
mCM.registerDefaultNetworkCallback(mNetworkTimeUpdateCallback, mHandler);
mSettingsObserver = new SettingsObserver(mHandler, EVENT_AUTO_TIME_CHANGED);
mSettingsObserver.observe(mContext);
}
private void registerForTelephonyIntents() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TelephonyManager.ACTION_NETWORK_SET_TIME);
mContext.registerReceiver(mNitzReceiver, intentFilter);
mAutoTimeSettingObserver = new AutoTimeSettingObserver(mContext, mHandler,
EVENT_AUTO_TIME_ENABLED);
mAutoTimeSettingObserver.observe();
}
private void registerForAlarms() {
@@ -152,8 +142,7 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
}
private void onPollNetworkTime(int event) {
// If Automatic time is not set, don't bother. Similarly, if we don't
// have any default network, don't bother.
// If we don't have any default network, don't bother.
if (mDefaultNetwork == null) return;
mWakeLock.acquire();
try {
@@ -173,10 +162,12 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
if (mTime.getCacheAge() < mPollingIntervalMs) {
// Obtained fresh fix; schedule next normal update
resetAlarm(mPollingIntervalMs);
if (isAutomaticTimeRequested()) {
updateSystemClock(event);
}
// Suggest the time to the time detector. It may choose use it to set the system clock.
TimestampedValue<Long> timeSignal = mTime.getCachedNtpTimeSignal();
NetworkTimeSuggestion timeSuggestion = new NetworkTimeSuggestion(timeSignal);
timeSuggestion.addDebugInfo("Origin: NetworkTimeUpdateServiceImpl. event=" + event);
mTimeDetector.suggestNetworkTime(timeSuggestion);
} else {
// No fresh fix; schedule retry
mTryAgainCounter++;
@@ -190,36 +181,6 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
}
}
private long getNitzAge() {
if (mNitzTimeSetTime == NOT_SET) {
return Long.MAX_VALUE;
} else {
return SystemClock.elapsedRealtime() - mNitzTimeSetTime;
}
}
/**
* Consider updating system clock based on current NTP fix, if requested by
* user, significant enough delta, and we don't have a recent NITZ.
*/
private void updateSystemClock(int event) {
final boolean forceUpdate = (event == EVENT_AUTO_TIME_CHANGED);
if (!forceUpdate) {
if (getNitzAge() < mPollingIntervalMs) {
if (DBG) Log.d(TAG, "Ignoring NTP update due to recent NITZ");
return;
}
final long skew = Math.abs(mTime.currentTimeMillis() - System.currentTimeMillis());
if (skew < mTimeErrorThresholdMs) {
if (DBG) Log.d(TAG, "Ignoring NTP update due to low skew");
return;
}
}
SystemClock.setCurrentTimeMillis(mTime.currentTimeMillis());
}
/**
* Cancel old alarm and starts a new one for the specified interval.
*
@@ -232,27 +193,6 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, next, mPendingPollIntent);
}
/**
* Checks if the user prefers to automatically set the time.
*/
private boolean isAutomaticTimeRequested() {
return Settings.Global.getInt(
mContext.getContentResolver(), Settings.Global.AUTO_TIME, 0) != 0;
}
/** Receiver for Nitz time events */
private BroadcastReceiver mNitzReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DBG) Log.d(TAG, "Received " + action);
if (TelephonyManager.ACTION_NETWORK_SET_TIME.equals(action)) {
mNitzTimeSetTime = SystemClock.elapsedRealtime();
}
}
};
/** Handler to do the network accesses on */
private class MyHandler extends Handler {
@@ -263,7 +203,7 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_AUTO_TIME_CHANGED:
case EVENT_AUTO_TIME_ENABLED:
case EVENT_POLL_NETWORK_TIME:
case EVENT_NETWORK_CHANGED:
onPollNetworkTime(msg.what);
@@ -287,27 +227,42 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
}
}
/** Observer to watch for changes to the AUTO_TIME setting */
private static class SettingsObserver extends ContentObserver {
/**
* Observer to watch for changes to the AUTO_TIME setting. It only triggers when the setting
* is enabled.
*/
private static class AutoTimeSettingObserver extends ContentObserver {
private int mMsg;
private Handler mHandler;
private final Context mContext;
private final int mMsg;
private final Handler mHandler;
SettingsObserver(Handler handler, int msg) {
AutoTimeSettingObserver(Context context, Handler handler, int msg) {
super(handler);
mContext = context;
mHandler = handler;
mMsg = msg;
}
void observe(Context context) {
ContentResolver resolver = context.getContentResolver();
void observe() {
ContentResolver resolver = mContext.getContentResolver();
resolver.registerContentObserver(Settings.Global.getUriFor(Settings.Global.AUTO_TIME),
false, this);
}
@Override
public void onChange(boolean selfChange) {
mHandler.obtainMessage(mMsg).sendToTarget();
if (isAutomaticTimeEnabled()) {
mHandler.obtainMessage(mMsg).sendToTarget();
}
}
/**
* Checks if the user prefers to automatically set the time.
*/
private boolean isAutomaticTimeEnabled() {
ContentResolver resolver = mContext.getContentResolver();
return Settings.Global.getInt(resolver, Settings.Global.AUTO_TIME, 0) != 0;
}
}
@@ -319,8 +274,6 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
pw.print("\nPollingIntervalShorterMs: ");
TimeUtils.formatDuration(mPollingIntervalShorterMs, pw);
pw.println("\nTryAgainTimesMax: " + mTryAgainTimesMax);
pw.print("TimeErrorThresholdMs: ");
TimeUtils.formatDuration(mTimeErrorThresholdMs, pw);
pw.println("\nTryAgainCounter: " + mTryAgainCounter);
pw.println("NTP cache age: " + mTime.getCacheAge());
pw.println("NTP cache certainty: " + mTime.getCacheCertainty());

View File

@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.timedetector.ITimeDetectorService;
import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.NetworkTimeSuggestion;
import android.app.timedetector.PhoneTimeSuggestion;
import android.content.ContentResolver;
import android.content.Context;
@@ -105,6 +106,14 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
mHandler.post(() -> mTimeDetectorStrategy.suggestManualTime(timeSignal));
}
@Override
public void suggestNetworkTime(@NonNull NetworkTimeSuggestion timeSignal) {
enforceSuggestNetworkTimePermission();
Objects.requireNonNull(timeSignal);
mHandler.post(() -> mTimeDetectorStrategy.suggestNetworkTime(timeSignal));
}
@VisibleForTesting
public void handleAutoTimeDetectionToggle() {
mHandler.post(mTimeDetectorStrategy::handleAutoTimeDetectionChanged);
@@ -129,4 +138,10 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE,
"suggest manual time and time zone");
}
private void enforceSuggestNetworkTimePermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.SET_TIME,
"set time");
}
}

View File

@@ -19,6 +19,7 @@ package com.android.server.timedetector;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.NetworkTimeSuggestion;
import android.app.timedetector.PhoneTimeSuggestion;
import android.content.Intent;
import android.util.TimestampedValue;
@@ -86,6 +87,9 @@ public interface TimeDetectorStrategy {
/** Process the suggested manually entered time. */
void suggestManualTime(@NonNull ManualTimeSuggestion timeSuggestion);
/** Process the suggested time from network sources. */
void suggestNetworkTime(@NonNull NetworkTimeSuggestion timeSuggestion);
/** Handle the auto-time setting being toggled on or off. */
void handleAutoTimeDetectionChanged();

View File

@@ -21,6 +21,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AlarmManager;
import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.NetworkTimeSuggestion;
import android.app.timedetector.PhoneTimeSuggestion;
import android.content.Intent;
import android.telephony.TelephonyManager;
@@ -32,6 +33,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.timezonedetector.ArrayMapWithHistory;
import com.android.server.timezonedetector.ReferenceWithHistory;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -56,11 +58,11 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
/** 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. */
/** Phone and network suggestions older than this value are considered too old to be used. */
@VisibleForTesting
static final long PHONE_MAX_AGE_MILLIS = PHONE_BUCKET_COUNT * PHONE_BUCKET_SIZE_MILLIS;
static final long MAX_UTC_TIME_AGE_MILLIS = PHONE_BUCKET_COUNT * PHONE_BUCKET_SIZE_MILLIS;
@IntDef({ ORIGIN_PHONE, ORIGIN_MANUAL })
@IntDef({ ORIGIN_PHONE, ORIGIN_MANUAL, ORIGIN_NETWORK })
@Retention(RetentionPolicy.SOURCE)
public @interface Origin {}
@@ -72,6 +74,10 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
@Origin
private static final int ORIGIN_MANUAL = 2;
/** Used when a time value originated from a network signal. */
@Origin
private static final int ORIGIN_NETWORK = 3;
/**
* 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
@@ -101,9 +107,13 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
* will have a small number of telephony devices and phoneIds are assumed to be stable.
*/
@GuardedBy("this")
private ArrayMapWithHistory<Integer, PhoneTimeSuggestion> mSuggestionByPhoneId =
private final ArrayMapWithHistory<Integer, PhoneTimeSuggestion> mSuggestionByPhoneId =
new ArrayMapWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
@GuardedBy("this")
private final ReferenceWithHistory<NetworkTimeSuggestion> mLastNetworkSuggestion =
new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
@Override
public void initialize(@NonNull Callback callback) {
mCallback = callback;
@@ -121,6 +131,19 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
setSystemClockIfRequired(ORIGIN_MANUAL, newUtcTime, cause);
}
@Override
public synchronized void suggestNetworkTime(@NonNull NetworkTimeSuggestion timeSuggestion) {
if (!validateSuggestionTime(timeSuggestion.getUtcTime(), timeSuggestion)) {
return;
}
mLastNetworkSuggestion.set(timeSuggestion);
// Now perform auto time detection. The new suggestion may be used to modify the system
// clock.
String reason = "New network time suggested. timeSuggestion=" + timeSuggestion;
doAutoTimeDetection(reason);
}
@Override
public synchronized void suggestPhoneTime(@NonNull PhoneTimeSuggestion timeSuggestion) {
// Empty time suggestion means that telephony network connectivity has been lost.
@@ -184,6 +207,11 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
mSuggestionByPhoneId.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.println("Network suggestion history:");
ipw.increaseIndent(); // level 2
mLastNetworkSuggestion.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.decreaseIndent(); // level 1
ipw.flush();
}
@@ -253,23 +281,34 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
return;
}
// Android devices currently prioritize any telephony over network signals. There are
// carrier compliance tests that would need to be changed before we could ignore NITZ or
// prefer NTP generally. This check is cheap on devices without phone hardware.
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);
}
if (bestPhoneSuggestion != null) {
final TimestampedValue<Long> newUtcTime = bestPhoneSuggestion.getUtcTime();
String cause = "Found good phone suggestion."
+ ", bestPhoneSuggestion=" + bestPhoneSuggestion
+ ", detectionReason=" + detectionReason;
setSystemClockIfRequired(ORIGIN_PHONE, newUtcTime, cause);
return;
}
final TimestampedValue<Long> newUtcTime = bestPhoneSuggestion.getUtcTime();
String cause = "Found good suggestion."
+ ", bestPhoneSuggestion=" + bestPhoneSuggestion
+ ", detectionReason=" + detectionReason;
setSystemClockIfRequired(ORIGIN_PHONE, newUtcTime, cause);
// There is no good phone suggestion, try network.
NetworkTimeSuggestion networkSuggestion = findLatestValidNetworkSuggestion();
if (networkSuggestion != null) {
final TimestampedValue<Long> newUtcTime = networkSuggestion.getUtcTime();
String cause = "Found good network suggestion."
+ ", networkSuggestion=" + networkSuggestion
+ ", detectionReason=" + detectionReason;
setSystemClockIfRequired(ORIGIN_NETWORK, newUtcTime, cause);
return;
}
if (DBG) {
Slog.d(LOG_TAG, "Could not determine time: No best phone or network suggestion."
+ " detectionReason=" + detectionReason);
}
}
@GuardedBy("this")
@@ -348,37 +387,50 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
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.
// Validate first.
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. "
if (!validateSuggestionUtcTime(elapsedRealtimeMillis, utcTime)) {
Slog.w(LOG_TAG, "Existing suggestion found to be invalid "
+ " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+ ", timeSuggestion=" + timeSuggestion);
return PHONE_INVALID_SCORE;
}
long ageMillis = elapsedRealtimeMillis - referenceTimeMillis;
// 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.
long ageMillis = elapsedRealtimeMillis - utcTime.getReferenceTimeMillis();
// 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) {
// Turn the age into a discrete value: 0 <= bucketIndex < PHONE_BUCKET_COUNT.
int bucketIndex = (int) (ageMillis / PHONE_BUCKET_SIZE_MILLIS);
if (bucketIndex >= PHONE_BUCKET_COUNT) {
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;
}
/** Returns the latest, valid, network suggestion. Returns {@code null} if there isn't one. */
@GuardedBy("this")
@Nullable
private NetworkTimeSuggestion findLatestValidNetworkSuggestion() {
NetworkTimeSuggestion networkSuggestion = mLastNetworkSuggestion.get();
if (networkSuggestion == null) {
// No network suggestions received. This is normal if there's no connectivity.
return null;
}
TimestampedValue<Long> utcTime = networkSuggestion.getUtcTime();
long elapsedRealTimeMillis = mCallback.elapsedRealtimeMillis();
if (!validateSuggestionUtcTime(elapsedRealTimeMillis, utcTime)) {
// The latest suggestion is not valid, usually due to its age.
return null;
}
return networkSuggestion;
}
@GuardedBy("this")
private void setSystemClockIfRequired(
@Origin int origin, @NonNull TimestampedValue<Long> time, @NonNull String cause) {
@@ -415,7 +467,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
}
private static boolean isOriginAutomatic(@Origin int origin) {
return origin == ORIGIN_PHONE;
return origin != ORIGIN_MANUAL;
}
@GuardedBy("this")
@@ -506,6 +558,16 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
return findBestPhoneSuggestion();
}
/**
* Returns the latest valid network suggestion. Not intended for general use: it is used during
* tests to check strategy behavior.
*/
@VisibleForTesting
@Nullable
public NetworkTimeSuggestion findLatestValidNetworkSuggestionForTests() {
return findLatestValidNetworkSuggestion();
}
/**
* A method used to inspect state during tests. Not intended for general use.
*/
@@ -514,4 +576,32 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
public synchronized PhoneTimeSuggestion getLatestPhoneSuggestion(int phoneId) {
return mSuggestionByPhoneId.get(phoneId);
}
/**
* A method used to inspect state during tests. Not intended for general use.
*/
@VisibleForTesting
@Nullable
public NetworkTimeSuggestion getLatestNetworkSuggestion() {
return mLastNetworkSuggestion.get();
}
private static boolean validateSuggestionUtcTime(
long elapsedRealtimeMillis, TimestampedValue<Long> utcTime) {
long referenceTimeMillis = utcTime.getReferenceTimeMillis();
if (referenceTimeMillis > elapsedRealtimeMillis) {
// Future reference times are ignored. They imply the reference time was wrong, or the
// elapsed realtime clock used to derive it has gone backwards, neither of which are
// supportable situations.
return false;
}
// 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.
long ageMillis = elapsedRealtimeMillis - referenceTimeMillis;
return ageMillis <= MAX_UTC_TIME_AGE_MILLIS;
}
}

View File

@@ -29,6 +29,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.NetworkTimeSuggestion;
import android.app.timedetector.PhoneTimeSuggestion;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -143,6 +144,36 @@ public class TimeDetectorServiceTest {
mStubbedTimeDetectorStrategy.verifySuggestManualTimeCalled(manualTimeSuggestion);
}
@Test(expected = SecurityException.class)
public void testSuggestNetworkTime_withoutPermission() {
doThrow(new SecurityException("Mock"))
.when(mMockContext).enforceCallingOrSelfPermission(anyString(), any());
NetworkTimeSuggestion NetworkTimeSuggestion = createNetworkTimeSuggestion();
try {
mTimeDetectorService.suggestNetworkTime(NetworkTimeSuggestion);
fail();
} finally {
verify(mMockContext).enforceCallingOrSelfPermission(
eq(android.Manifest.permission.SET_TIME), anyString());
}
}
@Test
public void testSuggestNetworkTime() throws Exception {
doNothing().when(mMockContext).enforceCallingOrSelfPermission(anyString(), any());
NetworkTimeSuggestion NetworkTimeSuggestion = createNetworkTimeSuggestion();
mTimeDetectorService.suggestNetworkTime(NetworkTimeSuggestion);
mTestHandler.assertTotalMessagesEnqueued(1);
verify(mMockContext).enforceCallingOrSelfPermission(
eq(android.Manifest.permission.SET_TIME), anyString());
mTestHandler.waitForEmptyQueue();
mStubbedTimeDetectorStrategy.verifySuggestNetworkTimeCalled(NetworkTimeSuggestion);
}
@Test
public void testDump() {
when(mMockContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP))
@@ -180,11 +211,17 @@ public class TimeDetectorServiceTest {
return new ManualTimeSuggestion(timeValue);
}
private static NetworkTimeSuggestion createNetworkTimeSuggestion() {
TimestampedValue<Long> timeValue = new TimestampedValue<>(100L, 1_000_000L);
return new NetworkTimeSuggestion(timeValue);
}
private static class StubbedTimeDetectorStrategy implements TimeDetectorStrategy {
// Call tracking.
private PhoneTimeSuggestion mLastPhoneSuggestion;
private ManualTimeSuggestion mLastManualSuggestion;
private NetworkTimeSuggestion mLastNetworkSuggestion;
private boolean mLastAutoTimeDetectionToggleCalled;
private boolean mDumpCalled;
@@ -204,6 +241,12 @@ public class TimeDetectorServiceTest {
mLastManualSuggestion = timeSuggestion;
}
@Override
public void suggestNetworkTime(NetworkTimeSuggestion timeSuggestion) {
resetCallTracking();
mLastNetworkSuggestion = timeSuggestion;
}
@Override
public void handleAutoTimeDetectionChanged() {
resetCallTracking();
@@ -219,6 +262,7 @@ public class TimeDetectorServiceTest {
void resetCallTracking() {
mLastPhoneSuggestion = null;
mLastManualSuggestion = null;
mLastNetworkSuggestion = null;
mLastAutoTimeDetectionToggleCalled = false;
mDumpCalled = false;
}
@@ -231,6 +275,10 @@ public class TimeDetectorServiceTest {
assertEquals(expectedSuggestion, mLastManualSuggestion);
}
public void verifySuggestNetworkTimeCalled(NetworkTimeSuggestion expectedSuggestion) {
assertEquals(expectedSuggestion, mLastNetworkSuggestion);
}
void verifyHandleAutoTimeDetectionToggleCalled() {
assertTrue(mLastAutoTimeDetectionToggleCalled);
}

View File

@@ -24,6 +24,7 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.NetworkTimeSuggestion;
import android.app.timedetector.PhoneTimeSuggestion;
import android.content.Intent;
import android.icu.util.Calendar;
@@ -45,14 +46,16 @@ public class TimeDetectorStrategyImplTest {
private static final TimestampedValue<Long> ARBITRARY_CLOCK_INITIALIZATION_INFO =
new TimestampedValue<>(
123456789L /* realtimeClockMillis */,
createUtcTime(1977, 1, 1, 12, 0, 0));
createUtcTime(2008, 5, 23, 12, 0, 0));
/**
* An arbitrary time, very different from the {@link #ARBITRARY_CLOCK_INITIALIZATION_INFO}
* time. Can be used as the basis for time suggestions.
*/
private static final long ARBITRARY_TEST_TIME_MILLIS = createUtcTime(2018, 1, 1, 12, 0, 0);
private static final int ARBITRARY_PHONE_ID = 123456;
private static final long ONE_DAY_MILLIS = Duration.ofDays(1).toMillis();
private Script mScript;
@Before
@@ -67,15 +70,16 @@ public class TimeDetectorStrategyImplTest {
int phoneId = ARBITRARY_PHONE_ID;
long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS;
PhoneTimeSuggestion timeSuggestion =
mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis);
int clockIncrement = 1000;
long expectedSystemClockMillis = testTimeMillis + clockIncrement;
mScript.simulateTimePassing()
.simulatePhoneTimeSuggestion(timeSuggestion);
mScript.simulateTimePassing(clockIncrement)
.simulatePhoneTimeSuggestion(timeSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
expectedSystemClockMillis, true /* expectNetworkBroadcast */)
long expectedSystemClockMillis =
mScript.calculateTimeInMillisForNow(timeSuggestion.getUtcTime());
mScript.verifySystemClockWasSetAndResetCallTracking(
expectedSystemClockMillis, true /* expectNetworkBroadcast */)
.assertLatestPhoneSuggestion(phoneId, timeSuggestion);
}
@@ -94,26 +98,24 @@ public class TimeDetectorStrategyImplTest {
@Test
public void testSuggestPhoneTime_systemClockThreshold() {
int systemClockUpdateThresholdMillis = 1000;
final int systemClockUpdateThresholdMillis = 1000;
final int clockIncrementMillis = 100;
mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
.pokeThresholds(systemClockUpdateThresholdMillis)
.pokeAutoTimeDetectionEnabled(true);
final int clockIncrement = 100;
int phoneId = ARBITRARY_PHONE_ID;
// Send the first time signal. It should be used.
{
long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS;
PhoneTimeSuggestion timeSuggestion1 =
mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis);
TimestampedValue<Long> utcTime1 = timeSuggestion1.getUtcTime();
mScript.generatePhoneTimeSuggestion(phoneId, ARBITRARY_TEST_TIME_MILLIS);
// Increment the the device clocks to simulate the passage of time.
mScript.simulateTimePassing(clockIncrement);
mScript.simulateTimePassing(clockIncrementMillis);
long expectedSystemClockMillis1 =
TimeDetectorStrategy.getTimeAt(utcTime1, mScript.peekElapsedRealtimeMillis());
mScript.calculateTimeInMillisForNow(timeSuggestion1.getUtcTime());
mScript.simulatePhoneTimeSuggestion(timeSuggestion1)
.verifySystemClockWasSetAndResetCallTracking(
@@ -127,7 +129,7 @@ public class TimeDetectorStrategyImplTest {
int underThresholdMillis = systemClockUpdateThresholdMillis - 1;
PhoneTimeSuggestion timeSuggestion2 = mScript.generatePhoneTimeSuggestion(
phoneId, mScript.peekSystemClockMillis() + underThresholdMillis);
mScript.simulateTimePassing(clockIncrement)
mScript.simulateTimePassing(clockIncrementMillis)
.simulatePhoneTimeSuggestion(timeSuggestion2)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestPhoneSuggestion(phoneId, timeSuggestion2);
@@ -138,11 +140,10 @@ public class TimeDetectorStrategyImplTest {
PhoneTimeSuggestion timeSuggestion3 = mScript.generatePhoneTimeSuggestion(
phoneId,
mScript.peekSystemClockMillis() + systemClockUpdateThresholdMillis);
mScript.simulateTimePassing(clockIncrement);
mScript.simulateTimePassing(clockIncrementMillis);
long expectedSystemClockMillis3 =
TimeDetectorStrategy.getTimeAt(timeSuggestion3.getUtcTime(),
mScript.peekElapsedRealtimeMillis());
mScript.calculateTimeInMillisForNow(timeSuggestion3.getUtcTime());
mScript.simulatePhoneTimeSuggestion(timeSuggestion3)
.verifySystemClockWasSetAndResetCallTracking(
@@ -162,17 +163,16 @@ public class TimeDetectorStrategyImplTest {
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;
long phone2TimeMillis = ARBITRARY_TEST_TIME_MILLIS + Duration.ofDays(1).toMillis();
// Make a suggestion with phone2Id.
{
PhoneTimeSuggestion phone2TimeSuggestion =
mScript.generatePhoneTimeSuggestion(phone2Id, phone2TimeMillis);
mScript.simulateTimePassing(clockIncrement);
mScript.simulateTimePassing();
long expectedSystemClockMillis = phone2TimeMillis + clockIncrement;
long expectedSystemClockMillis =
mScript.calculateTimeInMillisForNow(phone2TimeSuggestion.getUtcTime());
mScript.simulatePhoneTimeSuggestion(phone2TimeSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
@@ -181,15 +181,16 @@ public class TimeDetectorStrategyImplTest {
.assertLatestPhoneSuggestion(phone2Id, phone2TimeSuggestion);
}
mScript.simulateTimePassing(clockIncrement);
mScript.simulateTimePassing();
// Now make a different suggestion with phone1Id.
{
PhoneTimeSuggestion phone1TimeSuggestion =
mScript.generatePhoneTimeSuggestion(phone1Id, phone1TimeMillis);
mScript.simulateTimePassing(clockIncrement);
mScript.simulateTimePassing();
long expectedSystemClockMillis = phone1TimeMillis + clockIncrement;
long expectedSystemClockMillis =
mScript.calculateTimeInMillisForNow(phone1TimeSuggestion.getUtcTime());
mScript.simulatePhoneTimeSuggestion(phone1TimeSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
@@ -198,14 +199,14 @@ public class TimeDetectorStrategyImplTest {
}
mScript.simulateTimePassing(clockIncrement);
mScript.simulateTimePassing();
// 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.simulateTimePassing();
mScript.simulatePhoneTimeSuggestion(phone2TimeSuggestion)
.verifySystemClockWasNotSetAndResetCallTracking()
@@ -220,9 +221,10 @@ public class TimeDetectorStrategyImplTest {
{
PhoneTimeSuggestion phone2TimeSuggestion =
mScript.generatePhoneTimeSuggestion(phone2Id, phone2TimeMillis);
mScript.simulateTimePassing(clockIncrement);
mScript.simulateTimePassing();
long expectedSystemClockMillis = phone2TimeMillis + clockIncrement;
long expectedSystemClockMillis =
mScript.calculateTimeInMillisForNow(phone2TimeSuggestion.getUtcTime());
mScript.simulatePhoneTimeSuggestion(phone2TimeSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
@@ -239,7 +241,7 @@ public class TimeDetectorStrategyImplTest {
int phoneId = ARBITRARY_PHONE_ID;
PhoneTimeSuggestion timeSuggestion =
mScript.generatePhoneTimeSuggestion(phoneId, ARBITRARY_TEST_TIME_MILLIS);
mScript.simulateTimePassing(1000)
mScript.simulateTimePassing()
.simulatePhoneTimeSuggestion(timeSuggestion)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestPhoneSuggestion(phoneId, timeSuggestion);
@@ -260,9 +262,8 @@ public class TimeDetectorStrategyImplTest {
TimestampedValue<Long> utcTime1 = timeSuggestion1.getUtcTime();
// Initialize the strategy / device with a time set from a phone suggestion.
mScript.simulateTimePassing(100);
long expectedSystemClockMillis1 =
TimeDetectorStrategy.getTimeAt(utcTime1, mScript.peekElapsedRealtimeMillis());
mScript.simulateTimePassing();
long expectedSystemClockMillis1 = mScript.calculateTimeInMillisForNow(utcTime1);
mScript.simulatePhoneTimeSuggestion(timeSuggestion1)
.verifySystemClockWasSetAndResetCallTracking(
expectedSystemClockMillis1, true /* expectNetworkBroadcast */)
@@ -299,8 +300,7 @@ public class TimeDetectorStrategyImplTest {
long validReferenceTimeMillis = utcTime1.getReferenceTimeMillis() + 100;
TimestampedValue<Long> utcTime4 = new TimestampedValue<>(
validReferenceTimeMillis, validUtcTimeMillis);
long expectedSystemClockMillis4 =
TimeDetectorStrategy.getTimeAt(utcTime4, mScript.peekElapsedRealtimeMillis());
long expectedSystemClockMillis4 = mScript.calculateTimeInMillisForNow(utcTime4);
PhoneTimeSuggestion timeSuggestion4 =
createPhoneTimeSuggestion(phoneId, utcTime4);
mScript.simulatePhoneTimeSuggestion(timeSuggestion4)
@@ -335,8 +335,7 @@ public class TimeDetectorStrategyImplTest {
// Simulate more time passing.
mScript.simulateTimePassing(clockIncrementMillis);
long expectedSystemClockMillis1 =
TimeDetectorStrategy.getTimeAt(utcTime1, mScript.peekElapsedRealtimeMillis());
long expectedSystemClockMillis1 = mScript.calculateTimeInMillisForNow(utcTime1);
// Turn on auto time detection.
mScript.simulateAutoTimeDetectionToggle()
@@ -357,8 +356,8 @@ public class TimeDetectorStrategyImplTest {
// Simulate more time passing.
mScript.simulateTimePassing(clockIncrementMillis);
long expectedSystemClockMillis2 = TimeDetectorStrategy.getTimeAt(
timeSuggestion2.getUtcTime(), mScript.peekElapsedRealtimeMillis());
long expectedSystemClockMillis2 =
mScript.calculateTimeInMillisForNow(timeSuggestion2.getUtcTime());
// The new time, though valid, should not be set in the system clock because auto time is
// disabled.
@@ -382,19 +381,21 @@ public class TimeDetectorStrategyImplTest {
long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS;
PhoneTimeSuggestion phoneSuggestion =
mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis);
int clockIncrementMillis = 1000;
mScript.simulateTimePassing(clockIncrementMillis)
.simulatePhoneTimeSuggestion(phoneSuggestion)
mScript.simulateTimePassing();
long expectedSystemClockMillis =
mScript.calculateTimeInMillisForNow(phoneSuggestion.getUtcTime());
mScript.simulatePhoneTimeSuggestion(phoneSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
testTimeMillis + clockIncrementMillis, true /* expectedNetworkBroadcast */)
expectedSystemClockMillis, 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(TimeDetectorStrategyImpl.PHONE_MAX_AGE_MILLIS);
mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_UTC_TIME_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.
@@ -407,13 +408,14 @@ public class TimeDetectorStrategyImplTest {
mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
.pokeAutoTimeDetectionEnabled(false);
long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS;
ManualTimeSuggestion timeSuggestion = mScript.generateManualTimeSuggestion(testTimeMillis);
final int clockIncrement = 1000;
long expectedSystemClockMillis = testTimeMillis + clockIncrement;
ManualTimeSuggestion timeSuggestion =
mScript.generateManualTimeSuggestion(ARBITRARY_TEST_TIME_MILLIS);
mScript.simulateTimePassing(clockIncrement)
.simulateManualTimeSuggestion(timeSuggestion)
mScript.simulateTimePassing();
long expectedSystemClockMillis =
mScript.calculateTimeInMillisForNow(timeSuggestion.getUtcTime());
mScript.simulateManualTimeSuggestion(timeSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
expectedSystemClockMillis, false /* expectNetworkBroadcast */);
}
@@ -430,21 +432,19 @@ public class TimeDetectorStrategyImplTest {
long testTimeMillis = ARBITRARY_TEST_TIME_MILLIS;
PhoneTimeSuggestion phoneTimeSuggestion =
mScript.generatePhoneTimeSuggestion(phoneId, testTimeMillis);
long expectedAutoClockMillis = phoneTimeSuggestion.getUtcTime().getValue();
final int clockIncrement = 1000;
// Simulate the passage of time.
mScript.simulateTimePassing(clockIncrement);
expectedAutoClockMillis += clockIncrement;
mScript.simulateTimePassing();
long expectedAutoClockMillis =
mScript.calculateTimeInMillisForNow(phoneTimeSuggestion.getUtcTime());
mScript.simulatePhoneTimeSuggestion(phoneTimeSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
expectedAutoClockMillis, true /* expectNetworkBroadcast */)
.assertLatestPhoneSuggestion(phoneId, phoneTimeSuggestion);
// Simulate the passage of time.
mScript.simulateTimePassing(clockIncrement);
expectedAutoClockMillis += clockIncrement;
mScript.simulateTimePassing();
// Switch to manual.
mScript.simulateAutoTimeDetectionToggle()
@@ -452,26 +452,29 @@ public class TimeDetectorStrategyImplTest {
.assertLatestPhoneSuggestion(phoneId, phoneTimeSuggestion);
// Simulate the passage of time.
mScript.simulateTimePassing(clockIncrement);
expectedAutoClockMillis += clockIncrement;
mScript.simulateTimePassing();
// Simulate a manual suggestion 1 day different from the auto suggestion.
long manualTimeMillis = testTimeMillis + ONE_DAY_MILLIS;
long expectedManualClockMillis = manualTimeMillis;
long manualTimeMillis = testTimeMillis + Duration.ofDays(1).toMillis();
ManualTimeSuggestion manualTimeSuggestion =
mScript.generateManualTimeSuggestion(manualTimeMillis);
mScript.simulateTimePassing();
long expectedManualClockMillis =
mScript.calculateTimeInMillisForNow(manualTimeSuggestion.getUtcTime());
mScript.simulateManualTimeSuggestion(manualTimeSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
expectedManualClockMillis, false /* expectNetworkBroadcast */)
.assertLatestPhoneSuggestion(phoneId, phoneTimeSuggestion);
// Simulate the passage of time.
mScript.simulateTimePassing(clockIncrement);
expectedAutoClockMillis += clockIncrement;
mScript.simulateTimePassing();
// Switch back to auto.
mScript.simulateAutoTimeDetectionToggle();
expectedAutoClockMillis =
mScript.calculateTimeInMillisForNow(phoneTimeSuggestion.getUtcTime());
mScript.verifySystemClockWasSetAndResetCallTracking(
expectedAutoClockMillis, true /* expectNetworkBroadcast */)
.assertLatestPhoneSuggestion(phoneId, phoneTimeSuggestion);
@@ -492,13 +495,143 @@ public class TimeDetectorStrategyImplTest {
ManualTimeSuggestion timeSuggestion =
mScript.generateManualTimeSuggestion(ARBITRARY_TEST_TIME_MILLIS);
final int clockIncrement = 1000;
mScript.simulateTimePassing(clockIncrement)
mScript.simulateTimePassing()
.simulateManualTimeSuggestion(timeSuggestion)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@Test
public void testSuggestNetworkTime_autoTimeEnabled() {
mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
.pokeAutoTimeDetectionEnabled(true);
NetworkTimeSuggestion timeSuggestion =
mScript.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME_MILLIS);
mScript.simulateTimePassing();
long expectedSystemClockMillis =
mScript.calculateTimeInMillisForNow(timeSuggestion.getUtcTime());
mScript.simulateNetworkTimeSuggestion(timeSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
expectedSystemClockMillis, false /* expectNetworkBroadcast */);
}
@Test
public void testSuggestNetworkTime_autoTimeDisabled() {
mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
.pokeAutoTimeDetectionEnabled(false);
NetworkTimeSuggestion timeSuggestion =
mScript.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME_MILLIS);
mScript.simulateTimePassing()
.simulateNetworkTimeSuggestion(timeSuggestion)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@Test
public void testSuggestNetworkTime_phoneSuggestionsBeatNetworkSuggestions() {
mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
.pokeAutoTimeDetectionEnabled(true);
// Three obviously different times that could not be mistaken for each other.
long networkTimeMillis1 = ARBITRARY_TEST_TIME_MILLIS;
long networkTimeMillis2 = ARBITRARY_TEST_TIME_MILLIS + Duration.ofDays(30).toMillis();
long phoneTimeMillis = ARBITRARY_TEST_TIME_MILLIS + Duration.ofDays(60).toMillis();
// A small increment used to simulate the passage of time, but not enough to interfere with
// macro-level time changes associated with suggestion age.
final long smallTimeIncrementMillis = 101;
// A network suggestion is made. It should be used because there is no phone suggestion.
NetworkTimeSuggestion networkTimeSuggestion1 =
mScript.generateNetworkTimeSuggestion(networkTimeMillis1);
mScript.simulateTimePassing(smallTimeIncrementMillis)
.simulateNetworkTimeSuggestion(networkTimeSuggestion1)
.verifySystemClockWasSetAndResetCallTracking(
mScript.calculateTimeInMillisForNow(networkTimeSuggestion1.getUtcTime()),
false /* expectNetworkBroadcast */);
// Check internal state.
mScript.assertLatestPhoneSuggestion(ARBITRARY_PHONE_ID, null)
.assertLatestNetworkSuggestion(networkTimeSuggestion1);
assertEquals(networkTimeSuggestion1, mScript.peekLatestValidNetworkSuggestion());
assertNull(mScript.peekBestPhoneSuggestion());
// Simulate a little time passing.
mScript.simulateTimePassing(smallTimeIncrementMillis)
.verifySystemClockWasNotSetAndResetCallTracking();
// Now a phone suggestion is made. Phone suggestions are prioritized over network
// suggestions so it should "win".
PhoneTimeSuggestion phoneTimeSuggestion =
mScript.generatePhoneTimeSuggestion(ARBITRARY_PHONE_ID, phoneTimeMillis);
mScript.simulateTimePassing(smallTimeIncrementMillis)
.simulatePhoneTimeSuggestion(phoneTimeSuggestion)
.verifySystemClockWasSetAndResetCallTracking(
mScript.calculateTimeInMillisForNow(phoneTimeSuggestion.getUtcTime()),
true /* expectNetworkBroadcast */);
// Check internal state.
mScript.assertLatestPhoneSuggestion(ARBITRARY_PHONE_ID, phoneTimeSuggestion)
.assertLatestNetworkSuggestion(networkTimeSuggestion1);
assertEquals(networkTimeSuggestion1, mScript.peekLatestValidNetworkSuggestion());
assertEquals(phoneTimeSuggestion, mScript.peekBestPhoneSuggestion());
// Simulate some significant time passing: half the time allowed before a time signal
// becomes "too old to use".
mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_UTC_TIME_AGE_MILLIS / 2)
.verifySystemClockWasNotSetAndResetCallTracking();
// Now another network suggestion is made. Phone suggestions are prioritized over network
// suggestions so the latest phone suggestion should still "win".
NetworkTimeSuggestion networkTimeSuggestion2 =
mScript.generateNetworkTimeSuggestion(networkTimeMillis2);
mScript.simulateTimePassing(smallTimeIncrementMillis)
.simulateNetworkTimeSuggestion(networkTimeSuggestion2)
.verifySystemClockWasNotSetAndResetCallTracking();
// Check internal state.
mScript.assertLatestPhoneSuggestion(ARBITRARY_PHONE_ID, phoneTimeSuggestion)
.assertLatestNetworkSuggestion(networkTimeSuggestion2);
assertEquals(networkTimeSuggestion2, mScript.peekLatestValidNetworkSuggestion());
assertEquals(phoneTimeSuggestion, mScript.peekBestPhoneSuggestion());
// Simulate some significant time passing: half the time allowed before a time signal
// becomes "too old to use". This should mean that phoneTimeSuggestion is now too old to be
// used but networkTimeSuggestion2 is not.
mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_UTC_TIME_AGE_MILLIS / 2);
// NOTE: The TimeDetectorStrategyImpl doesn't set an alarm for the point when the last
// suggestion it used becomes too old: it requires a new suggestion or an auto-time toggle
// to re-run the detection logic. This may change in future but until then we rely on a
// steady stream of suggestions to re-evaluate.
mScript.verifySystemClockWasNotSetAndResetCallTracking();
// Check internal state.
mScript.assertLatestPhoneSuggestion(ARBITRARY_PHONE_ID, phoneTimeSuggestion)
.assertLatestNetworkSuggestion(networkTimeSuggestion2);
assertEquals(networkTimeSuggestion2, mScript.peekLatestValidNetworkSuggestion());
assertNull(mScript.peekBestPhoneSuggestion());
// Toggle auto-time off and on to force the detection logic to run.
mScript.simulateAutoTimeDetectionToggle()
.simulateTimePassing(smallTimeIncrementMillis)
.simulateAutoTimeDetectionToggle();
// Verify the latest network time now wins.
mScript.verifySystemClockWasSetAndResetCallTracking(
mScript.calculateTimeInMillisForNow(networkTimeSuggestion2.getUtcTime()),
false /* expectNetworkTimeBroadcast */);
// Check internal state.
mScript.assertLatestPhoneSuggestion(ARBITRARY_PHONE_ID, phoneTimeSuggestion)
.assertLatestNetworkSuggestion(networkTimeSuggestion2);
assertEquals(networkTimeSuggestion2, mScript.peekLatestValidNetworkSuggestion());
assertNull(mScript.peekBestPhoneSuggestion());
}
/**
* A fake implementation of TimeDetectorStrategy.Callback. Besides tracking changes and behaving
* like the real thing should, it also asserts preconditions.
@@ -674,6 +807,11 @@ public class TimeDetectorStrategyImplTest {
return this;
}
Script simulateNetworkTimeSuggestion(NetworkTimeSuggestion timeSuggestion) {
mTimeDetectorStrategy.suggestNetworkTime(timeSuggestion);
return this;
}
Script simulateAutoTimeDetectionToggle() {
mFakeCallback.simulateAutoTimeZoneDetectionToggle();
mTimeDetectorStrategy.handleAutoTimeDetectionChanged();
@@ -685,6 +823,13 @@ public class TimeDetectorStrategyImplTest {
return this;
}
/**
* Simulates time passing by an arbitrary (but relatively small) amount.
*/
Script simulateTimePassing() {
return simulateTimePassing(999);
}
Script verifySystemClockWasNotSetAndResetCallTracking() {
mFakeCallback.verifySystemClockNotSet();
mFakeCallback.verifyIntentWasNotBroadcast();
@@ -710,14 +855,30 @@ public class TimeDetectorStrategyImplTest {
return this;
}
/**
* White box test info: Asserts the latest network suggestion is as expected.
*/
Script assertLatestNetworkSuggestion(NetworkTimeSuggestion expected) {
assertEquals(expected, mTimeDetectorStrategy.getLatestNetworkSuggestion());
return this;
}
/**
* White box test info: Returns the phone suggestion that would be used, if any, given the
* current elapsed real time clock.
* current elapsed real time clock and regardless of origin prioritization.
*/
PhoneTimeSuggestion peekBestPhoneSuggestion() {
return mTimeDetectorStrategy.findBestPhoneSuggestionForTests();
}
/**
* White box test info: Returns the network suggestion that would be used, if any, given the
* current elapsed real time clock and regardless of origin prioritization.
*/
NetworkTimeSuggestion peekLatestValidNetworkSuggestion() {
return mTimeDetectorStrategy.findLatestValidNetworkSuggestionForTests();
}
/**
* Generates a ManualTimeSuggestion using the current elapsed realtime clock for the
* reference time.
@@ -739,6 +900,24 @@ public class TimeDetectorStrategyImplTest {
}
return createPhoneTimeSuggestion(phoneId, time);
}
/**
* Generates a NetworkTimeSuggestion using the current elapsed realtime clock for the
* reference time.
*/
NetworkTimeSuggestion generateNetworkTimeSuggestion(long timeMillis) {
TimestampedValue<Long> utcTime =
new TimestampedValue<>(mFakeCallback.peekElapsedRealtimeMillis(), timeMillis);
return new NetworkTimeSuggestion(utcTime);
}
/**
* Calculates what the supplied time would be when adjusted for the movement of the fake
* elapsed realtime clock.
*/
long calculateTimeInMillisForNow(TimestampedValue<Long> utcTime) {
return TimeDetectorStrategy.getTimeAt(utcTime, peekElapsedRealtimeMillis());
}
}
private static PhoneTimeSuggestion createPhoneTimeSuggestion(int phoneId,