) {}
+ override fun lastSntpRequestAttempt(ipHost: InetAddress) {}
+ override fun sntpRequestFailed(e: Exception) {}
+ override fun syncDispatcherException(t: Throwable) {}
+
+ override fun sntpRequest(address: InetAddress) {}
+ override fun sntpRequestSuccessful(address: InetAddress) {}
+ override fun sntpRequestFailed(address: InetAddress, e: Exception) {}
+
+ override fun storingTrueTime(ntpResult: LongArray) {}
+ override fun returningTrueTime(trueTime: Date) {}
+ override fun returningDeviceTime() {}
+}
diff --git a/library/src/main/java/com/instacart/truetime/sntp/Sntp.kt b/library/src/main/java/com/instacart/truetime/sntp/Sntp.kt
new file mode 100644
index 00000000..0cd69b75
--- /dev/null
+++ b/library/src/main/java/com/instacart/truetime/sntp/Sntp.kt
@@ -0,0 +1,46 @@
+package com.instacart.truetime.sntp
+
+import com.instacart.truetime.SntpEventListener
+import java.io.IOException
+import java.net.InetAddress
+
+interface Sntp {
+
+ /**
+ * See δ :
+ * https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm
+ */
+ fun roundTripDelay(ntpResult: LongArray): Long
+
+ /**
+ * See θ :
+ * https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm
+ */
+ fun clockOffset(ntpResult: LongArray): Long
+
+ /**
+ * @return NTP/"true" time when NTP call was made
+ */
+ fun trueTime(ntpResult: LongArray): Long
+
+ /**
+ * @return milliseconds since boot (including time spent in sleep) when NTP call was made
+ */
+ fun timeSinceBoot(ntpResult: LongArray): Long
+
+ /**
+ * Sends an NTP request to the given host and processes the response.
+ *
+ * @param ntpHostAddress host name of the server.
+ */
+ @Throws(IOException::class)
+ fun requestTime(
+ ntpHostAddress: InetAddress,
+ rootDelayMax: Float,
+ rootDispersionMax: Float,
+ serverResponseDelayMax: Int,
+ timeoutInMillis: Int,
+ listener: SntpEventListener,
+ ): LongArray
+
+}
diff --git a/library/src/main/java/com/instacart/truetime/sntp/SntpImpl.java b/library/src/main/java/com/instacart/truetime/sntp/SntpImpl.java
new file mode 100644
index 00000000..38b76505
--- /dev/null
+++ b/library/src/main/java/com/instacart/truetime/sntp/SntpImpl.java
@@ -0,0 +1,319 @@
+package com.instacart.truetime.sntp;
+
+/*
+ * Original work Copyright (C) 2008 The Android Open Source Project
+ * Modified work Copyright (C) 2016, Instacart
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.os.SystemClock;
+import com.instacart.truetime.InvalidNtpServerResponseException;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+
+import com.instacart.truetime.SntpEventListener;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Simple SNTP client class for retrieving network time. Original source: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/net/SntpClient.java
+ *
+ * Intentionally keeping this Java for easier diffing and keeping up to date with platform
+ */
+public class SntpImpl implements Sntp {
+
+ public static final int RESPONSE_INDEX_ORIGINATE_TIME = 0;
+ public static final int RESPONSE_INDEX_RECEIVE_TIME = 1;
+ public static final int RESPONSE_INDEX_TRANSMIT_TIME = 2;
+ public static final int RESPONSE_INDEX_RESPONSE_TIME = 3;
+ public static final int RESPONSE_INDEX_ROOT_DELAY = 4;
+ public static final int RESPONSE_INDEX_DISPERSION = 5;
+ public static final int RESPONSE_INDEX_STRATUM = 6;
+ public static final int RESPONSE_INDEX_RESPONSE_TICKS = 7;
+ public static final int RESPONSE_INDEX_SIZE = 8;
+
+ private static final int NTP_PORT = 123;
+ private static final int NTP_MODE = 3;
+ private static final int NTP_VERSION = 3;
+ private static final int NTP_PACKET_SIZE = 48;
+
+ private static final int INDEX_VERSION = 0;
+ private static final int INDEX_ROOT_DELAY = 4;
+ private static final int INDEX_ROOT_DISPERSION = 8;
+ private static final int INDEX_ORIGINATE_TIME = 24;
+ private static final int INDEX_RECEIVE_TIME = 32;
+ private static final int INDEX_TRANSMIT_TIME = 40;
+
+ // 70 years plus 17 leap days
+ private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
+
+ public SntpImpl() {}
+
+ @Override
+ public long roundTripDelay(@NotNull long[] response) {
+ return (response[RESPONSE_INDEX_RESPONSE_TIME] - response[RESPONSE_INDEX_ORIGINATE_TIME]) -
+ (response[RESPONSE_INDEX_TRANSMIT_TIME] - response[RESPONSE_INDEX_RECEIVE_TIME]);
+ }
+
+ @Override
+ public long clockOffset(@NotNull long[] response) {
+ return ((response[RESPONSE_INDEX_RECEIVE_TIME] - response[RESPONSE_INDEX_ORIGINATE_TIME]) +
+ (response[RESPONSE_INDEX_TRANSMIT_TIME] - response[RESPONSE_INDEX_RESPONSE_TIME])) / 2;
+ }
+
+ @Override
+ public long trueTime(@NotNull long[] response) {
+ long clockOffset = clockOffset(response);
+ long responseTime = response[RESPONSE_INDEX_RESPONSE_TIME];
+ return responseTime + clockOffset;
+ }
+
+ @Override
+ public long timeSinceBoot(@NotNull long[] ntpResult) {
+ return ntpResult[RESPONSE_INDEX_RESPONSE_TICKS];
+ }
+
+ /**
+ * Sends an NTP request to the given host and processes the response.
+ *
+ * @param address host name of the server.
+ */
+ @NotNull
+ @Override
+ public synchronized long[] requestTime(
+ InetAddress address,
+ float rootDelayMax,
+ float rootDispersionMax,
+ int serverResponseDelayMax,
+ int timeoutInMillis,
+ SntpEventListener listener
+ ) throws IOException {
+
+ listener.sntpRequest(address);
+
+ DatagramSocket socket = null;
+
+ try {
+
+ socket = new DatagramSocket();
+ socket.setSoTimeout(timeoutInMillis);
+ byte[] buffer = new byte[NTP_PACKET_SIZE];
+ DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT);
+
+ writeNtpVersion(buffer);
+
+ // -----------------------------------------------------------------------------------
+ // get current time and write it to the request packet
+
+ long requestTime = System.currentTimeMillis();
+
+ // TODO: Move android dependency to separate package
+ // so we can make Truetime a pure kotlin library
+ long requestTicks = SystemClock.elapsedRealtime();
+
+ writeTimeStamp(buffer, INDEX_TRANSMIT_TIME, requestTime);
+
+ socket.send(request);
+
+ // -----------------------------------------------------------------------------------
+ // read the response
+
+ long[] t = new long[RESPONSE_INDEX_SIZE];
+ DatagramPacket response = new DatagramPacket(buffer, buffer.length);
+ socket.receive(response);
+
+ long responseTicks = SystemClock.elapsedRealtime();
+ t[RESPONSE_INDEX_RESPONSE_TICKS] = responseTicks;
+
+ // -----------------------------------------------------------------------------------
+ // extract the results
+ // See here for the algorithm used:
+ // https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm
+
+ long originateTime = readTimeStamp(buffer, INDEX_ORIGINATE_TIME); // T0
+ long receiveTime = readTimeStamp(buffer, INDEX_RECEIVE_TIME); // T1
+ long transmitTime = readTimeStamp(buffer, INDEX_TRANSMIT_TIME); // T2
+ long responseTime = requestTime + (responseTicks - requestTicks); // T3
+
+ t[RESPONSE_INDEX_ORIGINATE_TIME] = originateTime;
+ t[RESPONSE_INDEX_RECEIVE_TIME] = receiveTime;
+ t[RESPONSE_INDEX_TRANSMIT_TIME] = transmitTime;
+ t[RESPONSE_INDEX_RESPONSE_TIME] = responseTime;
+
+ // -----------------------------------------------------------------------------------
+ // check validity of response
+
+ t[RESPONSE_INDEX_ROOT_DELAY] = read(buffer, INDEX_ROOT_DELAY);
+ double rootDelay = doubleMillis(t[RESPONSE_INDEX_ROOT_DELAY]);
+ if (rootDelay > rootDelayMax) {
+ throw new InvalidNtpServerResponseException(
+ "Invalid response from NTP server. %s violation. %f [actual] > %f [expected]",
+ "root_delay",
+ (float) rootDelay,
+ rootDelayMax);
+ }
+
+ t[RESPONSE_INDEX_DISPERSION] = read(buffer, INDEX_ROOT_DISPERSION);
+ double rootDispersion = doubleMillis(t[RESPONSE_INDEX_DISPERSION]);
+ if (rootDispersion > rootDispersionMax) {
+ throw new InvalidNtpServerResponseException(
+ "Invalid response from NTP server. %s violation. %f [actual] > %f [expected]",
+ "root_dispersion",
+ (float) rootDispersion,
+ rootDispersionMax);
+ }
+
+ final byte mode = (byte) (buffer[0] & 0x7);
+ if (mode != 4 && mode != 5) {
+ throw new InvalidNtpServerResponseException("untrusted mode value for TrueTime: " + mode);
+ }
+
+ final int stratum = buffer[1] & 0xff;
+ t[RESPONSE_INDEX_STRATUM] = stratum;
+ if (stratum < 1 || stratum > 15) {
+ throw new InvalidNtpServerResponseException("untrusted stratum value for TrueTime: " + stratum);
+ }
+
+ final byte leap = (byte) ((buffer[0] >> 6) & 0x3);
+ if (leap == 3) {
+ throw new InvalidNtpServerResponseException("unsynchronized server responded for TrueTime");
+ }
+
+ double delay = Math.abs((responseTime - originateTime) - (transmitTime - receiveTime));
+ if (delay >= serverResponseDelayMax) {
+ throw new InvalidNtpServerResponseException(
+ "%s too large for comfort %f [actual] >= %f [expected]",
+ "server_response_delay",
+ (float) delay,
+ serverResponseDelayMax);
+ }
+
+ long timeElapsedSinceRequest = Math.abs(originateTime - System.currentTimeMillis());
+ if (timeElapsedSinceRequest >= 10_000) {
+ throw new InvalidNtpServerResponseException("Request was sent more than 10 seconds back " +
+ timeElapsedSinceRequest);
+ }
+
+ listener.sntpRequestSuccessful(address);
+
+ return t;
+
+ } catch (Exception e) {
+ listener.sntpRequestFailed(address, e);
+ throw e;
+ } finally {
+ if (socket != null) {
+ socket.close();
+ }
+ }
+ }
+
+ //region private helpers
+
+ /**
+ * Writes NTP version as defined in RFC-1305
+ */
+ private void writeNtpVersion(byte[] buffer) {
+ // mode is in low 3 bits of first byte
+ // version is in bits 3-5 of first byte
+ buffer[INDEX_VERSION] = NTP_MODE | (NTP_VERSION << 3);
+ }
+
+ /**
+ * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp as defined in RFC-1305 at the given
+ * offset in the buffer
+ */
+ private void writeTimeStamp(byte[] buffer, int offset, long time) {
+
+ long seconds = time / 1000L;
+ long milliseconds = time - seconds * 1000L;
+
+ // consider offset for number of seconds
+ // between Jan 1, 1900 (NTP epoch) and Jan 1, 1970 (Java epoch)
+ seconds += OFFSET_1900_TO_1970;
+
+ // write seconds in big endian format
+ buffer[offset++] = (byte) (seconds >> 24);
+ buffer[offset++] = (byte) (seconds >> 16);
+ buffer[offset++] = (byte) (seconds >> 8);
+ buffer[offset++] = (byte) (seconds >> 0);
+
+ long fraction = milliseconds * 0x100000000L / 1000L;
+
+ // write fraction in big endian format
+ buffer[offset++] = (byte) (fraction >> 24);
+ buffer[offset++] = (byte) (fraction >> 16);
+ buffer[offset++] = (byte) (fraction >> 8);
+
+ // low order bits should be random data
+ buffer[offset++] = (byte) (Math.random() * 255.0);
+ }
+
+ /**
+ * @param offset offset index in buffer to start reading from
+ * @return NTP timestamp in Java epoch
+ */
+ private long readTimeStamp(byte[] buffer, int offset) {
+ long seconds = read(buffer, offset);
+ long fraction = read(buffer, offset + 4);
+
+ return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L);
+ }
+
+ /**
+ * Reads an unsigned 32 bit big endian number from the given offset in the buffer
+ *
+ * @return 4 bytes as a 32-bit long (unsigned big endian)
+ */
+ private long read(byte[] buffer, int offset) {
+ byte b0 = buffer[offset];
+ byte b1 = buffer[offset + 1];
+ byte b2 = buffer[offset + 2];
+ byte b3 = buffer[offset + 3];
+
+ return ((long) ui(b0) << 24) +
+ ((long) ui(b1) << 16) +
+ ((long) ui(b2) << 8) +
+ (long) ui(b3);
+ }
+
+ /***
+ * Convert (signed) byte to an unsigned int
+ *
+ * Java only has signed types so we have to do
+ * more work to get unsigned ops
+ *
+ * @param b input byte
+ * @return unsigned int value of byte
+ */
+ private int ui(byte b) {
+ return b & 0xFF;
+ }
+
+ /**
+ * Used for root delay and dispersion
+ *
+ * According to the NTP spec, they are in the NTP Short format viz. signed 16.16 fixed point
+ *
+ * @param fix signed fixed point number
+ * @return as a double in milliseconds
+ */
+ private double doubleMillis(long fix) {
+ return fix / 65.536D;
+ }
+ //endregion
+
+}
diff --git a/library/src/main/java/com/instacart/truetime/time/TimeKeeper.kt b/library/src/main/java/com/instacart/truetime/time/TimeKeeper.kt
new file mode 100644
index 00000000..4b901b51
--- /dev/null
+++ b/library/src/main/java/com/instacart/truetime/time/TimeKeeper.kt
@@ -0,0 +1,39 @@
+package com.instacart.truetime.time
+
+import android.os.SystemClock
+import com.instacart.truetime.TimeKeeperListener
+import com.instacart.truetime.sntp.Sntp
+import java.util.Date
+import java.util.concurrent.atomic.AtomicReference
+
+// TODO: move android dependency to separate package
+// so we can make Truetime a pure kotlin library
+
+/**
+ * Helper class that stores the NTP [LongArray] result
+ * and derives true time from that result
+ */
+internal class TimeKeeper(
+ private val sntp: Sntp,
+ private val listener: TimeKeeperListener,
+) {
+ private var ttResult: AtomicReference = AtomicReference()
+
+ fun hasTheTime(): Boolean = ttResult.get() != null
+
+ fun save(ntpResult: LongArray) {
+ listener.storingTrueTime(ntpResult)
+ ttResult.set(ntpResult)
+ }
+
+ fun now(): Date {
+ val ntpResult = ttResult.get()
+ val savedSntpTime: Long = sntp.trueTime(ntpResult)
+ val timeSinceBoot: Long = sntp.timeSinceBoot(ntpResult)
+ val currentTimeSinceBoot: Long = SystemClock.elapsedRealtime()
+ val trueTime = Date(savedSntpTime + (currentTimeSinceBoot - timeSinceBoot))
+
+ listener.returningTrueTime(trueTime)
+ return trueTime
+ }
+}
diff --git a/library/src/main/java/com/instacart/truetime/time/TrueTime.kt b/library/src/main/java/com/instacart/truetime/time/TrueTime.kt
new file mode 100644
index 00000000..a5d2dacd
--- /dev/null
+++ b/library/src/main/java/com/instacart/truetime/time/TrueTime.kt
@@ -0,0 +1,37 @@
+package com.instacart.truetime.time
+
+import kotlinx.coroutines.Job
+import java.util.Date
+
+interface TrueTime {
+
+ /**
+ * Run [com.instacart.truetime.sntp.Sntp.requestTime]
+ * in the background repeatedly to account for clock drift
+ * and update the locally stored SNTP result
+ *
+ * @return Use this Coroutines job
+ * to cancel the [sync] and all background work
+ */
+ fun sync(): Job
+
+ fun hasTheTime(): Boolean
+
+ /**
+ * This is [TrueTime]'s main function to get time
+ * It should respect [TrueTimeParameters.returnSafelyWhenUninitialized] setting
+ */
+ fun now(): Date
+
+ /**
+ * return the current time as calculated by TrueTime.
+ * If TrueTime doesn't [hasTheTime], will throw [IllegalStateException]
+ */
+ @Throws(IllegalStateException::class)
+ fun nowTrueOnly(): Date
+
+ /**
+ * return [nowTrueOnly] if TrueTime is available otherwise fallback to System clock date
+ */
+ fun nowSafely(): Date = if (hasTheTime()) nowTrueOnly() else Date()
+}
diff --git a/library/src/main/java/com/instacart/truetime/time/TrueTimeImpl.kt b/library/src/main/java/com/instacart/truetime/time/TrueTimeImpl.kt
new file mode 100644
index 00000000..1fda65b5
--- /dev/null
+++ b/library/src/main/java/com/instacart/truetime/time/TrueTimeImpl.kt
@@ -0,0 +1,163 @@
+package com.instacart.truetime.time
+
+import com.instacart.truetime.NoOpEventListener
+import com.instacart.truetime.TrueTimeEventListener
+import com.instacart.truetime.sntp.Sntp
+import com.instacart.truetime.sntp.SntpImpl
+import com.instacart.truetime.time.TrueTimeParameters.Builder
+import kotlinx.coroutines.*
+import kotlinx.coroutines.selects.select
+import java.net.Inet6Address
+import java.net.InetAddress
+import java.net.UnknownHostException
+import java.util.*
+
+class TrueTimeImpl(
+ private val params: TrueTimeParameters = Builder().buildParams(),
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ private val listener: TrueTimeEventListener = NoOpEventListener,
+) : TrueTime {
+
+ private val sntp: Sntp = SntpImpl()
+ private val timeKeeper = TimeKeeper(sntp, listener)
+ private val scope = CoroutineScope(SupervisorJob() + dispatcher +
+ CoroutineExceptionHandler { _, throwable -> listener.syncDispatcherException(throwable) })
+
+ override fun sync(): Job {
+ return scope.launch(CoroutineName("TrueTime-Syncer")) {
+ while (true) {
+ try {
+ initialize(params)
+ } catch (e: Exception) {
+ listener.initializeFailed(e)
+ }
+
+ listener.nextInitializeIn(delayInMillis = params.syncIntervalInMillis)
+ delay(params.syncIntervalInMillis)
+ }
+ }
+ }
+
+ override fun hasTheTime(): Boolean = timeKeeper.hasTheTime()
+
+ override fun now(): Date {
+ return if (params.returnSafelyWhenUninitialized) nowSafely() else nowTrueOnly()
+ }
+
+ override fun nowSafely(): Date {
+ return if (timeKeeper.hasTheTime()) {
+ nowTrueOnly()
+ } else {
+ listener.returningDeviceTime()
+ Date()
+ }
+ }
+
+ override fun nowTrueOnly(): Date {
+ if (!hasTheTime()) throw IllegalStateException("TrueTime was not initialized successfully yet")
+ return timeKeeper.now()
+ }
+
+ //region private helpers
+
+ /**
+ * Initialize TrueTime with an ntp pool server address
+ */
+ private suspend fun initialize(params: TrueTimeParameters): LongArray {
+ listener.initialize(params)
+
+ // resolve NTP pool -> single IPs
+ val resolvedIPs = resolveNtpHostToIPs(params.ntpHostPool.first())
+
+ val ntpResult: LongArray = if (this.params.strictNtpMode) {
+ // for each IP resolved
+ resolvedIPs.map { ipHost ->
+ // 5 times against each IP
+ (1..5)
+ .map { requestTime(params, ipHost) }
+ // collect the 5 results to list
+ .toList()
+ // filter least round trip delay to get single Result
+ .filterLeastRoundTripDelay()
+ }
+ // collect 5 of the results made so far to any of the IPs
+ .take(5)
+ // filter median clock offset to get single Result
+ .filterMedianClockOffset()
+ } else {
+ coroutineScope {
+ select {
+ resolvedIPs.forEach { ipHost ->
+ async {
+ requestTime(params, ipHost)
+ }.onAwait { it }
+ }
+ }.also { coroutineContext.cancelChildren() }
+ }
+ }
+
+ listener.initializeSuccess(ntpResult)
+
+ timeKeeper.save(ntpResult = ntpResult)
+
+ return ntpResult
+ }
+
+ /**
+ * resolve ntp host pool address to single IPs
+ */
+ @Throws(UnknownHostException::class)
+ private fun resolveNtpHostToIPs(ntpHostAddress: String): List {
+ val ipList: List = InetAddress
+ .getAllByName(ntpHostAddress)
+ .toList()
+ listener.resolvedNtpHostToIPs(ntpHostAddress, ipList)
+
+ return ipList.filter {
+ if (params.filterIpv6Addresses) it !is Inet6Address else true
+ }
+ }
+
+ private fun requestTime(
+ with: TrueTimeParameters,
+ ipHostAddress: InetAddress,
+ ): LongArray {
+ // retrying up to (default 50) times if necessary
+ repeat(with.retryCountAgainstSingleIp - 1) {
+ try {
+ // request Time
+ return sntpRequest(with, ipHostAddress)
+ } catch (e: Exception) {
+ listener.sntpRequestFailed(e)
+ }
+ }
+
+ // last attempt
+ listener.lastSntpRequestAttempt(ipHostAddress)
+ return sntpRequest(with, ipHostAddress)
+ }
+
+ private fun sntpRequest(
+ with: TrueTimeParameters,
+ ipHostAddress: InetAddress,
+ ): LongArray = sntp.requestTime(
+ ntpHostAddress = ipHostAddress,
+ rootDelayMax = with.rootDelayMax,
+ rootDispersionMax = with.rootDispersionMax,
+ serverResponseDelayMax = with.serverResponseDelayMaxInMillis,
+ timeoutInMillis = with.connectionTimeoutInMillis,
+ listener = listener,
+ )
+
+ private fun List.filterLeastRoundTripDelay(): LongArray {
+ return minByOrNull { sntp.roundTripDelay(it) }
+ ?: throw IllegalStateException("Could not find any results from requestingTime")
+ }
+
+ private fun List.filterMedianClockOffset(): LongArray {
+ val sortedList = this.sortedBy { sntp.clockOffset(it) }
+ return sortedList[sortedList.size / 2]
+ }
+
+ //endregion
+}
diff --git a/library/src/main/java/com/instacart/truetime/time/TrueTimeParameters.kt b/library/src/main/java/com/instacart/truetime/time/TrueTimeParameters.kt
new file mode 100644
index 00000000..5ad4efd4
--- /dev/null
+++ b/library/src/main/java/com/instacart/truetime/time/TrueTimeParameters.kt
@@ -0,0 +1,128 @@
+package com.instacart.truetime.time
+
+class TrueTimeParameters private constructor(
+ val connectionTimeoutInMillis: Int,
+ val ntpHostPool: ArrayList,
+ val retryCountAgainstSingleIp: Int,
+ val rootDelayMax: Float,
+ val rootDispersionMax: Float,
+ val serverResponseDelayMaxInMillis: Int,
+ val syncIntervalInMillis: Long,
+ val returnSafelyWhenUninitialized: Boolean,
+ val filterIpv6Addresses: Boolean,
+ val strictNtpMode: Boolean,
+) {
+
+ class Builder {
+
+ private var connectionTimeoutInMillis: Int = 30_000
+
+ fun connectionTimeoutInMillis(value: Int): Builder {
+ connectionTimeoutInMillis = value
+ return this
+ }
+
+ private var ntpHostPool: ArrayList = arrayListOf("time.google.com")
+
+ // TODO: Utilize all ntp pool addresses from `TrueTimeParameters.ntpHostPool`
+ // currently only using the first ntp host
+ // in the future we want to leverage all the time providers
+ fun ntpHostPool(value: ArrayList): Builder {
+ ntpHostPool = value
+ return this
+ }
+
+ private var retryCountAgainstSingleIp: Int = 50
+
+ fun retryCountAgainstSingleIp(value: Int): Builder {
+ retryCountAgainstSingleIp = value
+ return this
+ }
+
+ private var rootDelayMax: Float = 100f
+
+ fun rootDelayMax(value: Float): Builder {
+ rootDelayMax = value
+ return this
+ }
+
+ private var rootDispersionMax: Float = 100f
+
+ fun rootDispersionMax(value: Float): Builder {
+ rootDispersionMax = value
+ return this
+ }
+
+ private var serverResponseDelayMaxInMillis: Int = 750
+
+ fun serverResponseDelayMaxInMillis(value: Int): Builder {
+ serverResponseDelayMaxInMillis = value
+ return this
+ }
+
+ // re-sync every 1 hour by default
+ private var syncIntervalInMillis: Long = 3600_000
+
+ fun syncIntervalInMillis(value: Long): Builder {
+ syncIntervalInMillis = value
+ return this
+ }
+
+ /**
+ * if set to [true] it will default to [TrueTime.nowSafely]
+ * else will return [TrueTime.nowTrueOnly]
+ */
+ private var returnSafelyWhenUninitialized: Boolean = true
+
+ fun returnSafelyWhenUninitialized(value: Boolean): Builder {
+ returnSafelyWhenUninitialized = value
+ return this
+ }
+
+ /**
+ * Certain NTP hosts like time.google.com return IPV6 addresses.
+ * This can cause problems (atleast in emulators) so filter by default.
+ *
+ * In practice, on real devices [InetAddress.getAllByName] tends to return
+ * only IPV4 addresses.
+ */
+ private var filterIpv6Addresses: Boolean = true
+
+ fun filterIpv6Addresses(value: Boolean): Builder {
+ filterIpv6Addresses = value
+ return this
+ }
+
+ /**
+ * The NTP spec requires a pretty strict set of SNTP call sequence to be made
+ * we resolve the ntp pool to single IPs (typically around 4-5 IP addresses)
+ * we now make 5 calls to each of these IPs (~ 5 * 4-5 calls)
+ * and if a call fails we repeat it at least [retryCountAgainstSingleIp] times.
+ *
+ * if [strictNtpMode] is false, we ignore all of the above and return as soon as we get
+ * at least one successful SNTP call. This is what many other common libraries do.
+ */
+ private var strictNtpMode: Boolean = true
+
+ fun strictNtpMode(value: Boolean): Builder {
+ strictNtpMode = value
+ return this
+ }
+
+ // TODO: Introduce a Cache provider
+ // val cacheProvider: TrueTimeCacheProvider? = null,
+
+ fun buildParams() = TrueTimeParameters(
+ connectionTimeoutInMillis,
+ ntpHostPool,
+ retryCountAgainstSingleIp,
+ rootDelayMax,
+ rootDispersionMax,
+ serverResponseDelayMaxInMillis,
+ syncIntervalInMillis,
+ returnSafelyWhenUninitialized,
+ filterIpv6Addresses,
+ strictNtpMode,
+ )
+ }
+}
diff --git a/library/src/test/java/com/instacart/truetime/time/TrueTimeImplTest.kt b/library/src/test/java/com/instacart/truetime/time/TrueTimeImplTest.kt
new file mode 100644
index 00000000..a5151f8b
--- /dev/null
+++ b/library/src/test/java/com/instacart/truetime/time/TrueTimeImplTest.kt
@@ -0,0 +1,88 @@
+package com.instacart.truetime.time
+
+class TrueTimeImplTest {
+
+ // ntp single ip : 216.239.35.4
+ /**
+ *
+
+ 0 = 1606352796468
+ 1 = 1606352795894
+ 2 = 1606352795894
+ 3 = 1606352796546
+ 4 = 0
+ 5 = 10
+ 6 = 1
+ 7 = 704540455
+
+
+ 0 = 1606352813766
+ 1 = 1606352813177
+ 2 = 1606352813177
+ 3 = 1606352813805
+ 4 = 0
+ 5 = 6
+ 6 = 1
+ 7 = 704557715
+
+ 0 = 1606352816644
+ 1 = 1606352816116
+ 2 = 1606352816116
+ 3 = 1606352816832
+ 4 = 0
+ 5 = 6
+ 6 = 1
+ 7 = 704560742
+
+ 0 = 1606352818690
+ 1 = 1606352818116
+ 2 = 1606352818116
+ 3 = 1606352818761
+ 4 = 0
+ 5 = 11
+ 6 = 1
+ 7 = 704562670
+
+ sorted by clock offset
+
+ 0 = 1606352816644
+ 1 = 1606352816116
+ 2 = 1606352816116
+ 3 = 1606352816832
+ 4 = 0
+ 5 = 6
+ 6 = 1
+ 7 = 704560742
+
+ 0 = 1606352796468
+ 1 = 1606352795894
+ 2 = 1606352795894
+ 3 = 1606352796546
+ 4 = 0
+ 5 = 10
+ 6 = 1
+ 7 = 704540455
+
+
+ 0 = 1606352818690
+ 1 = 1606352818116
+ 2 = 1606352818116
+ 3 = 1606352818761
+ 4 = 0
+ 5 = 11
+ 6 = 1
+ 7 = 704562670
+
+ 0 = 1606352813766
+ 1 = 1606352813177
+ 2 = 1606352813177
+ 3 = 1606352813805
+ 4 = 0
+ 5 = 6
+ 6 = 1
+ 7 = 704557715
+
+
+
+ */
+}
diff --git a/settings.gradle b/settings.gradle
index ff7fc959..33069973 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app', ':library', ':library-extension-rx'
+include ':app', ':library'