diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java
index b05f7cf241e37..0fb04b9d7299f 100644
--- a/core/java/android/net/SntpClient.java
+++ b/core/java/android/net/SntpClient.java
@@ -16,6 +16,7 @@
package android.net;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.sntp.Duration64;
import android.net.sntp.Timestamp64;
@@ -29,6 +30,7 @@ import com.android.internal.util.TrafficStatsConstants;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@@ -39,9 +41,7 @@ import java.util.Random;
import java.util.function.Supplier;
/**
- * {@hide}
- *
- * Simple SNTP client class for retrieving network time.
+ * Simple, single-use SNTP client class for retrieving network time.
*
* Sample usage:
*
SntpClient client = new SntpClient();
@@ -49,6 +49,10 @@ import java.util.function.Supplier;
* long now = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference();
* }
*
+ *
+ * This class is not thread-safe.
+ *
+ * @hide
*/
public class SntpClient {
private static final String TAG = "SntpClient";
@@ -87,6 +91,9 @@ public class SntpClient {
// The round trip (network) time in milliseconds
private long mRoundTripTime;
+ // Details of the NTP server used to obtain the time last.
+ @Nullable private InetSocketAddress mServerSocketAddress;
+
private static class InvalidServerReplyException extends Exception {
public InvalidServerReplyException(String message) {
super(message);
@@ -202,6 +209,7 @@ public class SntpClient {
mNtpTime = responseTime.plus(clockOffsetDuration).toEpochMilli();
mNtpTimeReference = responseTicks;
mRoundTripTime = roundTripTimeMillis;
+ mServerSocketAddress = new InetSocketAddress(address, port);
} catch (Exception e) {
EventLogTags.writeNtpFailure(address.toString(), e.toString());
if (DBG) Log.d(TAG, "request time failed: " + e);
@@ -284,6 +292,14 @@ public class SntpClient {
return mRoundTripTime;
}
+ /**
+ * Returns the address of the NTP server used in the NTP transaction
+ */
+ @Nullable
+ public InetSocketAddress getServerSocketAddress() {
+ return mServerSocketAddress;
+ }
+
private static void checkValidServerReply(
byte leap, byte mode, int stratum, Timestamp64 transmitTimestamp,
Timestamp64 referenceTimestamp, Timestamp64 randomizedRequestTimestamp,
diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java
index c544839f58896..12e33f56b3c63 100644
--- a/core/java/android/util/NtpTrustedTime.java
+++ b/core/java/android/util/NtpTrustedTime.java
@@ -35,6 +35,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
+import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
@@ -50,7 +51,7 @@ import java.util.function.Supplier;
*
* @hide
*/
-public class NtpTrustedTime implements TrustedTime {
+public abstract class NtpTrustedTime implements TrustedTime {
private static final String URI_SCHEME_NTP = "ntp";
@@ -107,16 +108,19 @@ public class NtpTrustedTime implements TrustedTime {
*
* @hide
*/
- public static class TimeResult {
+ public static final class TimeResult {
private final long mUnixEpochTimeMillis;
private final long mElapsedRealtimeMillis;
private final int mUncertaintyMillis;
+ @NonNull private final InetSocketAddress mNtpServerSocketAddress;
public TimeResult(
- long unixEpochTimeMillis, long elapsedRealtimeMillis, int uncertaintyMillis) {
+ long unixEpochTimeMillis, long elapsedRealtimeMillis, int uncertaintyMillis,
+ @NonNull InetSocketAddress ntpServerSocketAddress) {
mUnixEpochTimeMillis = unixEpochTimeMillis;
mElapsedRealtimeMillis = elapsedRealtimeMillis;
mUncertaintyMillis = uncertaintyMillis;
+ mNtpServerSocketAddress = Objects.requireNonNull(ntpServerSocketAddress);
}
public long getTimeMillis() {
@@ -152,12 +156,35 @@ public class NtpTrustedTime implements TrustedTime {
return currentElapsedRealtimeMillis - mElapsedRealtimeMillis;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof TimeResult)) {
+ return false;
+ }
+ TimeResult that = (TimeResult) o;
+ return mUnixEpochTimeMillis == that.mUnixEpochTimeMillis
+ && mElapsedRealtimeMillis == that.mElapsedRealtimeMillis
+ && mUncertaintyMillis == that.mUncertaintyMillis
+ && mNtpServerSocketAddress.equals(
+ that.mNtpServerSocketAddress);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mUnixEpochTimeMillis, mElapsedRealtimeMillis, mUncertaintyMillis,
+ mNtpServerSocketAddress);
+ }
+
@Override
public String toString() {
return "TimeResult{"
+ "unixEpochTime=" + Instant.ofEpochMilli(mUnixEpochTimeMillis)
+ ", elapsedRealtime=" + Duration.ofMillis(mElapsedRealtimeMillis)
+ ", mUncertaintyMillis=" + mUncertaintyMillis
+ + ", mNtpServerSocketAddress=" + mNtpServerSocketAddress
+ '}';
}
}
@@ -167,46 +194,23 @@ public class NtpTrustedTime implements TrustedTime {
private static NtpTrustedTime sSingleton;
- @NonNull
- private final Context mContext;
-
- /**
- * A supplier that returns the ConnectivityManager. The Supplier can return null if
- * ConnectivityService isn't running yet.
- */
- private final Supplier mConnectivityManagerSupplier =
- new Supplier() {
- private ConnectivityManager mConnectivityManager;
-
- @Nullable
- @Override
- public synchronized ConnectivityManager get() {
- // We can't do this at initialization time: ConnectivityService might not be running
- // yet.
- if (mConnectivityManager == null) {
- mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
- }
- return mConnectivityManager;
- }
- };
-
/** An in-memory config override for use during tests. */
+ @GuardedBy("this")
@Nullable
private NtpConfig mNtpConfigForTests;
- // Declared volatile and accessed outside of synchronized blocks to avoid blocking reads during
+ // Declared volatile and accessed outside synchronized blocks to avoid blocking reads during
// forceRefresh().
private volatile TimeResult mTimeResult;
- private NtpTrustedTime(Context context) {
- mContext = Objects.requireNonNull(context);
+ protected NtpTrustedTime() {
}
@UnsupportedAppUsage
public static synchronized NtpTrustedTime getInstance(Context context) {
if (sSingleton == null) {
Context appContext = context.getApplicationContext();
- sSingleton = new NtpTrustedTime(appContext);
+ sSingleton = new NtpTrustedTimeImpl(appContext);
}
return sSingleton;
}
@@ -224,65 +228,71 @@ public class NtpTrustedTime implements TrustedTime {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public boolean forceRefresh() {
synchronized (this) {
- NtpConfig connectionInfo = getNtpConfig();
- if (connectionInfo == null) {
+ NtpConfig ntpConfig = getNtpConfig();
+ if (ntpConfig == null) {
// missing server config, so no NTP time available
if (LOGD) Log.d(TAG, "forceRefresh: invalid server config");
return false;
}
- ConnectivityManager connectivityManager = mConnectivityManagerSupplier.get();
- if (connectivityManager == null) {
- if (LOGD) Log.d(TAG, "forceRefresh: no ConnectivityManager");
- return false;
- }
- final Network network = connectivityManager.getActiveNetwork();
- final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
-
- // This connectivity check is to avoid performing a DNS lookup for the time server on a
- // unconnected network. There are races to obtain time in Android when connectivity
- // changes, which means that forceRefresh() can be called by various components before
- // the network is actually available. This led in the past to DNS lookup failures being
- // cached (~2 seconds) thereby preventing the device successfully making an NTP request
- // when connectivity had actually been established.
- // A side effect of check is that tests that run a fake NTP server on the device itself
- // will only be able to use it if the active network is connected, even though loopback
- // addresses are actually reachable.
- if (ni == null || !ni.isConnected()) {
- if (LOGD) Log.d(TAG, "forceRefresh: no connectivity");
+ Network network = getNetwork();
+ if (network == null) {
+ if (LOGD) Log.d(TAG, "forceRefresh: no network available");
return false;
}
- if (LOGD) Log.d(TAG, "forceRefresh() from cache miss");
- final SntpClient client = new SntpClient();
- final URI ntpServerUri = connectionInfo.getServerUri();
- final String serverName = ntpServerUri.getHost();
- final int port = ntpServerUri.getPort() == -1
- ? SntpClient.STANDARD_NTP_PORT : ntpServerUri.getPort();
- final int timeoutMillis = saturatedCast(connectionInfo.getTimeout().toMillis());
- if (client.requestTime(serverName, port, timeoutMillis, network)) {
- int ntpUncertaintyMillis = saturatedCast(client.getRoundTripTime() / 2);
- mTimeResult = new TimeResult(
- client.getNtpTime(), client.getNtpTimeReference(), ntpUncertaintyMillis);
- return true;
- } else {
- return false;
+ if (LOGD) {
+ Log.d(TAG, "forceRefresh: NTP request network=" + network
+ + " ntpConfig=" + ntpConfig);
}
+ TimeResult timeResult =
+ queryNtpServer(network, ntpConfig.getServerUri(), ntpConfig.getTimeout());
+ if (timeResult != null) {
+ // Keep any previous time result.
+ mTimeResult = timeResult;
+ }
+ return timeResult != null;
}
}
+ @GuardedBy("this")
+ private NtpConfig getNtpConfig() {
+ if (mNtpConfigForTests != null) {
+ return mNtpConfigForTests;
+ }
+ return getNtpConfigInternal();
+ }
+
/**
- * Casts a {@code long} to an {@code int}, clamping the value within the int range.
+ * Returns the {@link NtpConfig} to use during an NTP query. This method can return {@code null}
+ * if there is no config, or the config found is invalid.
+ *
+ * This method has been made public for easy replacement during tests.
*/
- private static int saturatedCast(long longValue) {
- if (longValue > Integer.MAX_VALUE) {
- return Integer.MAX_VALUE;
- }
- if (longValue < Integer.MIN_VALUE) {
- return Integer.MIN_VALUE;
- }
- return (int) longValue;
- }
+ @VisibleForTesting
+ @Nullable
+ public abstract NtpConfig getNtpConfigInternal();
+
+ /**
+ * Returns the {@link Network} to use during an NTP query. This method can return {@code null}
+ * if there is no connectivity
+ *
+ *
This method has been made public for easy replacement during tests.
+ */
+ @VisibleForTesting
+ @Nullable
+ public abstract Network getNetwork();
+
+ /**
+ * Queries the specified NTP server. This is a blocking call. Returns {@code null} if the query
+ * fails.
+ *
+ *
This method has been made public for easy replacement during tests.
+ */
+ @VisibleForTesting
+ @Nullable
+ public abstract TimeResult queryNtpServer(
+ @NonNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout);
/**
* Only kept for UnsupportedAppUsage.
@@ -371,39 +381,6 @@ public class NtpTrustedTime implements TrustedTime {
}
}
- @GuardedBy("this")
- private NtpConfig getNtpConfig() {
- if (mNtpConfigForTests != null) {
- return mNtpConfigForTests;
- }
-
- final ContentResolver resolver = mContext.getContentResolver();
- final Resources res = mContext.getResources();
-
- // The Settings value has priority over static config. Check settings first.
- final String serverGlobalSetting =
- Settings.Global.getString(resolver, Settings.Global.NTP_SERVER);
- final URI settingsServerInfo = parseNtpServerSetting(serverGlobalSetting);
-
- URI ntpServerUri;
- if (settingsServerInfo != null) {
- ntpServerUri = settingsServerInfo;
- } else {
- String configValue = res.getString(com.android.internal.R.string.config_ntpServer);
- try {
- ntpServerUri = parseNtpUriStrict(configValue);
- } catch (URISyntaxException e) {
- ntpServerUri = null;
- }
- }
-
- final int defaultTimeoutMillis =
- res.getInteger(com.android.internal.R.integer.config_ntpTimeout);
- final Duration timeout = Duration.ofMillis(Settings.Global.getInt(
- resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis));
- return ntpServerUri == null ? null : new NtpConfig(ntpServerUri, timeout);
- }
-
/**
* Parses and returns an NTP server config URI, or throws an exception if the URI doesn't
* conform to expectations.
@@ -487,4 +464,129 @@ public class NtpTrustedTime implements TrustedTime {
}
}
}
+
+ /**
+ * The real implementation of {@link NtpTrustedTime}. Contains the parts that are more difficult
+ * to test.
+ */
+ private static final class NtpTrustedTimeImpl extends NtpTrustedTime {
+
+ /**
+ * A supplier that returns the ConnectivityManager. The Supplier can return null if
+ * ConnectivityService isn't running yet.
+ */
+ private final Supplier mConnectivityManagerSupplier =
+ new Supplier<>() {
+ private ConnectivityManager mConnectivityManager;
+
+ @Nullable
+ @Override
+ public synchronized ConnectivityManager get() {
+ // We can't do this at initialization time: ConnectivityService might not be running
+ // yet.
+ if (mConnectivityManager == null) {
+ mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+ }
+ return mConnectivityManager;
+ }
+ };
+
+ @NonNull
+ private final Context mContext;
+
+ private NtpTrustedTimeImpl(@NonNull Context context) {
+ mContext = Objects.requireNonNull(context);
+ }
+
+ @Override
+ @VisibleForTesting
+ @Nullable
+ public NtpConfig getNtpConfigInternal() {
+ final ContentResolver resolver = mContext.getContentResolver();
+ final Resources res = mContext.getResources();
+
+ // The Settings value has priority over static config. Check settings first.
+ final String serverGlobalSetting =
+ Settings.Global.getString(resolver, Settings.Global.NTP_SERVER);
+ final URI settingsServerInfo = parseNtpServerSetting(serverGlobalSetting);
+
+ URI ntpServerUri;
+ if (settingsServerInfo != null) {
+ ntpServerUri = settingsServerInfo;
+ } else {
+ String configValue = res.getString(com.android.internal.R.string.config_ntpServer);
+ try {
+ ntpServerUri = parseNtpUriStrict(configValue);
+ } catch (URISyntaxException e) {
+ ntpServerUri = null;
+ }
+ }
+
+ final int defaultTimeoutMillis =
+ res.getInteger(com.android.internal.R.integer.config_ntpTimeout);
+ final Duration timeout = Duration.ofMillis(Settings.Global.getInt(
+ resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis));
+ return ntpServerUri == null ? null : new NtpConfig(ntpServerUri, timeout);
+ }
+
+ @Override
+ public Network getNetwork() {
+ ConnectivityManager connectivityManager = mConnectivityManagerSupplier.get();
+ if (connectivityManager == null) {
+ if (LOGD) Log.d(TAG, "getNetwork: no ConnectivityManager");
+ return null;
+ }
+ final Network network = connectivityManager.getActiveNetwork();
+ final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
+
+ // This connectivity check is to avoid performing a DNS lookup for the time server on a
+ // unconnected network. There are races to obtain time in Android when connectivity
+ // changes, which means that forceRefresh() can be called by various components before
+ // the network is actually available. This led in the past to DNS lookup failures being
+ // cached (~2 seconds) thereby preventing the device successfully making an NTP request
+ // when connectivity had actually been established.
+ // A side effect of check is that tests that run a fake NTP server on the device itself
+ // will only be able to use it if the active network is connected, even though loopback
+ // addresses are actually reachable.
+ if (ni == null || !ni.isConnected()) {
+ if (LOGD) Log.d(TAG, "getNetwork: no connectivity");
+ return null;
+ }
+ return network;
+ }
+
+ @Override
+ @Nullable
+ public TimeResult queryNtpServer(
+ @NonNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout) {
+
+ final SntpClient client = new SntpClient();
+ final String serverName = ntpServerUri.getHost();
+ final int port = ntpServerUri.getPort() == -1
+ ? SntpClient.STANDARD_NTP_PORT : ntpServerUri.getPort();
+ final int timeoutMillis = saturatedCast(timeout.toMillis());
+ if (client.requestTime(serverName, port, timeoutMillis, network)) {
+ int ntpUncertaintyMillis = saturatedCast(client.getRoundTripTime() / 2);
+ InetSocketAddress ntpServerSocketAddress = client.getServerSocketAddress();
+ return new TimeResult(
+ client.getNtpTime(), client.getNtpTimeReference(), ntpUncertaintyMillis,
+ ntpServerSocketAddress);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Casts a {@code long} to an {@code int}, clamping the value within the int range.
+ */
+ private static int saturatedCast(long longValue) {
+ if (longValue > Integer.MAX_VALUE) {
+ return Integer.MAX_VALUE;
+ }
+ if (longValue < Integer.MIN_VALUE) {
+ return Integer.MIN_VALUE;
+ }
+ return (int) longValue;
+ }
+ }
}
diff --git a/core/tests/coretests/src/android/util/NtpTrustedTimeTest.java b/core/tests/coretests/src/android/util/NtpTrustedTimeTest.java
index 4d088b41310df..67a4f44a9863f 100644
--- a/core/tests/coretests/src/android/util/NtpTrustedTimeTest.java
+++ b/core/tests/coretests/src/android/util/NtpTrustedTimeTest.java
@@ -16,9 +16,20 @@
package android.util;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.Network;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -26,6 +37,7 @@ import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
@@ -138,4 +150,158 @@ public class NtpTrustedTimeTest {
public void testNtpConfig_negativeTimeout() {
new NtpTrustedTime.NtpConfig(VALID_SERVER_URI, Duration.ofMillis(-1));
}
+
+ @Test
+ public void testForceRefresh_nullConfig() {
+ NtpTrustedTime ntpTrustedTime = spy(NtpTrustedTime.class);
+ when(ntpTrustedTime.getNtpConfigInternal()).thenReturn(null);
+
+ assertFalse(ntpTrustedTime.forceRefresh());
+
+ assertFalse(ntpTrustedTime.hasCache());
+ assertEquals(0, ntpTrustedTime.getCachedNtpTime());
+ assertEquals(0, ntpTrustedTime.getCachedNtpTimeReference());
+ assertEquals(Long.MAX_VALUE, ntpTrustedTime.getCacheAge());
+ assertNull(ntpTrustedTime.getCachedTimeResult());
+
+ verify(ntpTrustedTime, times(1)).getNtpConfigInternal();
+ verify(ntpTrustedTime, never()).getNetwork();
+ verify(ntpTrustedTime, never()).queryNtpServer(any(), any(), any());
+ }
+
+ @Test
+ public void testForceRefresh_noConnectivity() {
+ NtpTrustedTime ntpTrustedTime = spy(NtpTrustedTime.class);
+ URI serverUri = URI.create("ntp://ntpserver.name");
+ Duration timeout = Duration.ofSeconds(5);
+ when(ntpTrustedTime.getNtpConfigInternal()).thenReturn(
+ new NtpTrustedTime.NtpConfig(serverUri, timeout));
+
+ when(ntpTrustedTime.getNetwork()).thenReturn(null);
+
+ assertFalse(ntpTrustedTime.forceRefresh());
+
+ assertFalse(ntpTrustedTime.hasCache());
+ assertEquals(0, ntpTrustedTime.getCachedNtpTime());
+ assertEquals(0, ntpTrustedTime.getCachedNtpTimeReference());
+ assertEquals(Long.MAX_VALUE, ntpTrustedTime.getCacheAge());
+ assertNull(ntpTrustedTime.getCachedTimeResult());
+
+ verify(ntpTrustedTime, times(1)).getNtpConfigInternal();
+ verify(ntpTrustedTime, times(1)).getNetwork();
+ verify(ntpTrustedTime, never()).queryNtpServer(any(), any(), any());
+ }
+
+ @Test
+ public void testForceRefresh_queryFailed() {
+ NtpTrustedTime ntpTrustedTime = spy(NtpTrustedTime.class);
+ URI serverUri = URI.create("ntp://ntpserver.name");
+ Duration timeout = Duration.ofSeconds(5);
+ when(ntpTrustedTime.getNtpConfigInternal()).thenReturn(
+ new NtpTrustedTime.NtpConfig(serverUri, timeout));
+
+ Network network = mock(Network.class);
+ when(ntpTrustedTime.getNetwork()).thenReturn(network);
+
+ when(ntpTrustedTime.queryNtpServer(network, serverUri, timeout)).thenReturn(null);
+
+ assertFalse(ntpTrustedTime.forceRefresh());
+
+ assertFalse(ntpTrustedTime.hasCache());
+ assertEquals(0, ntpTrustedTime.getCachedNtpTime());
+ assertEquals(0, ntpTrustedTime.getCachedNtpTimeReference());
+ assertEquals(Long.MAX_VALUE, ntpTrustedTime.getCacheAge());
+ assertNull(ntpTrustedTime.getCachedTimeResult());
+
+ verify(ntpTrustedTime, times(1)).getNtpConfigInternal();
+ verify(ntpTrustedTime, times(1)).getNetwork();
+ verify(ntpTrustedTime, times(1)).queryNtpServer(network, serverUri, timeout);
+ }
+
+ @Test
+ public void testForceRefresh_querySucceeded() {
+ NtpTrustedTime ntpTrustedTime = spy(NtpTrustedTime.class);
+ URI serverUri = URI.create("ntp://ntpserver.name");
+ Duration timeout = Duration.ofSeconds(5);
+ when(ntpTrustedTime.getNtpConfigInternal()).thenReturn(
+ new NtpTrustedTime.NtpConfig(serverUri, timeout));
+
+ Network network = mock(Network.class);
+ when(ntpTrustedTime.getNetwork()).thenReturn(network);
+
+ NtpTrustedTime.TimeResult successResult = new NtpTrustedTime.TimeResult(123L, 456L, 789,
+ InetSocketAddress.createUnresolved("placeholder", 123));
+ when(ntpTrustedTime.queryNtpServer(network, serverUri, timeout)).thenReturn(successResult);
+
+ assertTrue(ntpTrustedTime.forceRefresh());
+
+ assertTrue(ntpTrustedTime.hasCache());
+ assertEquals(successResult.getTimeMillis(), ntpTrustedTime.getCachedNtpTime());
+ assertEquals(successResult.getElapsedRealtimeMillis(),
+ ntpTrustedTime.getCachedNtpTimeReference());
+ assertTrue(ntpTrustedTime.getCacheAge() != Long.MAX_VALUE);
+ assertEquals(successResult, ntpTrustedTime.getCachedTimeResult());
+
+ verify(ntpTrustedTime, times(1)).getNtpConfigInternal();
+ verify(ntpTrustedTime, times(1)).getNetwork();
+ verify(ntpTrustedTime, times(1)).queryNtpServer(network, serverUri, timeout);
+ }
+
+ @Test
+ public void testForceRefresh_keepsOldValueOnFailure() {
+ NtpTrustedTime ntpTrustedTime = spy(NtpTrustedTime.class);
+ URI serverUri = URI.create("ntp://ntpserver.name");
+ Duration timeout = Duration.ofSeconds(5);
+ when(ntpTrustedTime.getNtpConfigInternal()).thenReturn(
+ new NtpTrustedTime.NtpConfig(serverUri, timeout));
+
+ Network network = mock(Network.class);
+ when(ntpTrustedTime.getNetwork()).thenReturn(network);
+
+ NtpTrustedTime.TimeResult successResult = new NtpTrustedTime.TimeResult(123L, 456L, 789,
+ InetSocketAddress.createUnresolved("placeholder", 123));
+ when(ntpTrustedTime.queryNtpServer(network, serverUri, timeout)).thenReturn(successResult);
+
+ assertTrue(ntpTrustedTime.forceRefresh());
+
+ assertTrue(ntpTrustedTime.hasCache());
+ assertEquals(successResult, ntpTrustedTime.getCachedTimeResult());
+
+ when(ntpTrustedTime.queryNtpServer(network, serverUri, timeout)).thenReturn(null);
+
+ assertFalse(ntpTrustedTime.forceRefresh());
+
+ assertTrue(ntpTrustedTime.hasCache());
+ assertEquals(successResult, ntpTrustedTime.getCachedTimeResult());
+ }
+
+ @Test
+ public void testForceRefresh_keepsNewValueOnSuccess() {
+ NtpTrustedTime ntpTrustedTime = spy(NtpTrustedTime.class);
+ URI serverUri = URI.create("ntp://ntpserver.name");
+ Duration timeout = Duration.ofSeconds(5);
+ when(ntpTrustedTime.getNtpConfigInternal()).thenReturn(
+ new NtpTrustedTime.NtpConfig(serverUri, timeout));
+
+ Network network = mock(Network.class);
+ when(ntpTrustedTime.getNetwork()).thenReturn(network);
+
+ NtpTrustedTime.TimeResult successResult1 = new NtpTrustedTime.TimeResult(123L, 456L, 789,
+ InetSocketAddress.createUnresolved("placeholder", 123));
+ when(ntpTrustedTime.queryNtpServer(network, serverUri, timeout)).thenReturn(successResult1);
+
+ assertTrue(ntpTrustedTime.forceRefresh());
+
+ assertTrue(ntpTrustedTime.hasCache());
+ assertEquals(successResult1, ntpTrustedTime.getCachedTimeResult());
+
+ NtpTrustedTime.TimeResult successResult2 = new NtpTrustedTime.TimeResult(123L, 456L, 789,
+ InetSocketAddress.createUnresolved("placeholder", 123));
+ when(ntpTrustedTime.queryNtpServer(network, serverUri, timeout)).thenReturn(successResult2);
+
+ assertTrue(ntpTrustedTime.forceRefresh());
+
+ assertTrue(ntpTrustedTime.hasCache());
+ assertEquals(successResult2, ntpTrustedTime.getCachedTimeResult());
+ }
}
diff --git a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
index 48888f2e00dde..1f8b0de8d903a 100644
--- a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
+++ b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
@@ -271,6 +271,7 @@ public class NetworkTimeUpdateService extends Binder {
NetworkTimeSuggestion timeSuggestion =
new NetworkTimeSuggestion(timeSignal, ntpResult.getUncertaintyMillis());
timeSuggestion.addDebugInfo(debugInfo);
+ timeSuggestion.addDebugInfo(ntpResult.toString());
mTimeDetectorInternal.suggestNetworkTime(timeSuggestion);
}
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
index 1ee23fd962e68..b9c74baaf3d3a 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
@@ -61,6 +61,7 @@ import org.junit.runner.RunWith;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.net.InetSocketAddress;
import java.time.Instant;
@RunWith(AndroidJUnit4.class)
@@ -405,8 +406,8 @@ public class TimeDetectorServiceTest {
@Test
public void testLatestNetworkTime() {
- NtpTrustedTime.TimeResult latestNetworkTime =
- new NtpTrustedTime.TimeResult(1234L, 54321L, 999);
+ NtpTrustedTime.TimeResult latestNetworkTime = new NtpTrustedTime.TimeResult(
+ 1234L, 54321L, 999, InetSocketAddress.createUnresolved("test.timeserver", 123));
when(mMockNtpTrustedTime.getCachedTimeResult())
.thenReturn(latestNetworkTime);
TimePoint expected = new TimePoint(latestNetworkTime.getTimeMillis(),