package com.instacart.library.truetime; import android.content.Context; import io.reactivex.BackpressureStrategy; import io.reactivex.Flowable; import io.reactivex.FlowableEmitter; import io.reactivex.FlowableOnSubscribe; import io.reactivex.FlowableTransformer; import io.reactivex.Single; import io.reactivex.annotations.NonNull; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import io.reactivex.functions.Predicate; import io.reactivex.schedulers.Schedulers; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import org.reactivestreams.Publisher; public class TrueTimeRx extends TrueTime { private static final TrueTimeRx RX_INSTANCE = new TrueTimeRx(); private static final String TAG = TrueTimeRx.class.getSimpleName(); private int _retryCount = 50; public static TrueTimeRx build() { return RX_INSTANCE; } public TrueTimeRx withSharedPreferencesCache(Context context) { super.withSharedPreferencesCache(context); return this; } /** * Provide your own cache interface to cache the true time information. * @param cacheInterface the customized cache interface to save the true time data. */ public TrueTimeRx withCustomizedCache(CacheInterface cacheInterface) { super.withCustomizedCache(cacheInterface); return this; } public TrueTimeRx withConnectionTimeout(int timeout) { super.withConnectionTimeout(timeout); return this; } public TrueTimeRx withRootDelayMax(float rootDelay) { super.withRootDelayMax(rootDelay); return this; } public TrueTimeRx withRootDispersionMax(float rootDispersion) { super.withRootDispersionMax(rootDispersion); return this; } public TrueTimeRx withServerResponseDelayMax(int serverResponseDelayInMillis) { super.withServerResponseDelayMax(serverResponseDelayInMillis); return this; } public TrueTimeRx withLoggingEnabled(boolean isLoggingEnabled) { super.withLoggingEnabled(isLoggingEnabled); return this; } public TrueTimeRx withRetryCount(int retryCount) { _retryCount = retryCount; return this; } /** * Initialize TrueTime * See {@link #initializeNtp(String)} for details on working * * @return accurate NTP Date */ public Single<Date> initializeRx(String ntpPoolAddress) { return isInitialized() ? Single.just(now()) : initializeNtp(ntpPoolAddress).map(new Function<long[], Date>() { @Override public Date apply(long[] longs) throws Exception { return now(); } }); } /** * Initialize TrueTime * A single NTP pool server is provided. * Using DNS we resolve that to multiple IP hosts (See {@link #initializeNtp(List)} for manually resolved IPs) * * Use this instead of {@link #initializeRx(String)} if you wish to also get additional info for * instrumentation/tracking actual NTP response data * * @param ntpPool NTP pool server e.g. time.apple.com, 0.us.pool.ntp.org * @return Observable of detailed long[] containing most important parts of the actual NTP response * See RESPONSE_INDEX_ prefixes in {@link SntpClient} for details */ public Single<long[]> initializeNtp(String ntpPool) { return Flowable .just(ntpPool) .compose(resolveNtpPoolToIpAddresses()) .compose(performNtpAlgorithm()) .firstOrError(); } /** * Initialize TrueTime * Use this if you want to resolve the NTP Pool address to individual IPs yourself * * See https://github.com/instacart/truetime-android/issues/42 * to understand why you may want to do something like this. * * @param resolvedNtpAddresses list of resolved IP addresses for an NTP * @return Observable of detailed long[] containing most important parts of the actual NTP response * See RESPONSE_INDEX_ prefixes in {@link SntpClient} for details */ public Single<long[]> initializeNtp(List<InetAddress> resolvedNtpAddresses) { return Flowable.fromIterable(resolvedNtpAddresses) .compose(performNtpAlgorithm()) .firstOrError(); } /** * Transformer that takes in a pool of NTP addresses * Against each IP host we issue a UDP call and retrieve the best response using the NTP algorithm */ private FlowableTransformer<InetAddress, long[]> performNtpAlgorithm() { return new FlowableTransformer<InetAddress, long[]>() { @Override public Flowable<long[]> apply(Flowable<InetAddress> inetAddressObservable) { return inetAddressObservable .map(new Function<InetAddress, String>() { @Override public String apply(InetAddress inetAddress) { return inetAddress.getHostAddress(); } }) .flatMap(bestResponseAgainstSingleIp(5)) // get best response from querying the ip 5 times .take(5) // take 5 of the best results .toList() .toFlowable() .filter(new Predicate<List<long[]>>() { @Override public boolean test(List<long[]> longs) throws Exception { return longs.size() > 0; } }) .map(filterMedianResponse()) .doOnNext(new Consumer<long[]>() { @Override public void accept(long[] ntpResponse) { cacheTrueTimeInfo(ntpResponse); saveTrueTimeInfoToDisk(); } }); } }; } private FlowableTransformer<String, InetAddress> resolveNtpPoolToIpAddresses() { return new FlowableTransformer<String, InetAddress>() { @Override public Publisher<InetAddress> apply(Flowable<String> ntpPoolFlowable) { return ntpPoolFlowable .observeOn(Schedulers.io()) .flatMap(new Function<String, Flowable<InetAddress>>() { @Override public Flowable<InetAddress> apply(String ntpPoolAddress) { try { TrueLog.d(TAG, "---- resolving ntpHost : " + ntpPoolAddress); return Flowable.fromArray(InetAddress.getAllByName(ntpPoolAddress)); } catch (UnknownHostException e) { return Flowable.error(e); } } }); } }; } private Function<String, Flowable<long[]>> bestResponseAgainstSingleIp(final int repeatCount) { return new Function<String, Flowable<long[]>>() { @Override public Flowable<long[]> apply(String singleIp) { return Flowable .just(singleIp) .repeat(repeatCount) .flatMap(new Function<String, Flowable<long[]>>() { @Override public Flowable<long[]> apply(final String singleIpHostAddress) { return Flowable.create(new FlowableOnSubscribe<long[]>() { @Override public void subscribe(@NonNull FlowableEmitter<long[]> o) throws Exception { TrueLog.d(TAG, "---- requestTime from: " + singleIpHostAddress); try { o.onNext(requestTime(singleIpHostAddress)); o.onComplete(); } catch (IOException e) { o.tryOnError(e); } } }, BackpressureStrategy.BUFFER) .subscribeOn(Schedulers.io()) .doOnError(new Consumer<Throwable>() { @Override public void accept(Throwable throwable) { TrueLog.e(TAG, "---- Error requesting time", throwable); } }) .retry(_retryCount); } }) .toList() .toFlowable() .map(filterLeastRoundTripDelay()); // pick best response for each ip } }; } private Function<List<long[]>, long[]> filterLeastRoundTripDelay() { return new Function<List<long[]>, long[]>() { @Override public long[] apply(List<long[]> responseTimeList) { Collections.sort(responseTimeList, new Comparator<long[]>() { @Override public int compare(long[] lhsParam, long[] rhsLongParam) { long lhs = SntpClient.getRoundTripDelay(lhsParam); long rhs = SntpClient.getRoundTripDelay(rhsLongParam); return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); } }); TrueLog.d(TAG, "---- filterLeastRoundTrip: " + responseTimeList); return responseTimeList.get(0); } }; } private Function<List<long[]>, long[]> filterMedianResponse() { return new Function<List<long[]>, long[]>() { @Override public long[] apply(List<long[]> bestResponses) { Collections.sort(bestResponses, new Comparator<long[]>() { @Override public int compare(long[] lhsParam, long[] rhsParam) { long lhs = SntpClient.getClockOffset(lhsParam); long rhs = SntpClient.getClockOffset(rhsParam); return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); } }); TrueLog.d(TAG, "---- bestResponse: " + Arrays.toString(bestResponses.get(bestResponses.size() / 2))); return bestResponses.get(bestResponses.size() / 2); } }; } }