Refactoring to test NetworkTimeUpdateService

Refactoring to make some of the NetworkTimeUpdateService logic testable.
Add tests to take advantage of the new structure.

Bug: 222295093
Test: atest services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java
Change-Id: I60dc4b4c5656e8e10c59dabde17413ae3da9724a
This commit is contained in:
Neil Fuller
2022-12-16 17:30:46 +00:00
parent 6bb629bb08
commit d7be628d05
2 changed files with 679 additions and 136 deletions

View File

@@ -16,6 +16,7 @@
package com.android.server.timedetector;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -39,12 +40,14 @@ import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
import android.util.Log;
import android.util.NtpTrustedTime;
import android.util.NtpTrustedTime.TimeResult;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.server.LocalServices;
@@ -52,12 +55,17 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Supplier;
/**
* 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.
* Refreshes network time periodically, when network connectivity becomes available and when the
* user enables automatic time detection.
*
* <p>When available, the time is always suggested to the {@link
* <p>For periodic requests, this service attempts to leave an interval between successful requests.
* If a request fails, it retries a number of times with a "short" interval and then resets to the
* normal interval. The process then repeats.
*
* <p>When a valid network time is 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.
*/
@@ -72,25 +80,11 @@ public class NetworkTimeUpdateService extends Binder {
private final Object mLock = new Object();
private final Context mContext;
private final NtpTrustedTime mNtpTrustedTime;
private final AlarmManager mAlarmManager;
private final TimeDetectorInternal mTimeDetectorInternal;
private final ConnectivityManager mCM;
private final PendingIntent mPendingPollIntent;
private final PowerManager.WakeLock mWakeLock;
// Normal polling frequency
private final int mNormalPollingIntervalMillis;
// Try-again polling interval, in case the network request failed
private final int mShortPollingIntervalMillis;
// Number of times to try again
private final int mTryAgainTimesMax;
/**
* A log that records the decisions to fetch a network time update.
* This is logged in bug reports to assist with debugging issues with network time suggestions.
*/
private final LocalLog mLocalLog = new LocalLog(30, false /* useLocalTimestamps */);
private final NtpTrustedTime mNtpTrustedTime;
private final Engine.RefreshCallbacks mRefreshCallbacks;
private final Engine mEngine;
// Blocking NTP lookup is done using this handler
private final Handler mHandler;
@@ -100,33 +94,43 @@ public class NetworkTimeUpdateService extends Binder {
@Nullable
private Network mDefaultNetwork = null;
// 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.
// This field is only updated and accessed by the mHandler thread (except dump()).
@GuardedBy("mLock")
private int mTryAgainCounter;
public NetworkTimeUpdateService(@NonNull Context context) {
mContext = Objects.requireNonNull(context);
mAlarmManager = mContext.getSystemService(AlarmManager.class);
mTimeDetectorInternal = LocalServices.getService(TimeDetectorInternal.class);
mCM = mContext.getSystemService(ConnectivityManager.class);
mWakeLock = context.getSystemService(PowerManager.class).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, TAG);
mNtpTrustedTime = NtpTrustedTime.getInstance(context);
mTryAgainTimesMax = mContext.getResources().getInteger(
Supplier<Long> elapsedRealtimeMillisSupplier = SystemClock::elapsedRealtime;
int tryAgainTimesMax = mContext.getResources().getInteger(
com.android.internal.R.integer.config_ntpRetry);
mNormalPollingIntervalMillis = mContext.getResources().getInteger(
int normalPollingIntervalMillis = mContext.getResources().getInteger(
com.android.internal.R.integer.config_ntpPollingInterval);
mShortPollingIntervalMillis = mContext.getResources().getInteger(
int shortPollingIntervalMillis = mContext.getResources().getInteger(
com.android.internal.R.integer.config_ntpPollingIntervalShorter);
mEngine = new EngineImpl(elapsedRealtimeMillisSupplier, normalPollingIntervalMillis,
shortPollingIntervalMillis, tryAgainTimesMax, mNtpTrustedTime);
AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
TimeDetectorInternal timeDetectorInternal =
LocalServices.getService(TimeDetectorInternal.class);
// Broadcast alarms sent by system are immutable
Intent pollIntent = new Intent(ACTION_POLL, null);
mPendingPollIntent = PendingIntent.getBroadcast(mContext, POLL_REQUEST,
PendingIntent pendingPollIntent = PendingIntent.getBroadcast(mContext, POLL_REQUEST,
pollIntent, PendingIntent.FLAG_IMMUTABLE);
mRefreshCallbacks = new Engine.RefreshCallbacks() {
@Override
public void scheduleNextRefresh(@ElapsedRealtimeLong long elapsedRealtimeMillis) {
alarmManager.cancel(pendingPollIntent);
alarmManager.set(
AlarmManager.ELAPSED_REALTIME, elapsedRealtimeMillis, pendingPollIntent);
}
@Override
public void submitSuggestion(NetworkTimeSuggestion suggestion) {
timeDetectorInternal.suggestNetworkTime(suggestion);
}
};
HandlerThread thread = new HandlerThread(TAG);
thread.start();
@@ -217,12 +221,7 @@ public class NetworkTimeUpdateService extends Binder {
}
if (network == null) return false;
boolean success = mNtpTrustedTime.forceRefresh(network);
if (success) {
makeNetworkTimeSuggestion(mNtpTrustedTime.getCachedTimeResult(),
"Origin: NetworkTimeUpdateService: forceRefreshForTests");
}
return success;
return mEngine.forceRefreshForTests(network, mRefreshCallbacks);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -238,96 +237,12 @@ public class NetworkTimeUpdateService extends Binder {
mWakeLock.acquire();
try {
onPollNetworkTimeUnderWakeLock(network, reason);
mEngine.refreshIfRequiredAndReschedule(network, reason, mRefreshCallbacks);
} finally {
mWakeLock.release();
}
}
private void onPollNetworkTimeUnderWakeLock(
@NonNull Network network, @NonNull String reason) {
long currentElapsedRealtimeMillis = SystemClock.elapsedRealtime();
final int maxNetworkTimeAgeMillis = mNormalPollingIntervalMillis;
// Force an NTP fix when outdated
NtpTrustedTime.TimeResult cachedNtpResult = mNtpTrustedTime.getCachedTimeResult();
if (cachedNtpResult == null
|| cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis)
>= maxNetworkTimeAgeMillis) {
if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh using network=" + network);
boolean success = mNtpTrustedTime.forceRefresh(network);
if (success) {
synchronized (mLock) {
mTryAgainCounter = 0;
}
} else {
String logMsg = "forceRefresh() returned false:"
+ " cachedNtpResult=" + cachedNtpResult
+ ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis;
if (DBG) {
Log.d(TAG, logMsg);
}
mLocalLog.log(logMsg);
}
cachedNtpResult = mNtpTrustedTime.getCachedTimeResult();
}
if (cachedNtpResult != null
&& cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis)
< maxNetworkTimeAgeMillis) {
// Obtained fresh fix; schedule next normal update
scheduleNextRefresh(mNormalPollingIntervalMillis
- cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis));
makeNetworkTimeSuggestion(cachedNtpResult, reason);
} else {
synchronized (mLock) {
// No fresh fix; schedule retry
mTryAgainCounter++;
if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) {
scheduleNextRefresh(mShortPollingIntervalMillis);
} else {
// Try much later
String logMsg = "mTryAgainTimesMax exceeded,"
+ " cachedNtpResult=" + cachedNtpResult;
if (DBG) {
Log.d(TAG, logMsg);
}
mLocalLog.log(logMsg);
mTryAgainCounter = 0;
scheduleNextRefresh(mNormalPollingIntervalMillis);
}
}
}
}
/** Suggests the time to the time detector. It may choose use it to set the system clock. */
private void makeNetworkTimeSuggestion(
@NonNull TimeResult ntpResult, @NonNull String debugInfo) {
UnixEpochTime timeSignal = new UnixEpochTime(
ntpResult.getElapsedRealtimeMillis(), ntpResult.getTimeMillis());
NetworkTimeSuggestion timeSuggestion =
new NetworkTimeSuggestion(timeSignal, ntpResult.getUncertaintyMillis());
timeSuggestion.addDebugInfo(debugInfo);
timeSuggestion.addDebugInfo(ntpResult.toString());
mTimeDetectorInternal.suggestNetworkTime(timeSuggestion);
}
/**
* Cancel old alarm and starts a new one for the specified interval.
*
* @param delayMillis when to trigger the alarm, starting from now.
*/
private void scheduleNextRefresh(long delayMillis) {
mAlarmManager.cancel(mPendingPollIntent);
long now = SystemClock.elapsedRealtime();
long next = now + delayMillis;
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, next, mPendingPollIntent);
}
// All callbacks will be invoked using mHandler because of how the callback is registered.
private class NetworkTimeUpdateCallback extends NetworkCallback {
@Override
@@ -385,21 +300,10 @@ public class NetworkTimeUpdateService extends Binder {
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
pw.println("mNormalPollingIntervalMillis="
+ Duration.ofMillis(mNormalPollingIntervalMillis));
pw.println("mShortPollingIntervalMillis="
+ Duration.ofMillis(mShortPollingIntervalMillis));
pw.println("mTryAgainTimesMax=" + mTryAgainTimesMax);
synchronized (mLock) {
pw.println("mDefaultNetwork=" + mDefaultNetwork);
pw.println("mTryAgainCounter=" + mTryAgainCounter);
}
pw.println();
pw.println("NtpTrustedTime:");
mNtpTrustedTime.dump(pw);
pw.println();
pw.println("Local logs:");
mLocalLog.dump(fd, pw, args);
mEngine.dump(pw);
pw.println();
}
@@ -409,4 +313,204 @@ public class NetworkTimeUpdateService extends Binder {
new NetworkTimeUpdateServiceShellCommand(this).exec(
this, in, out, err, args, callback, resultReceiver);
}
/**
* The interface the service uses to interact with the time refresh logic.
* Extracted for testing.
*/
@VisibleForTesting
interface Engine {
interface RefreshCallbacks {
void scheduleNextRefresh(@ElapsedRealtimeLong long elapsedRealtimeMillis);
void submitSuggestion(@NonNull NetworkTimeSuggestion suggestion);
}
/**
* Forces the engine to refresh the network time (for tests). See {@link
* NetworkTimeUpdateService#forceRefreshForTests()}. This is a blocking call. This method
* must not schedule any calls.
*/
boolean forceRefreshForTests(
@NonNull Network network, @NonNull RefreshCallbacks refreshCallbacks);
/**
* Attempts to refresh the network time if required, i.e. if there isn't a recent-enough
* network time available. It must also schedule the next call. This is a blocking call.
*
* @param network the network to use
* @param reason the reason for the refresh (for logging)
*/
void refreshIfRequiredAndReschedule(@NonNull Network network, @NonNull String reason,
@NonNull RefreshCallbacks refreshCallbacks);
void dump(@NonNull PrintWriter pw);
}
@VisibleForTesting
static class EngineImpl implements Engine {
/**
* A log that records the decisions to fetch a network time update.
* This is logged in bug reports to assist with debugging issues with network time
* suggestions.
*/
@NonNull
private final LocalLog mLocalDebugLog = new LocalLog(30, false /* useLocalTimestamps */);
private final int mNormalPollingIntervalMillis;
private final int mShortPollingIntervalMillis;
private final int mTryAgainTimesMax;
private final NtpTrustedTime mNtpTrustedTime;
/**
* Keeps track of successive time refresh failures have occurred. This is reset to zero when
* time refresh is successful or if the number exceeds (a non-negative) {@link
* #mTryAgainTimesMax}.
*/
@GuardedBy("this")
private int mTryAgainCounter;
private final Supplier<Long> mElapsedRealtimeMillisSupplier;
@VisibleForTesting
EngineImpl(@NonNull Supplier<Long> elapsedRealtimeMillisSupplier,
int normalPollingIntervalMillis, int shortPollingIntervalMillis,
int tryAgainTimesMax, @NonNull NtpTrustedTime ntpTrustedTime) {
mElapsedRealtimeMillisSupplier = Objects.requireNonNull(elapsedRealtimeMillisSupplier);
mNormalPollingIntervalMillis = normalPollingIntervalMillis;
mShortPollingIntervalMillis = shortPollingIntervalMillis;
mTryAgainTimesMax = tryAgainTimesMax;
mNtpTrustedTime = Objects.requireNonNull(ntpTrustedTime);
}
@Override
public boolean forceRefreshForTests(
@NonNull Network network, @NonNull RefreshCallbacks refreshCallbacks) {
boolean success = mNtpTrustedTime.forceRefresh(network);
logToDebugAndDumpsys("forceRefreshForTests: success=" + success);
if (success) {
makeNetworkTimeSuggestion(mNtpTrustedTime.getCachedTimeResult(),
"EngineImpl.forceRefreshForTests()", refreshCallbacks);
}
return success;
}
@Override
public void refreshIfRequiredAndReschedule(
@NonNull Network network, @NonNull String reason,
@NonNull RefreshCallbacks refreshCallbacks) {
long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get();
final int maxNetworkTimeAgeMillis = mNormalPollingIntervalMillis;
// Force an NTP fix when outdated
NtpTrustedTime.TimeResult initialTimeResult = mNtpTrustedTime.getCachedTimeResult();
if (calculateTimeResultAgeMillis(initialTimeResult, currentElapsedRealtimeMillis)
>= maxNetworkTimeAgeMillis) {
if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh using network=" + network);
boolean successful = mNtpTrustedTime.forceRefresh(network);
if (successful) {
synchronized (this) {
mTryAgainCounter = 0;
}
} else {
String logMsg = "forceRefresh() returned false:"
+ " initialTimeResult=" + initialTimeResult
+ ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis;
logToDebugAndDumpsys(logMsg);
}
}
synchronized (this) {
long nextPollDelayMillis;
NtpTrustedTime.TimeResult latestTimeResult = mNtpTrustedTime.getCachedTimeResult();
if (calculateTimeResultAgeMillis(latestTimeResult, currentElapsedRealtimeMillis)
< maxNetworkTimeAgeMillis) {
// Obtained fresh fix; schedule next normal update
nextPollDelayMillis = mNormalPollingIntervalMillis
- latestTimeResult.getAgeMillis(currentElapsedRealtimeMillis);
makeNetworkTimeSuggestion(latestTimeResult, reason, refreshCallbacks);
} else {
// No fresh fix; schedule retry
mTryAgainCounter++;
if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) {
nextPollDelayMillis = mShortPollingIntervalMillis;
} else {
// Try much later
mTryAgainCounter = 0;
nextPollDelayMillis = mNormalPollingIntervalMillis;
}
}
long nextRefreshElapsedRealtimeMillis =
currentElapsedRealtimeMillis + nextPollDelayMillis;
refreshCallbacks.scheduleNextRefresh(nextRefreshElapsedRealtimeMillis);
logToDebugAndDumpsys("refreshIfRequiredAndReschedule:"
+ " network=" + network
+ ", reason=" + reason
+ ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis
+ ", initialTimeResult=" + initialTimeResult
+ ", latestTimeResult=" + latestTimeResult
+ ", mTryAgainCounter=" + mTryAgainCounter
+ ", nextPollDelayMillis=" + nextPollDelayMillis
+ ", nextRefreshElapsedRealtimeMillis="
+ Duration.ofMillis(nextRefreshElapsedRealtimeMillis)
+ " (" + nextRefreshElapsedRealtimeMillis + ")");
}
}
private static long calculateTimeResultAgeMillis(
@Nullable TimeResult timeResult,
@ElapsedRealtimeLong long currentElapsedRealtimeMillis) {
return timeResult == null ? Long.MAX_VALUE
: timeResult.getAgeMillis(currentElapsedRealtimeMillis);
}
/** Suggests the time to the time detector. It may choose use it to set the system clock. */
private void makeNetworkTimeSuggestion(@NonNull TimeResult ntpResult,
@NonNull String debugInfo, @NonNull RefreshCallbacks refreshCallbacks) {
UnixEpochTime timeSignal = new UnixEpochTime(
ntpResult.getElapsedRealtimeMillis(), ntpResult.getTimeMillis());
NetworkTimeSuggestion timeSuggestion =
new NetworkTimeSuggestion(timeSignal, ntpResult.getUncertaintyMillis());
timeSuggestion.addDebugInfo(debugInfo);
timeSuggestion.addDebugInfo(ntpResult.toString());
refreshCallbacks.submitSuggestion(timeSuggestion);
}
@Override
public void dump(PrintWriter pw) {
IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
ipw.println("mNormalPollingIntervalMillis=" + mNormalPollingIntervalMillis);
ipw.println("mShortPollingIntervalMillis=" + mShortPollingIntervalMillis);
ipw.println("mTryAgainTimesMax=" + mTryAgainTimesMax);
synchronized (this) {
ipw.println("mTryAgainCounter=" + mTryAgainCounter);
}
ipw.println();
ipw.println("NtpTrustedTime:");
ipw.increaseIndent();
mNtpTrustedTime.dump(ipw);
ipw.decreaseIndent();
ipw.println();
ipw.println("Debug log:");
ipw.increaseIndent();
mLocalDebugLog.dump(ipw);
ipw.decreaseIndent();
ipw.println();
}
private void logToDebugAndDumpsys(String logMsg) {
if (DBG) {
Log.d(TAG, logMsg);
}
mLocalDebugLog.log(logMsg);
}
}
}

View File

@@ -0,0 +1,439 @@
/*
* Copyright (C) 2022 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 com.android.server.timedetector;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.time.UnixEpochTime;
import android.net.Network;
import android.util.NtpTrustedTime;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.timedetector.NetworkTimeUpdateService.Engine.RefreshCallbacks;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.net.InetSocketAddress;
import java.util.function.Supplier;
@RunWith(AndroidJUnit4.class)
public class NetworkTimeUpdateServiceTest {
private static final InetSocketAddress FAKE_SERVER_ADDRESS =
InetSocketAddress.createUnresolved("test", 123);
private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 100000000L;
private static final long ARBITRARY_UNIX_EPOCH_TIME_MILLIS = 5555555555L;
private static final int ARBITRARY_UNCERTAINTY_MILLIS = 999;
private FakeElapsedRealtimeClock mFakeElapsedRealtimeClock;
private NtpTrustedTime mMockNtpTrustedTime;
private Network mDummyNetwork;
@Before
public void setUp() {
mFakeElapsedRealtimeClock = new FakeElapsedRealtimeClock();
mMockNtpTrustedTime = mock(NtpTrustedTime.class);
mDummyNetwork = mock(Network.class);
}
@Test
public void engineImpl_refreshIfRequiredAndReschedule_success() {
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
int shortPollingIntervalMillis = 3333;
int tryAgainTimesMax = 5;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
mFakeElapsedRealtimeClock,
normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
mMockNtpTrustedTime);
// Simulated NTP client behavior: No cached time value available initially, then a
// successful refresh.
NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
// Expect the refresh attempt to have been made.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
// Check everything happened that was supposed to.
long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
timeResult, normalPollingIntervalMillis);
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
verify(mockCallback).submitSuggestion(expectedSuggestion);
}
@Test
public void engineImpl_refreshIfRequiredAndReschedule_failThenFailRepeatedly() {
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
int shortPollingIntervalMillis = 3333;
int tryAgainTimesMax = 5;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
mFakeElapsedRealtimeClock,
normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
mMockNtpTrustedTime);
for (int i = 0; i < tryAgainTimesMax + 1; i++) {
// Simulated NTP client behavior: No cached time value available and failure to refresh.
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
// Expect a refresh attempt each time: there's no currently cached result.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
// Check everything happened that was supposed to.
long expectedDelayMillis;
if (i < tryAgainTimesMax) {
expectedDelayMillis = shortPollingIntervalMillis;
} else {
expectedDelayMillis = normalPollingIntervalMillis;
}
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
verify(mockCallback, never()).submitSuggestion(any());
reset(mMockNtpTrustedTime);
}
}
@Test
public void engineImpl_refreshIfRequiredAndReschedule_successThenFailRepeatedly() {
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
int maxTimeResultAgeMillis = normalPollingIntervalMillis;
int shortPollingIntervalMillis = 3333;
int tryAgainTimesMax = 5;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
mFakeElapsedRealtimeClock,
normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
mMockNtpTrustedTime);
NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
{
// Simulated NTP client behavior: No cached time value available initially, with a
// successful refresh.
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
// Expect the refresh attempt to have been made: there is no cached network time
// initially.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
timeResult, normalPollingIntervalMillis);
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
reset(mMockNtpTrustedTime);
}
// Increment the current time by enough so that an attempt to refresh the time should be
// made every time refreshIfRequiredAndReschedule() is called.
mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis);
// Test multiple follow-up calls.
for (int i = 0; i < tryAgainTimesMax + 1; i++) {
// Simulated NTP client behavior: (Too old) cached time value available, unsuccessful
// refresh.
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
// Expect a refresh attempt each time as the cached network time is too old.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
// Check the scheduling.
long expectedDelayMillis;
if (i < tryAgainTimesMax) {
expectedDelayMillis = shortPollingIntervalMillis;
} else {
expectedDelayMillis = normalPollingIntervalMillis;
}
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
// No valid time, no suggestion.
verify(mockCallback, never()).submitSuggestion(any());
reset(mMockNtpTrustedTime);
}
}
@Test
public void engineImpl_refreshIfRequiredAndReschedule_successFailSuccess() {
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
int maxTimeResultAgeMillis = normalPollingIntervalMillis;
int shortPollingIntervalMillis = 3333;
int tryAgainTimesMax = 5;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
mFakeElapsedRealtimeClock,
normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
mMockNtpTrustedTime);
NtpTrustedTime.TimeResult timeResult1 = createNtpTimeResult(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
{
// Simulated NTP client behavior: No cached time value available initially, with a
// successful refresh.
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult1);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
// Expect the refresh attempt to have been made: there is no cached network time
// initially.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
timeResult1, normalPollingIntervalMillis);
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult1);
verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
reset(mMockNtpTrustedTime);
}
// Increment the current time by enough so that the cached time result is too old and an
// attempt to refresh the time should be made every time refreshIfRequiredAndReschedule() is
// called.
mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis);
{
// Simulated NTP client behavior: (Old) cached time value available initially, with an
// unsuccessful refresh.
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult1);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
// Expect the refresh attempt to have been made: the timeResult is too old.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
long expectedDelayMillis = shortPollingIntervalMillis;
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
// No valid time, no suggestion.
verify(mockCallback, never()).submitSuggestion(any());
reset(mMockNtpTrustedTime);
}
NtpTrustedTime.TimeResult timeResult2 = createNtpTimeResult(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
{
// Simulated NTP client behavior: (Old) cached time value available initially, with a
// successful refresh and a new cached time value.
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult1, timeResult2);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
// Expect the refresh attempt to have been made: the timeResult is too old.
verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
timeResult2, normalPollingIntervalMillis);
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult2);
verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
reset(mMockNtpTrustedTime);
}
}
/**
* Confirms that if a refreshIfRequiredAndReschedule() call is made, e.g. for reasons besides
* scheduled alerts, and the latest time is not too old, then an NTP refresh won't be attempted.
* A suggestion will still be made.
*/
@Test
public void engineImpl_refreshIfRequiredAndReschedule_noRefreshIfLatestIsNotTooOld() {
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
int maxTimeResultAgeMillis = normalPollingIntervalMillis;
int shortPollingIntervalMillis = 3333;
int tryAgainTimesMax = 5;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
mFakeElapsedRealtimeClock,
normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
mMockNtpTrustedTime);
// Simulated NTP client behavior: A cached time value is available, increment the clock, but
// not enough to consider the cached value too old.
NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis - 1);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
// Expect no refresh attempt to have been made.
verify(mMockNtpTrustedTime, never()).forceRefresh(any());
// The next wake-up should be rescheduled for when the cached time value will become too
// old.
long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(timeResult,
normalPollingIntervalMillis);
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
// Suggestions must be made every time if the cached time value is not too old in case it
// was refreshed by a different component.
NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
}
/**
* Confirms that if a refreshIfRequiredAndReschedule() call is made, e.g. for reasons besides
* scheduled alerts, and the latest time is not too old, then an NTP refresh won't be attempted.
* A suggestion will still be made.
*/
@Test
public void engineImpl_refreshIfRequiredAndReschedule_failureHandlingAfterLatestIsTooOld() {
mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
int normalPollingIntervalMillis = 7777777;
int maxTimeResultAgeMillis = normalPollingIntervalMillis;
int shortPollingIntervalMillis = 3333;
int tryAgainTimesMax = 5;
NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
mFakeElapsedRealtimeClock,
normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
mMockNtpTrustedTime);
// Simulated NTP client behavior: A cached time value is available, increment the clock,
// enough to consider the cached value too old. The refresh attempt will fail.
NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis);
when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
// Trigger the engine's logic.
engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
// Expect a refresh attempt to have been made.
verify(mMockNtpTrustedTime, times(1)).forceRefresh(mDummyNetwork);
// The next wake-up should be rescheduled using the short polling interval.
long expectedDelayMillis = shortPollingIntervalMillis;
verify(mockCallback).scheduleNextRefresh(
mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
// Suggestions should not be made if the cached time value is too old.
verify(mockCallback, never()).submitSuggestion(any());
}
private long calculateRefreshDelayMillisForTimeResult(NtpTrustedTime.TimeResult timeResult,
int normalPollingIntervalMillis) {
long currentElapsedRealtimeMillis = mFakeElapsedRealtimeClock.getElapsedRealtimeMillis();
long timeResultAgeMillis = timeResult.getAgeMillis(currentElapsedRealtimeMillis);
return normalPollingIntervalMillis - timeResultAgeMillis;
}
private static NetworkTimeSuggestion createExpectedSuggestion(
NtpTrustedTime.TimeResult timeResult) {
UnixEpochTime unixEpochTime = new UnixEpochTime(
timeResult.getElapsedRealtimeMillis(), timeResult.getTimeMillis());
return new NetworkTimeSuggestion(unixEpochTime, timeResult.getUncertaintyMillis());
}
private static NtpTrustedTime.TimeResult createNtpTimeResult(long elapsedRealtimeMillis) {
return new NtpTrustedTime.TimeResult(
ARBITRARY_UNIX_EPOCH_TIME_MILLIS,
elapsedRealtimeMillis,
ARBITRARY_UNCERTAINTY_MILLIS,
FAKE_SERVER_ADDRESS);
}
private static class FakeElapsedRealtimeClock implements Supplier<Long> {
private long mElapsedRealtimeMillis;
public void setElapsedRealtimeMillis(long elapsedRealtimeMillis) {
mElapsedRealtimeMillis = elapsedRealtimeMillis;
}
public long getElapsedRealtimeMillis() {
return mElapsedRealtimeMillis;
}
public long incrementMillis(int millis) {
mElapsedRealtimeMillis += millis;
return mElapsedRealtimeMillis;
}
@Override
public Long get() {
return getElapsedRealtimeMillis();
}
}
}