• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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