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.SystemClock;
30 import android.provider.Settings;
31 import android.text.TextUtils;
32 
33 import com.android.internal.annotations.GuardedBy;
34 
35 import java.util.Objects;
36 import java.util.function.Supplier;
37 
38 /**
39  * A singleton that connects with a remote NTP server as its trusted time source. This class
40  * is thread-safe. The {@link #forceRefresh()} method is synchronous, i.e. it may occupy the
41  * current thread while performing an NTP request. All other threads calling {@link #forceRefresh()}
42  * will block during that request.
43  *
44  * @hide
45  */
46 public class NtpTrustedTime implements TrustedTime {
47 
48     /**
49      * The result of a successful NTP query.
50      *
51      * @hide
52      */
53     public static class TimeResult {
54         private final long mTimeMillis;
55         private final long mElapsedRealtimeMillis;
56         private final long mCertaintyMillis;
57 
TimeResult(long timeMillis, long elapsedRealtimeMillis, long certaintyMillis)58         public TimeResult(long timeMillis, long elapsedRealtimeMillis, long certaintyMillis) {
59             mTimeMillis = timeMillis;
60             mElapsedRealtimeMillis = elapsedRealtimeMillis;
61             mCertaintyMillis = certaintyMillis;
62         }
63 
getTimeMillis()64         public long getTimeMillis() {
65             return mTimeMillis;
66         }
67 
getElapsedRealtimeMillis()68         public long getElapsedRealtimeMillis() {
69             return mElapsedRealtimeMillis;
70         }
71 
getCertaintyMillis()72         public long getCertaintyMillis() {
73             return mCertaintyMillis;
74         }
75 
76         /** Calculates and returns the current time accounting for the age of this result. */
currentTimeMillis()77         public long currentTimeMillis() {
78             return mTimeMillis + getAgeMillis();
79         }
80 
81         /** Calculates and returns the age of this result. */
getAgeMillis()82         public long getAgeMillis() {
83             return SystemClock.elapsedRealtime() - mElapsedRealtimeMillis;
84         }
85 
86         @Override
toString()87         public String toString() {
88             return "TimeResult{"
89                     + "mTimeMillis=" + mTimeMillis
90                     + ", mElapsedRealtimeMillis=" + mElapsedRealtimeMillis
91                     + ", mCertaintyMillis=" + mCertaintyMillis
92                     + '}';
93         }
94     }
95 
96     private static final String TAG = "NtpTrustedTime";
97     private static final boolean LOGD = false;
98 
99     private static NtpTrustedTime sSingleton;
100 
101     @NonNull
102     private final Context mContext;
103 
104     /**
105      * A supplier that returns the ConnectivityManager. The Supplier can return null if
106      * ConnectivityService isn't running yet.
107      */
108     private final Supplier<ConnectivityManager> mConnectivityManagerSupplier =
109             new Supplier<ConnectivityManager>() {
110         private ConnectivityManager mConnectivityManager;
111 
112         @Nullable
113         @Override
114         public synchronized ConnectivityManager get() {
115             // We can't do this at initialization time: ConnectivityService might not be running
116             // yet.
117             if (mConnectivityManager == null) {
118                 mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
119             }
120             return mConnectivityManager;
121         }
122     };
123 
124     // Declared volatile and accessed outside of synchronized blocks to avoid blocking reads during
125     // forceRefresh().
126     private volatile TimeResult mTimeResult;
127 
NtpTrustedTime(Context context)128     private NtpTrustedTime(Context context) {
129         mContext = Objects.requireNonNull(context);
130     }
131 
132     @UnsupportedAppUsage
getInstance(Context context)133     public static synchronized NtpTrustedTime getInstance(Context context) {
134         if (sSingleton == null) {
135             Context appContext = context.getApplicationContext();
136             sSingleton = new NtpTrustedTime(appContext);
137         }
138         return sSingleton;
139     }
140 
141     @UnsupportedAppUsage
forceRefresh()142     public boolean forceRefresh() {
143         synchronized (this) {
144             NtpConnectionInfo connectionInfo = getNtpConnectionInfo();
145             if (connectionInfo == null) {
146                 // missing server config, so no trusted time available
147                 if (LOGD) Log.d(TAG, "forceRefresh: invalid server config");
148                 return false;
149             }
150 
151             ConnectivityManager connectivityManager = mConnectivityManagerSupplier.get();
152             if (connectivityManager == null) {
153                 if (LOGD) Log.d(TAG, "forceRefresh: no ConnectivityManager");
154                 return false;
155             }
156             final Network network = connectivityManager.getActiveNetwork();
157             final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
158             if (ni == null || !ni.isConnected()) {
159                 if (LOGD) Log.d(TAG, "forceRefresh: no connectivity");
160                 return false;
161             }
162 
163             if (LOGD) Log.d(TAG, "forceRefresh() from cache miss");
164             final SntpClient client = new SntpClient();
165             final String serverName = connectionInfo.getServer();
166             final int timeoutMillis = connectionInfo.getTimeoutMillis();
167             if (client.requestTime(serverName, timeoutMillis, network)) {
168                 long ntpCertainty = client.getRoundTripTime() / 2;
169                 mTimeResult = new TimeResult(
170                         client.getNtpTime(), client.getNtpTimeReference(), ntpCertainty);
171                 return true;
172             } else {
173                 return false;
174             }
175         }
176     }
177 
178     /**
179      * Only kept for UnsupportedAppUsage.
180      *
181      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
182      */
183     @Deprecated
184     @UnsupportedAppUsage
hasCache()185     public boolean hasCache() {
186         return mTimeResult != null;
187     }
188 
189     /**
190      * Only kept for UnsupportedAppUsage.
191      *
192      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
193      */
194     @Deprecated
195     @Override
getCacheAge()196     public long getCacheAge() {
197         TimeResult timeResult = mTimeResult;
198         if (timeResult != null) {
199             return SystemClock.elapsedRealtime() - timeResult.getElapsedRealtimeMillis();
200         } else {
201             return Long.MAX_VALUE;
202         }
203     }
204 
205     /**
206      * Only kept for UnsupportedAppUsage.
207      *
208      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
209      */
210     @Deprecated
211     @UnsupportedAppUsage
currentTimeMillis()212     public long currentTimeMillis() {
213         TimeResult timeResult = mTimeResult;
214         if (timeResult == null) {
215             throw new IllegalStateException("Missing authoritative time source");
216         }
217         if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit");
218 
219         // current time is age after the last ntp cache; callers who
220         // want fresh values will hit forceRefresh() first.
221         return timeResult.currentTimeMillis();
222     }
223 
224     /**
225      * Only kept for UnsupportedAppUsage.
226      *
227      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
228      */
229     @Deprecated
230     @UnsupportedAppUsage
getCachedNtpTime()231     public long getCachedNtpTime() {
232         if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit");
233         TimeResult timeResult = mTimeResult;
234         return timeResult == null ? 0 : timeResult.getTimeMillis();
235     }
236 
237     /**
238      * Only kept for UnsupportedAppUsage.
239      *
240      * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
241      */
242     @Deprecated
243     @UnsupportedAppUsage
getCachedNtpTimeReference()244     public long getCachedNtpTimeReference() {
245         TimeResult timeResult = mTimeResult;
246         return timeResult == null ? 0 : timeResult.getElapsedRealtimeMillis();
247     }
248 
249     /**
250      * Returns an object containing the latest NTP information available. Can return {@code null} if
251      * no information is available.
252      */
253     @Nullable
getCachedTimeResult()254     public TimeResult getCachedTimeResult() {
255         return mTimeResult;
256     }
257 
258     private static class NtpConnectionInfo {
259 
260         @NonNull private final String mServer;
261         private final int mTimeoutMillis;
262 
NtpConnectionInfo(@onNull String server, int timeoutMillis)263         NtpConnectionInfo(@NonNull String server, int timeoutMillis) {
264             mServer = Objects.requireNonNull(server);
265             mTimeoutMillis = timeoutMillis;
266         }
267 
268         @NonNull
getServer()269         public String getServer() {
270             return mServer;
271         }
272 
getTimeoutMillis()273         int getTimeoutMillis() {
274             return mTimeoutMillis;
275         }
276     }
277 
278     @GuardedBy("this")
getNtpConnectionInfo()279     private NtpConnectionInfo getNtpConnectionInfo() {
280         final ContentResolver resolver = mContext.getContentResolver();
281 
282         final Resources res = mContext.getResources();
283         final String defaultServer = res.getString(
284                 com.android.internal.R.string.config_ntpServer);
285         final int defaultTimeoutMillis = res.getInteger(
286                 com.android.internal.R.integer.config_ntpTimeout);
287 
288         final String secureServer = Settings.Global.getString(
289                 resolver, Settings.Global.NTP_SERVER);
290         final int timeoutMillis = Settings.Global.getInt(
291                 resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis);
292 
293         final String server = secureServer != null ? secureServer : defaultServer;
294         return TextUtils.isEmpty(server) ? null : new NtpConnectionInfo(server, timeoutMillis);
295     }
296 }
297