├── LICENSE
├── README.md
└── SNTPClient.java
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Aslam Anver
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### SNTPClient for Android
2 | Simple SNTP Client class for retrieving network time on Android (SNTPClient)
3 |
4 |
5 |
6 | #### Copy-Paste
7 | Copy the `SNTPClient.java` into your project, there you go. It's ready.
8 |
9 | #### Usage
10 |
11 | 1. Retrieve the time of a specific time zone.
12 |
13 | ```java
14 | SNTPClient.getDate(TimeZone.getTimeZone("Asia/Colombo"), new SNTPClient.Listener() {
15 |
16 | @Override
17 | public void onTimeResponse(String rawDate, Date date, Exception ex) {
18 |
19 | }
20 | });
21 | ```
22 |
36 | * Simple SNTP client class for retrieving network time. 37 | *
38 | * Sample usage: 39 | *
SntpClient client = new SntpClient();
40 | * if (client.requestTime("time.foo.com")) {
41 | * long now = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference();
42 | * }
43 | *
44 | */
45 | public class SNTPClient {
46 |
47 | public interface Listener {
48 | void onTimeResponse(String rawDate, Date date, Exception ex);
49 | }
50 |
51 | public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
52 | public static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat(DATE_FORMAT, Locale.US);
53 |
54 | private static final int REFERENCE_TIME_OFFSET = 16;
55 | private static final int ORIGINATE_TIME_OFFSET = 24;
56 | private static final int RECEIVE_TIME_OFFSET = 32;
57 | private static final int TRANSMIT_TIME_OFFSET = 40;
58 | private static final int NTP_PACKET_SIZE = 48;
59 |
60 | private static final int NTP_PORT = 123;
61 | private static final int NTP_MODE_CLIENT = 3;
62 | private static final int NTP_VERSION = 3;
63 |
64 | // Number of seconds between Jan 1, 1900 and Jan 1, 1970
65 | // 70 years plus 17 leap days
66 | private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
67 |
68 | // system time computed from NTP server response
69 | private long mNtpTime;
70 |
71 | // value of SystemClock.elapsedRealtime() corresponding to mNtpTime
72 | private long mNtpTimeReference;
73 |
74 | // round trip time in milliseconds
75 | private long mRoundTripTime;
76 |
77 | // callback listener
78 | private Listener listener;
79 |
80 | /**
81 | * Construct SntpClient for retrieve time with callback
82 | *
83 | * @param listener callback listener after time received.
84 | */
85 | SNTPClient(Listener listener) {
86 | this.listener = listener;
87 | }
88 |
89 | /**
90 | * Sends an SNTP request to the given host and processes the response.
91 | *
92 | * @param host host name of the server.
93 | * @param timeout network timeout in milliseconds.
94 | * @return true if the transaction was successful.
95 | */
96 | public boolean requestTime(String host, int timeout) {
97 | DatagramSocket socket = null;
98 | try {
99 | socket = new DatagramSocket();
100 | socket.setSoTimeout(timeout);
101 | InetAddress address = InetAddress.getByName(host);
102 | byte[] buffer = new byte[NTP_PACKET_SIZE];
103 | DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT);
104 |
105 | // set mode = 3 (client) and version = 3
106 | // mode is in low 3 bits of first byte
107 | // version is in bits 3-5 of first byte
108 | buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3);
109 |
110 | // get current time and write it to the request packet
111 | long requestTime = System.currentTimeMillis();
112 | long requestTicks = SystemClock.elapsedRealtime();
113 | writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime);
114 |
115 | socket.send(request);
116 |
117 | // read the response
118 | DatagramPacket response = new DatagramPacket(buffer, buffer.length);
119 | socket.receive(response);
120 | long responseTicks = SystemClock.elapsedRealtime();
121 | long responseTime = requestTime + (responseTicks - requestTicks);
122 |
123 | // extract the results
124 | long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
125 | long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
126 | long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
127 | long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime);
128 | // receiveTime = originateTime + transit + skew
129 | // responseTime = transmitTime + transit - skew
130 | // clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2
131 | // = ((originateTime + transit + skew - originateTime) +
132 | // (transmitTime - (transmitTime + transit - skew)))/2
133 | // = ((transit + skew) + (transmitTime - transmitTime - transit + skew))/2
134 | // = (transit + skew - transit + skew)/2
135 | // = (2 * skew)/2 = skew
136 | long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2;
137 | // if (false) Log.d(TAG, "round trip: " + roundTripTime + " ms");
138 | // if (false) Log.d(TAG, "clock offset: " + clockOffset + " ms");
139 |
140 | // save our results - use the times on this side of the network latency
141 | // (response rather than request time)
142 | mNtpTime = responseTime + clockOffset;
143 | mNtpTimeReference = responseTicks;
144 | mRoundTripTime = roundTripTime;
145 | } catch (Exception e) {
146 | new Handler(Looper.getMainLooper()).post(new Runnable() {
147 | @Override
148 | public void run() {
149 | listener.onTimeResponse(null, null, e);
150 | }
151 | });
152 | return false;
153 | } finally {
154 | if (socket != null) {
155 | socket.close();
156 | }
157 | }
158 |
159 | return true;
160 | }
161 |
162 | /**
163 | * Returns the time computed from the NTP transaction.
164 | *
165 | * @return time value computed from NTP server response.
166 | */
167 | public long getNtpTime() {
168 | return mNtpTime;
169 | }
170 |
171 | /**
172 | * Returns the reference clock value (value of SystemClock.elapsedRealtime())
173 | * corresponding to the NTP time.
174 | *
175 | * @return reference clock corresponding to the NTP time.
176 | */
177 | public long getNtpTimeReference() {
178 | return mNtpTimeReference;
179 | }
180 |
181 | /**
182 | * Returns the round trip time of the NTP transaction
183 | *
184 | * @return round trip time in milliseconds.
185 | */
186 | public long getRoundTripTime() {
187 | return mRoundTripTime;
188 | }
189 |
190 | /**
191 | * Reads an unsigned 32 bit big endian number from the given offset in the buffer.
192 | */
193 | private long read32(byte[] buffer, int offset) {
194 | byte b0 = buffer[offset];
195 | byte b1 = buffer[offset + 1];
196 | byte b2 = buffer[offset + 2];
197 | byte b3 = buffer[offset + 3];
198 |
199 | // convert signed bytes to unsigned values
200 | int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0);
201 | int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1);
202 | int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2);
203 | int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3);
204 |
205 | return ((long) i0 << 24) + ((long) i1 << 16) + ((long) i2 << 8) + (long) i3;
206 | }
207 |
208 | /**
209 | * Reads the NTP time stamp at the given offset in the buffer and returns
210 | * it as a system time (milliseconds since January 1, 1970).
211 | */
212 | private long readTimeStamp(byte[] buffer, int offset) {
213 | long seconds = read32(buffer, offset);
214 | long fraction = read32(buffer, offset + 4);
215 | return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L);
216 | }
217 |
218 | /**
219 | * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp
220 | * at the given offset in the buffer.
221 | */
222 | private void writeTimeStamp(byte[] buffer, int offset, long time) {
223 | long seconds = time / 1000L;
224 | long milliseconds = time - seconds * 1000L;
225 | seconds += OFFSET_1900_TO_1970;
226 |
227 | // write seconds in big endian format
228 | buffer[offset++] = (byte) (seconds >> 24);
229 | buffer[offset++] = (byte) (seconds >> 16);
230 | buffer[offset++] = (byte) (seconds >> 8);
231 | buffer[offset++] = (byte) (seconds >> 0);
232 |
233 | long fraction = milliseconds * 0x100000000L / 1000L;
234 | // write fraction in big endian format
235 | buffer[offset++] = (byte) (fraction >> 24);
236 | buffer[offset++] = (byte) (fraction >> 16);
237 | buffer[offset++] = (byte) (fraction >> 8);
238 | // low order bits should be random data
239 | buffer[offset++] = (byte) (Math.random() * 255.0);
240 | }
241 |
242 | public static void getDate(TimeZone _timeZone, Listener _listener) {
243 |
244 | new Thread(() -> {
245 |
246 | SNTPClient sntpClient = new SNTPClient(_listener);
247 |
248 | if (sntpClient.requestTime("time.google.com", 5000)) {
249 |
250 | long nowAsPerDeviceTimeZone = sntpClient.getNtpTime();
251 |
252 | SIMPLE_DATE_FORMAT.setTimeZone(_timeZone);
253 | String rawDate = SIMPLE_DATE_FORMAT.format(nowAsPerDeviceTimeZone);
254 |
255 | try {
256 | Date date = SIMPLE_DATE_FORMAT.parse(rawDate);
257 | new Handler(Looper.getMainLooper()).post(() -> _listener.onTimeResponse(rawDate, date, null));
258 | } catch (ParseException e) {
259 | new Handler(Looper.getMainLooper()).post(() -> _listener.onTimeResponse(null, null, e));
260 | }
261 | }
262 | }).start();
263 | }
264 | }
265 |
--------------------------------------------------------------------------------