1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.util; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.net.ConnectivityManager; 26 import android.net.Network; 27 import android.net.NetworkInfo; 28 import android.net.SntpClient; 29 import android.os.Build; 30 import android.os.SystemClock; 31 import android.provider.Settings; 32 import android.text.TextUtils; 33 34 import com.android.internal.annotations.GuardedBy; 35 36 import java.io.PrintWriter; 37 import java.time.Duration; 38 import java.time.Instant; 39 import java.util.Objects; 40 import java.util.function.Supplier; 41 42 /** 43 * A singleton that connects with a remote NTP server as its trusted time source. This class 44 * is thread-safe. The {@link #forceRefresh()} method is synchronous, i.e. it may occupy the 45 * current thread while performing an NTP request. All other threads calling {@link #forceRefresh()} 46 * will block during that request. 47 * 48 * @hide 49 */ 50 public class NtpTrustedTime implements TrustedTime { 51 52 /** 53 * The result of a successful NTP query. 54 * 55 * @hide 56 */ 57 public static class TimeResult { 58 private final long mTimeMillis; 59 private final long mElapsedRealtimeMillis; 60 private final long mCertaintyMillis; 61 TimeResult(long timeMillis, long elapsedRealtimeMillis, long certaintyMillis)62 public TimeResult(long timeMillis, long elapsedRealtimeMillis, long certaintyMillis) { 63 mTimeMillis = timeMillis; 64 mElapsedRealtimeMillis = elapsedRealtimeMillis; 65 mCertaintyMillis = certaintyMillis; 66 } 67 getTimeMillis()68 public long getTimeMillis() { 69 return mTimeMillis; 70 } 71 getElapsedRealtimeMillis()72 public long getElapsedRealtimeMillis() { 73 return mElapsedRealtimeMillis; 74 } 75 getCertaintyMillis()76 public long getCertaintyMillis() { 77 return mCertaintyMillis; 78 } 79 80 /** Calculates and returns the current time accounting for the age of this result. */ currentTimeMillis()81 public long currentTimeMillis() { 82 return mTimeMillis + getAgeMillis(); 83 } 84 85 /** Calculates and returns the age of this result. */ getAgeMillis()86 public long getAgeMillis() { 87 return getAgeMillis(SystemClock.elapsedRealtime()); 88 } 89 90 /** 91 * Calculates and returns the age of this result relative to currentElapsedRealtimeMillis. 92 * 93 * @param currentElapsedRealtimeMillis - reference elapsed real time 94 */ getAgeMillis(long currentElapsedRealtimeMillis)95 public long getAgeMillis(long currentElapsedRealtimeMillis) { 96 return currentElapsedRealtimeMillis - mElapsedRealtimeMillis; 97 } 98 99 @Override toString()100 public String toString() { 101 return "TimeResult{" 102 + "mTimeMillis=" + Instant.ofEpochMilli(mTimeMillis) 103 + ", mElapsedRealtimeMillis=" + Duration.ofMillis(mElapsedRealtimeMillis) 104 + ", mCertaintyMillis=" + mCertaintyMillis 105 + '}'; 106 } 107 } 108 109 private static final String TAG = "NtpTrustedTime"; 110 private static final boolean LOGD = false; 111 112 private static NtpTrustedTime sSingleton; 113 114 @NonNull 115 private final Context mContext; 116 117 /** 118 * A supplier that returns the ConnectivityManager. The Supplier can return null if 119 * ConnectivityService isn't running yet. 120 */ 121 private final Supplier<ConnectivityManager> mConnectivityManagerSupplier = 122 new Supplier<ConnectivityManager>() { 123 private ConnectivityManager mConnectivityManager; 124 125 @Nullable 126 @Override 127 public synchronized ConnectivityManager get() { 128 // We can't do this at initialization time: ConnectivityService might not be running 129 // yet. 130 if (mConnectivityManager == null) { 131 mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); 132 } 133 return mConnectivityManager; 134 } 135 }; 136 137 /** An in-memory config override for use during tests. */ 138 @Nullable 139 private String mHostnameForTests; 140 141 /** An in-memory config override for use during tests. */ 142 @Nullable 143 private Integer mPortForTests; 144 145 /** An in-memory config override for use during tests. */ 146 @Nullable 147 private Duration mTimeoutForTests; 148 149 // Declared volatile and accessed outside of synchronized blocks to avoid blocking reads during 150 // forceRefresh(). 151 private volatile TimeResult mTimeResult; 152 NtpTrustedTime(Context context)153 private NtpTrustedTime(Context context) { 154 mContext = Objects.requireNonNull(context); 155 } 156 157 @UnsupportedAppUsage getInstance(Context context)158 public static synchronized NtpTrustedTime getInstance(Context context) { 159 if (sSingleton == null) { 160 Context appContext = context.getApplicationContext(); 161 sSingleton = new NtpTrustedTime(appContext); 162 } 163 return sSingleton; 164 } 165 166 /** 167 * Overrides the NTP server config for tests. Passing {@code null} to a parameter clears the 168 * test value, i.e. so the normal value will be used next time. 169 */ setServerConfigForTests( @ullable String hostname, @Nullable Integer port, @Nullable Duration timeout)170 public void setServerConfigForTests( 171 @Nullable String hostname, @Nullable Integer port, @Nullable Duration timeout) { 172 synchronized (this) { 173 mHostnameForTests = hostname; 174 mPortForTests = port; 175 mTimeoutForTests = timeout; 176 } 177 } 178 179 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) forceRefresh()180 public boolean forceRefresh() { 181 synchronized (this) { 182 NtpConnectionInfo connectionInfo = getNtpConnectionInfo(); 183 if (connectionInfo == null) { 184 // missing server config, so no NTP time available 185 if (LOGD) Log.d(TAG, "forceRefresh: invalid server config"); 186 return false; 187 } 188 189 ConnectivityManager connectivityManager = mConnectivityManagerSupplier.get(); 190 if (connectivityManager == null) { 191 if (LOGD) Log.d(TAG, "forceRefresh: no ConnectivityManager"); 192 return false; 193 } 194 final Network network = connectivityManager.getActiveNetwork(); 195 final NetworkInfo ni = connectivityManager.getNetworkInfo(network); 196 197 // This connectivity check is to avoid performing a DNS lookup for the time server on a 198 // unconnected network. There are races to obtain time in Android when connectivity 199 // changes, which means that forceRefresh() can be called by various components before 200 // the network is actually available. This led in the past to DNS lookup failures being 201 // cached (~2 seconds) thereby preventing the device successfully making an NTP request 202 // when connectivity had actually been established. 203 // A side effect of check is that tests that run a fake NTP server on the device itself 204 // will only be able to use it if the active network is connected, even though loopback 205 // addresses are actually reachable. 206 if (ni == null || !ni.isConnected()) { 207 if (LOGD) Log.d(TAG, "forceRefresh: no connectivity"); 208 return false; 209 } 210 211 if (LOGD) Log.d(TAG, "forceRefresh() from cache miss"); 212 final SntpClient client = new SntpClient(); 213 final String serverName = connectionInfo.getServer(); 214 final int port = connectionInfo.getPort(); 215 final int timeoutMillis = connectionInfo.getTimeoutMillis(); 216 if (client.requestTime(serverName, port, timeoutMillis, network)) { 217 long ntpCertainty = client.getRoundTripTime() / 2; 218 mTimeResult = new TimeResult( 219 client.getNtpTime(), client.getNtpTimeReference(), ntpCertainty); 220 return true; 221 } else { 222 return false; 223 } 224 } 225 } 226 227 /** 228 * Only kept for UnsupportedAppUsage. 229 * 230 * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. 231 */ 232 @Deprecated 233 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) hasCache()234 public boolean hasCache() { 235 return mTimeResult != null; 236 } 237 238 /** 239 * Only kept for UnsupportedAppUsage. 240 * 241 * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. 242 */ 243 @Deprecated 244 @Override getCacheAge()245 public long getCacheAge() { 246 TimeResult timeResult = mTimeResult; 247 if (timeResult != null) { 248 return SystemClock.elapsedRealtime() - timeResult.getElapsedRealtimeMillis(); 249 } else { 250 return Long.MAX_VALUE; 251 } 252 } 253 254 /** 255 * Only kept for UnsupportedAppUsage. 256 * 257 * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. 258 */ 259 @Deprecated 260 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) currentTimeMillis()261 public long currentTimeMillis() { 262 TimeResult timeResult = mTimeResult; 263 if (timeResult == null) { 264 throw new IllegalStateException("Missing authoritative time source"); 265 } 266 if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit"); 267 268 // current time is age after the last ntp cache; callers who 269 // want fresh values will hit forceRefresh() first. 270 return timeResult.currentTimeMillis(); 271 } 272 273 /** 274 * Only kept for UnsupportedAppUsage. 275 * 276 * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. 277 */ 278 @Deprecated 279 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getCachedNtpTime()280 public long getCachedNtpTime() { 281 if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit"); 282 TimeResult timeResult = mTimeResult; 283 return timeResult == null ? 0 : timeResult.getTimeMillis(); 284 } 285 286 /** 287 * Only kept for UnsupportedAppUsage. 288 * 289 * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically. 290 */ 291 @Deprecated 292 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getCachedNtpTimeReference()293 public long getCachedNtpTimeReference() { 294 TimeResult timeResult = mTimeResult; 295 return timeResult == null ? 0 : timeResult.getElapsedRealtimeMillis(); 296 } 297 298 /** 299 * Returns an object containing the latest NTP information available. Can return {@code null} if 300 * no information is available. 301 */ 302 @Nullable getCachedTimeResult()303 public TimeResult getCachedTimeResult() { 304 return mTimeResult; 305 } 306 307 /** Clears the last received NTP. Intended for use during tests. */ clearCachedTimeResult()308 public void clearCachedTimeResult() { 309 synchronized (this) { 310 mTimeResult = null; 311 } 312 } 313 314 private static class NtpConnectionInfo { 315 316 @NonNull private final String mServer; 317 private final int mPort; 318 private final int mTimeoutMillis; 319 NtpConnectionInfo(@onNull String server, int port, int timeoutMillis)320 NtpConnectionInfo(@NonNull String server, int port, int timeoutMillis) { 321 mServer = Objects.requireNonNull(server); 322 mPort = port; 323 mTimeoutMillis = timeoutMillis; 324 } 325 326 @NonNull getServer()327 public String getServer() { 328 return mServer; 329 } 330 331 @NonNull getPort()332 public int getPort() { 333 return mPort; 334 } 335 getTimeoutMillis()336 int getTimeoutMillis() { 337 return mTimeoutMillis; 338 } 339 340 @Override toString()341 public String toString() { 342 return "NtpConnectionInfo{" 343 + "mServer='" + mServer + '\'' 344 + ", mPort='" + mPort + '\'' 345 + ", mTimeoutMillis=" + mTimeoutMillis 346 + '}'; 347 } 348 } 349 350 @GuardedBy("this") getNtpConnectionInfo()351 private NtpConnectionInfo getNtpConnectionInfo() { 352 final ContentResolver resolver = mContext.getContentResolver(); 353 354 final Resources res = mContext.getResources(); 355 356 final String hostname; 357 if (mHostnameForTests != null) { 358 hostname = mHostnameForTests; 359 } else { 360 String serverGlobalSetting = 361 Settings.Global.getString(resolver, Settings.Global.NTP_SERVER); 362 if (serverGlobalSetting != null) { 363 hostname = serverGlobalSetting; 364 } else { 365 hostname = res.getString(com.android.internal.R.string.config_ntpServer); 366 } 367 } 368 369 final Integer port; 370 if (mPortForTests != null) { 371 port = mPortForTests; 372 } else { 373 port = SntpClient.STANDARD_NTP_PORT; 374 } 375 376 final int timeoutMillis; 377 if (mTimeoutForTests != null) { 378 timeoutMillis = (int) mTimeoutForTests.toMillis(); 379 } else { 380 int defaultTimeoutMillis = 381 res.getInteger(com.android.internal.R.integer.config_ntpTimeout); 382 timeoutMillis = Settings.Global.getInt( 383 resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis); 384 } 385 return TextUtils.isEmpty(hostname) ? null : 386 new NtpConnectionInfo(hostname, port, timeoutMillis); 387 } 388 389 /** Prints debug information. */ dump(PrintWriter pw)390 public void dump(PrintWriter pw) { 391 synchronized (this) { 392 pw.println("getNtpConnectionInfo()=" + getNtpConnectionInfo()); 393 pw.println("mTimeResult=" + mTimeResult); 394 if (mTimeResult != null) { 395 pw.println("mTimeResult.getAgeMillis()=" 396 + Duration.ofMillis(mTimeResult.getAgeMillis())); 397 } 398 } 399 } 400 } 401