diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5058ff9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# space indentation for Kotlin +[*.{kt,kts}] +indent_style = space +indent_size = 4 +continuation_indent_size = 4 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 17653ac..9365ddf 100644 --- a/build.gradle +++ b/build.gradle @@ -2,14 +2,13 @@ ext { def versionMajor = 0 def versionMinor = 1 - def versionPatch = 1 + def versionPatch = 2 def versionCode = versionPatch + versionMinor * 100 + versionMajor * 10000 androidConfig = [ - compileSdkVersion : 25, - buildToolsVersion : "25.0.3", + compileSdkVersion : 28, minSdkVersion : 16, - targetSdkVersion : 25, + targetSdkVersion : 28, versionCode : versionCode, versionName : "${versionMajor}.${versionMinor}.${versionPatch}", @@ -21,11 +20,11 @@ buildscript { ext { depPaths = { def versions = [ - gradlePlugin : '2.3.3', - kotlin : '1.1.3', - supportLib : '25.4.0', - rxJava : '2.1.1', - rxAndroid : '2.0.1', + gradlePlugin : '3.2.1', + kotlin : '1.3.11', + supportLib : '28.0.0', + rxJava : '2.2.5', + rxAndroid : '2.1.0', androidJob : '1.1.11', play : '11.0.2', @@ -40,7 +39,7 @@ buildscript { gradlePlugin : "com.android.tools.build:gradle:$versions.gradlePlugin", kotlinPlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin", - kotlinStd : "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin", + kotlinStd : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin", kotlinReflect : "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin", supportAppCompat: "com.android.support:appcompat-v7:$versions.supportLib", supportDesign : "com.android.support:design:$versions.supportLib", @@ -60,10 +59,8 @@ buildscript { } repositories { + google() jcenter() - maven { - url "https://maven.google.com" - } } dependencies { @@ -77,11 +74,8 @@ buildscript { allprojects { repositories { + google() jcenter() - mavenCentral() - maven { - url "https://maven.google.com" - } } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372ae..1948b90 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 60adaee..a4fc03d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jun 30 15:39:57 BRT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index 9d82f78..cccdd3d --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,20 +6,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282..f955316 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/sample/build.gradle b/sample/build.gradle index d735c3a..62aec09 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -4,7 +4,6 @@ apply plugin: 'kotlin-android-extensions' android { compileSdkVersion androidConfig.compileSdkVersion - buildToolsVersion androidConfig.buildToolsVersion defaultConfig { applicationId "io.tempo.sample" @@ -24,18 +23,18 @@ android { } dependencies { - compile project(':tempo') - compile project(':tempo-android-job-scheduler') - compile project(':tempo-android-gps-time-source') - compile depPaths.kotlinStd - compile depPaths.supportAppCompat - compile depPaths.supportDesign - compile depPaths.supportCardView - compile depPaths.rxJava - compile depPaths.rxAndroid + implementation project(':tempo') + implementation project(':tempo-android-job-scheduler') + implementation project(':tempo-android-gps-time-source') + implementation depPaths.kotlinStd + implementation depPaths.supportAppCompat + implementation depPaths.supportDesign + implementation depPaths.supportCardView + implementation depPaths.rxJava + implementation depPaths.rxAndroid - testCompile depPaths.junit - androidTestCompile(depPaths.espressoCore, { + testImplementation depPaths.junit + androidTestImplementation(depPaths.espressoCore, { exclude group: 'com.android.support', module: 'support-annotations' }) } diff --git a/sample/src/main/java/io/tempo/sample/MainActivity.kt b/sample/src/main/java/io/tempo/sample/MainActivity.kt index 8fc3694..e02d668 100644 --- a/sample/src/main/java/io/tempo/sample/MainActivity.kt +++ b/sample/src/main/java/io/tempo/sample/MainActivity.kt @@ -36,7 +36,8 @@ import io.tempo.TempoEvent import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.item_tempo_event.view.* import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { @@ -105,16 +106,16 @@ class EventsRvAdapter : RecyclerView.Adapter() { } } - val events = mutableListOf() + private val events = mutableListOf() fun addEvent(event: TempoEvent) { events.add(event) notifyItemInserted(events.size - 1) } - override fun onBindViewHolder(holder: VH?, position: Int) { + override fun onBindViewHolder(holder: VH, position: Int) { events.getOrNull(position)?.let { event -> - holder?.bind(event) + holder.bind(event) } } diff --git a/tempo-android-gps-time-source/build.gradle b/tempo-android-gps-time-source/build.gradle index bcc95c8..124d30d 100644 --- a/tempo-android-gps-time-source/build.gradle +++ b/tempo-android-gps-time-source/build.gradle @@ -3,7 +3,6 @@ apply plugin: 'kotlin-android' android { compileSdkVersion androidConfig.compileSdkVersion - buildToolsVersion androidConfig.buildToolsVersion defaultConfig { minSdkVersion androidConfig.minSdkVersion @@ -22,11 +21,15 @@ android { } dependencies { - compile project(':tempo') + implementation project(':tempo') + implementation depPaths.kotlinStd + implementation depPaths.rxJava + implementation depPaths.rxAndroid + implementation depPaths.supportAppCompat - testCompile depPaths.junit - androidTestCompile depPaths.hamkrest - androidTestCompile(depPaths.espressoCore, { + testImplementation depPaths.junit + androidTestImplementation depPaths.hamkrest + androidTestImplementation(depPaths.espressoCore, { exclude group: 'com.android.support', module: 'support-annotations' }) } diff --git a/tempo-android-gps-time-source/src/main/java/io/tempo/time_sources/AndroidGPSTimeSource.kt b/tempo-android-gps-time-source/src/main/java/io/tempo/time_sources/AndroidGPSTimeSource.kt index 3be3851..27055a3 100644 --- a/tempo-android-gps-time-source/src/main/java/io/tempo/time_sources/AndroidGPSTimeSource.kt +++ b/tempo-android-gps-time-source/src/main/java/io/tempo/time_sources/AndroidGPSTimeSource.kt @@ -33,9 +33,10 @@ import io.tempo.TimeSource import io.tempo.TimeSourceConfig class AndroidGPSTimeSource( - val context: Context, - val id: String = "tempo-default-android-gps", - val priority: Int = 5) : TimeSource { + val context: Context, + val id: String = "tempo-default-android-gps", + private val priority: Int = 5 +) : TimeSource { class PermissionNotSet : RuntimeException("We don't have permission to access the GPS.") @@ -45,36 +46,46 @@ class AndroidGPSTimeSource( data class GPSInfo(val provider: String, val time: Long) return Flowable - .create({ emitter -> - val listener = object : LocationListener { - override fun onLocationChanged(location: Location?) { - location?.let { + .create({ emitter -> + val listener = object : LocationListener { + override fun onLocationChanged(location: Location?) { + location?.let { + if (!emitter.isCancelled) { emitter.onNext(GPSInfo(location.provider, location.time)) } } - - override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} - override fun onProviderEnabled(provider: String?) {} - override fun onProviderDisabled(provider: String?) {} } - val hasPermission = ContextCompat.checkSelfPermission(context, - Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED - if (hasPermission) { - val mgr = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - emitter.setCancellable { - mgr.removeUpdates(listener) - } - mgr.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0L, 0.0f, listener, null) - } else { - throw PermissionNotSet() + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + override fun onProviderEnabled(provider: String?) {} + override fun onProviderDisabled(provider: String?) {} + } + + val hasPermission = ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + if (hasPermission) { + val mgr = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + emitter.setCancellable { + mgr.removeUpdates(listener) } - }, BackpressureStrategy.BUFFER) - .subscribeOn(AndroidSchedulers.mainThread()) - .observeOn(Schedulers.io()) - .filter { it.provider == LocationManager.GPS_PROVIDER } - .map { it.time } - .take(1) - .singleOrError() + mgr.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 0L, + 0.0f, + listener, + null + ) + } else { + throw PermissionNotSet() + } + }, BackpressureStrategy.BUFFER) + .subscribeOn(AndroidSchedulers.mainThread()) + .observeOn(Schedulers.io()) + .filter { it.provider == LocationManager.GPS_PROVIDER } + .map { it.time } + .take(1) + .singleOrError() } } \ No newline at end of file diff --git a/tempo-android-job-scheduler/build.gradle b/tempo-android-job-scheduler/build.gradle index 80ec1c7..c375f2c 100644 --- a/tempo-android-job-scheduler/build.gradle +++ b/tempo-android-job-scheduler/build.gradle @@ -3,7 +3,6 @@ apply plugin: 'kotlin-android' android { compileSdkVersion androidConfig.compileSdkVersion - buildToolsVersion androidConfig.buildToolsVersion defaultConfig { minSdkVersion androidConfig.minSdkVersion @@ -22,7 +21,10 @@ android { } dependencies { - compile project(':tempo') - compile depPaths.androidJob - compile depPaths.playGCM + implementation project(':tempo') + implementation depPaths.kotlinStd + implementation depPaths.rxJava + implementation depPaths.rxAndroid + implementation depPaths.androidJob + implementation depPaths.playGCM } diff --git a/tempo-android-job-scheduler/src/main/java/io/tempo/schedulers/AndroidJobScheduler.kt b/tempo-android-job-scheduler/src/main/java/io/tempo/schedulers/AndroidJobScheduler.kt index 8d38981..eaf3c46 100644 --- a/tempo-android-job-scheduler/src/main/java/io/tempo/schedulers/AndroidJobScheduler.kt +++ b/tempo-android-job-scheduler/src/main/java/io/tempo/schedulers/AndroidJobScheduler.kt @@ -26,26 +26,29 @@ import io.tempo.Tempo import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -class AndroidJobScheduler(val application: Application, - val periodicIntervalMinutes: Long = 60L) : Scheduler { +class AndroidJobScheduler( + val application: Application, + val periodicIntervalMinutes: Long = 60L +) : Scheduler { companion object { - private val JOB_TAG = "tempo-android-job-scheduler" - private val PKEY_INTERVAL_MIN = "tempo-interval" + private const val JOB_TAG = "tempo-android-job-scheduler" + private const val PKEY_INTERVAL_MIN = "tempo-interval" private fun schedule(intervalMinutes: Long, updateCurrent: Boolean) { - val startMinutes = intervalMinutes val endMinutes = intervalMinutes * 2 val toMs = { minutes: Long -> minutes * 60L * 1000L } JobRequest.Builder(JOB_TAG) - .setExecutionWindow(toMs(startMinutes), toMs(endMinutes)) - .setPersisted(true) - .setUpdateCurrent(updateCurrent) - .setExtras(PersistableBundleCompat().apply { + .setExecutionWindow(toMs(intervalMinutes), toMs(endMinutes)) + .setPersisted(true) + .setUpdateCurrent(updateCurrent) + .setExtras( + PersistableBundleCompat().apply { putLong(PKEY_INTERVAL_MIN, intervalMinutes) - }) - .build() - .schedule() + } + ) + .build() + .schedule() } } diff --git a/tempo/build.gradle b/tempo/build.gradle index 1a01113..848d999 100644 --- a/tempo/build.gradle +++ b/tempo/build.gradle @@ -3,7 +3,6 @@ apply plugin: 'kotlin-android' android { compileSdkVersion androidConfig.compileSdkVersion - buildToolsVersion androidConfig.buildToolsVersion defaultConfig { minSdkVersion androidConfig.minSdkVersion @@ -22,21 +21,21 @@ android { } dependencies { - compile depPaths.kotlinStd - compile depPaths.rxJava - compile depPaths.rxAndroid - compile depPaths.supportAppCompat + implementation depPaths.kotlinStd + implementation depPaths.rxJava + implementation depPaths.rxAndroid + implementation depPaths.supportAppCompat - testCompile depPaths.junit - testCompile depPaths.hamkrest - testCompile depPaths.kotlinReflect - testCompile depPaths.mockitoKotlin + testImplementation depPaths.junit + testImplementation depPaths.hamkrest + testImplementation depPaths.kotlinReflect + testImplementation depPaths.mockitoKotlin - androidTestCompile depPaths.kotlinReflect - androidTestCompile depPaths.hamkrest - androidTestCompile depPaths.mockitoKotlin - androidTestCompile depPaths.mockitoAndroid - androidTestCompile(depPaths.espressoCore, { + androidTestImplementation depPaths.kotlinReflect + androidTestImplementation depPaths.hamkrest + androidTestImplementation depPaths.mockitoKotlin + androidTestImplementation depPaths.mockitoAndroid + androidTestImplementation(depPaths.espressoCore, { exclude group: 'com.android.support', module: 'support-annotations' }) } diff --git a/tempo/src/main/java/io/tempo/config.kt b/tempo/src/main/java/io/tempo/config.kt index fe0d00b..73133f0 100644 --- a/tempo/src/main/java/io/tempo/config.kt +++ b/tempo/src/main/java/io/tempo/config.kt @@ -19,7 +19,7 @@ package io.tempo import io.tempo.SyncRetryStrategy.ExpBackoff sealed class SyncRetryStrategy { - class None : SyncRetryStrategy() + object None : SyncRetryStrategy() /** * Will retry with the following intervals: [timerMs], [intervalMs], [intervalMs], [intervalMs], ... diff --git a/tempo/src/main/java/io/tempo/internal/AndroidSntpClient.kt b/tempo/src/main/java/io/tempo/internal/AndroidSntpClient.kt index b44aa55..36fa9ae 100644 --- a/tempo/src/main/java/io/tempo/internal/AndroidSntpClient.kt +++ b/tempo/src/main/java/io/tempo/internal/AndroidSntpClient.kt @@ -19,15 +19,14 @@ package io.tempo.internal import android.os.SystemClock import io.tempo.internal.SntpClient.Result -import java.lang.Exception import java.net.DatagramPacket import java.net.DatagramSocket import java.net.Inet4Address import java.net.InetAddress -import java.util.* +import java.util.Arrays import kotlin.experimental.and -object AndroidSntpClient : SntpClient { +internal object AndroidSntpClient : SntpClient { class InetException(errorMsg: String) : RuntimeException(errorMsg) val NTP_PORT = 123 diff --git a/tempo/src/main/java/io/tempo/internal/SntpClient.kt b/tempo/src/main/java/io/tempo/internal/SntpClient.kt index 26b393f..1db0d67 100644 --- a/tempo/src/main/java/io/tempo/internal/SntpClient.kt +++ b/tempo/src/main/java/io/tempo/internal/SntpClient.kt @@ -18,7 +18,7 @@ package io.tempo.internal import java.net.InetAddress -interface SntpClient { +internal interface SntpClient { sealed class Result { /** * @property[ntpTimeMs] The resulting time in milliseconds at the time the request completed. diff --git a/tempo/src/main/java/io/tempo/internal/TempoInstance.kt b/tempo/src/main/java/io/tempo/internal/TempoInstance.kt index ebf50a4..5ee4fe0 100644 --- a/tempo/src/main/java/io/tempo/internal/TempoInstance.kt +++ b/tempo/src/main/java/io/tempo/internal/TempoInstance.kt @@ -16,6 +16,7 @@ package io.tempo.internal +import android.annotation.SuppressLint import io.reactivex.Flowable import io.reactivex.Single import io.reactivex.functions.BiFunction @@ -33,12 +34,12 @@ import io.tempo.TimeSourceWrapper import io.tempo.schedulers.NoOpScheduler import java.util.concurrent.TimeUnit -class TempoInstance( - val timeSources: List, - val config: TempoConfig, - private val storage: Storage, - private val deviceClocks: DeviceClocks, - private val scheduler: Scheduler) { +internal class TempoInstance( + val timeSources: List, + val config: TempoConfig, + private val storage: Storage, + private val deviceClocks: DeviceClocks, + private val scheduler: Scheduler) { val initialized get() = activeTimeWrapper() != null @@ -46,7 +47,7 @@ class TempoInstance( private val timeWrappers = mutableMapOf() private val eventsSubject = ReplayProcessor.createWithTime( - 1000, TimeUnit.MILLISECONDS, Schedulers.io()) + 1000, TimeUnit.MILLISECONDS, Schedulers.io()) init { require(timeSources.isNotEmpty()) { @@ -56,13 +57,7 @@ class TempoInstance( "Duplicate ids in 'timeSources' aren't allowed." } - Flowable.just(timeSources) - .doOnNext { eventsSubject.onNext(TempoEvent.Initializing()) } - .observeOn(Schedulers.io()) - .doOnNext { restoreCache() } - .doOnNext { setupScheduler() } - .flatMap { syncFlow() } - .subscribe({}, {}, {}) + fireUpFirstSyncFlow() } fun observeEvents(): Flowable = eventsSubject.onBackpressureLatest() @@ -73,71 +68,71 @@ class TempoInstance( fun syncFlow(): Flowable { return Flowable.fromIterable(timeSources) - .observeOn(Schedulers.io()) - .flatMap { source -> - requestTime(source) - .timeout(config.syncTimeoutMs, TimeUnit.MILLISECONDS) - .toFlowable() - .map { wrapper -> TempoEvent.TSSyncSuccess(wrapper) as TempoEvent } - .startWith(TempoEvent.TSSyncRequest(source) as TempoEvent) - .onErrorReturn { error -> - val defaultMsg = "Error requesting time to '${source.config().id}'" - TempoEvent.TSSyncFailure(source, error, error.message ?: defaultMsg) - } - } - .publish { flow -> - val endFlow = flow - .buffer(timeSources.size * 2) - .take(1) - .map { it.filter { it is TempoEvent.TSSyncSuccess }.isNotEmpty() } - .map { hasSuccess -> - val activeTw = activeTimeWrapper() - if (hasSuccess && activeTw != null) { - TempoEvent.SyncSuccess(activeTw) - } else { - TempoEvent.SyncFail() - } - } - val initializedFlow = Flowable.fromCallable { initialized } - .flatMap { - when (it) { - true -> Flowable.just(TempoEvent.Initialized()) - else -> Flowable.empty() - } - } - flow.mergeWith(endFlow).concatWith(initializedFlow) - } - .startWith(TempoEvent.SyncStart()) - .doOnNext { event -> - eventsSubject.onNext(event) - if (event is TempoEvent.TSSyncSuccess) { - val name = event.wrapper.timeSource.config().id - val cache = event.wrapper.cache - synchronized(timeWrappers) { - storage.putCache(cache) - timeWrappers[name] = event.wrapper - updateActiveTimeWrapper() + .observeOn(Schedulers.io()) + .flatMap { source -> + requestTime(source) + .timeout(config.syncTimeoutMs, TimeUnit.MILLISECONDS) + .toFlowable() + .map { wrapper -> TempoEvent.TSSyncSuccess(wrapper) as TempoEvent } + .startWith(TempoEvent.TSSyncRequest(source) as TempoEvent) + .onErrorReturn { error -> + val defaultMsg = "Error requesting time to '${source.config().id}'" + TempoEvent.TSSyncFailure(source, error, error.message ?: defaultMsg) + } + } + .publish { flow -> + val endFlow = flow + .buffer(timeSources.size * 2) + .take(1) + .map { it.any { it is TempoEvent.TSSyncSuccess } } + .map { hasSuccess -> + val activeTw = activeTimeWrapper() + if (hasSuccess && activeTw != null) { + TempoEvent.SyncSuccess(activeTw) + } else { + TempoEvent.SyncFail() } - eventsSubject.onNext(TempoEvent.CacheSaved(cache)) } + val initializedFlow = Flowable.fromCallable { initialized } + .flatMap { + when (it) { + true -> Flowable.just(TempoEvent.Initialized()) + else -> Flowable.empty() + } + } + flow.mergeWith(endFlow).concatWith(initializedFlow) + } + .startWith(TempoEvent.SyncStart()) + .doOnNext { event -> + eventsSubject.onNext(event) + if (event is TempoEvent.TSSyncSuccess) { + val name = event.wrapper.timeSource.config().id + val cache = event.wrapper.cache + synchronized(timeWrappers) { + storage.putCache(cache) + timeWrappers[name] = event.wrapper + updateActiveTimeWrapper() + } + eventsSubject.onNext(TempoEvent.CacheSaved(cache)) } - .repeatWhen { completed -> - val retryFlow = syncRetryStratFlow(config.syncRetryStrategy) - completed.zipWith(retryFlow, BiFunction { _: Any, _: Any -> initialized }) - .takeWhile { !it } - } + } + .repeatWhen { completed -> + val retryFlow = syncRetryStratFlow(config.syncRetryStrategy) + completed.zipWith(retryFlow, BiFunction { _: Any, _: Any -> initialized }) + .takeWhile { !it } + } } private fun requestTime(timeSource: TimeSource): Single = - timeSource.requestTime() - .map { reqTime -> - val cache = TimeSourceCache( - timeSourceId = timeSource.config().id, - estimatedBootTime = deviceClocks.estimatedBootTime(), - requestDeviceUptime = deviceClocks.uptime(), - requestTime = reqTime) - TimeSourceWrapper(timeSource, cache) - } + timeSource.requestTime() + .map { reqTime -> + val cache = TimeSourceCache( + timeSourceId = timeSource.config().id, + estimatedBootTime = deviceClocks.estimatedBootTime(), + requestDeviceUptime = deviceClocks.uptime(), + requestTime = reqTime) + TimeSourceWrapper(timeSource, cache) + } private fun updateActiveTimeWrapper() { val topPriority = timeWrappers.values.maxBy { it.timeSource.config().priority } @@ -152,16 +147,16 @@ class TempoInstance( } timeSources - .map { source -> source to storage.getCache(source.config().id) } - .filter { it.second?.let(::isCacheValid) ?: false } - .onEach { it.second?.let { eventsSubject.onNext(TempoEvent.CacheRestored(it)) } } - .map { it.first.config().id to TimeSourceWrapper(it.first, it.second!!) } - .also { - synchronized(timeWrappers) { - timeWrappers.putAll(it) - updateActiveTimeWrapper() - } + .map { source -> source to storage.getCache(source.config().id) } + .filter { it.second?.let(::isCacheValid) ?: false } + .onEach { it.second?.let { eventsSubject.onNext(TempoEvent.CacheRestored(it)) } } + .map { it.first.config().id to TimeSourceWrapper(it.first, it.second!!) } + .also { + synchronized(timeWrappers) { + timeWrappers.putAll(it) + updateActiveTimeWrapper() } + } } private fun setupScheduler() { @@ -172,14 +167,13 @@ class TempoInstance( eventsSubject.onNext(TempoEvent.SchedulerSetupComplete()) } catch (e: Exception) { eventsSubject.onNext(TempoEvent.SchedulerSetupFailure(e, - "Error while setting up scheduler.")) + "Error while setting up scheduler.")) } } else { eventsSubject.onNext(TempoEvent.SchedulerSetupSkip()) } } - private fun syncRetryStratFlow(strat: SyncRetryStrategy): Flowable { return when (strat) { is SyncRetryStrategy.None -> Flowable.empty() @@ -193,11 +187,22 @@ class TempoInstance( val interval = { idx: Int -> val backoff = Math.pow(2.0, idx.toDouble()) * strat.multiplier val timer = (strat.timerMs + backoff).toLong() - .coerceAtMost(strat.maxIntervalMs) + .coerceAtMost(strat.maxIntervalMs) Flowable.timer(timer, TimeUnit.MILLISECONDS) } tries.concatMap { idx -> interval(idx) } } } } + + @SuppressLint("CheckResult") + private fun fireUpFirstSyncFlow() { + Flowable.just(timeSources) + .doOnNext { eventsSubject.onNext(TempoEvent.Initializing()) } + .observeOn(Schedulers.io()) + .doOnNext { restoreCache() } + .doOnNext { setupScheduler() } + .flatMap { syncFlow() } + .subscribe({}, {}, {}) + } } \ No newline at end of file diff --git a/tempo/src/main/java/io/tempo/storage/SharedPrefStorage.kt b/tempo/src/main/java/io/tempo/storage/SharedPrefStorage.kt index 8901e34..925700f 100644 --- a/tempo/src/main/java/io/tempo/storage/SharedPrefStorage.kt +++ b/tempo/src/main/java/io/tempo/storage/SharedPrefStorage.kt @@ -21,10 +21,9 @@ import android.content.Context import io.tempo.Storage import io.tempo.TimeSourceCache - -class SharedPrefStorage(val context: Context) : Storage { +class SharedPrefStorage(private val context: Context) : Storage { companion object { - private val FILE = "tempo-storage" + private const val FILE = "tempo-storage" private fun keyCacheEstBootTime(name: String) = "$name-est-boot-time" private fun keyCacheReqUptime(name: String) = "$name-req-uptime" private fun keyCacheReqTime(name: String) = "$name-req-time" @@ -32,7 +31,7 @@ class SharedPrefStorage(val context: Context) : Storage { private val accessLock = Any() - @SuppressLint("CommitPrefEdits") + @SuppressLint("CommitPrefEdits", "ApplySharedPref") override fun putCache(cache: TimeSourceCache) { synchronized(accessLock) { getSharedPref().edit().apply { @@ -52,15 +51,15 @@ class SharedPrefStorage(val context: Context) : Storage { val reqTime = getSharedPref().getLong(keyCacheReqTime(timeSourceId), -1L) return when (reqUptime > 0L && reqTime > 0L && estBootTime > 0L) { true -> TimeSourceCache(timeSourceId, - estimatedBootTime = estBootTime, - requestDeviceUptime = reqUptime, - requestTime = reqTime) + estimatedBootTime = estBootTime, + requestDeviceUptime = reqUptime, + requestTime = reqTime) else -> null } } } - @SuppressLint("CommitPrefEdits") + @SuppressLint("CommitPrefEdits", "ApplySharedPref") override fun clearCaches() { synchronized(accessLock) { getSharedPref().edit().clear().commit() diff --git a/tempo/src/main/java/io/tempo/time_sources/SlackSntpTimeSource.kt b/tempo/src/main/java/io/tempo/time_sources/SlackSntpTimeSource.kt index b77e89b..20b9f5e 100644 --- a/tempo/src/main/java/io/tempo/time_sources/SlackSntpTimeSource.kt +++ b/tempo/src/main/java/io/tempo/time_sources/SlackSntpTimeSource.kt @@ -23,6 +23,7 @@ import io.tempo.TimeSourceConfig import io.tempo.internal.AndroidSntpClient import io.tempo.internal.SntpClient import io.tempo.internal.SntpClient.Result +import java.net.InetAddress /** * A [TimeSource] implementation using a more forgiving SNTP algorithm. It queries the [ntpPool] @@ -37,72 +38,97 @@ import io.tempo.internal.SntpClient.Result * @param[timeoutMs] The maximum time allowed per each query, in milliseconds. */ class SlackSntpTimeSource(val id: String = "default-slack-sntp", - val priority: Int = 10, - val ntpPool: String = "time.google.com", - val maxRoundTripMs: Int = 1_000, - val timeoutMs: Int = 10_000) : TimeSource { + private val priority: Int = 10, + private val ntpPool: String = "time.google.com", + private val maxRoundTripMs: Int = 1_000, + private val timeoutMs: Int = 10_000 +) : TimeSource { - class AllRequestsFailure(errorMsg: String, val failures: List) - : RuntimeException(errorMsg, failures.firstOrNull()?.error) + class AllRequestsFailure(errorMsg: String, cause: Throwable?) + : RuntimeException(errorMsg, cause) override fun config() = TimeSourceConfig(id = id, priority = priority) - override fun requestTime(): Single { - return Single - .fromCallable { AndroidSntpClient.queryHostAddress(ntpPool) } - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .flatMap { address -> - val createRequest = { - Single - .fromCallable { - AndroidSntpClient.requestTime(address, - AndroidSntpClient.NTP_PORT, timeoutMs) - } - .onErrorReturn { error -> - val msg = error.message ?: "Error requesting time source time." - Result.Failure(error, msg) - } - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) + override fun requestTime(): Single = + Single + .create { emitter -> + try { + val result = AndroidSntpClient.queryHostAddress(ntpPool) + if (!emitter.isDisposed) { + emitter.onSuccess(result) } + } catch (t: Throwable) { + emitter.tryOnError(t) + } + } + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .flatMap { address -> + // Make 5 requests + val requests = (1..5).map { requestTimeToAddress(address) } - // Make 5 requests - val requests = (1..5).map { createRequest() } + // Wait for completion, then join them + Single.zip(requests) { + @Suppress("UNCHECKED_CAST") + it.toList() as List + } + } + .map { rawResults -> + val results = turnSlowRequestsIntoFailure(rawResults) - // Wait for completion, then join them - Single.zip(requests) { - @Suppress("UNCHECKED_CAST") - it.toList() as List - } + val successes = results.mapNotNull { it as? Result.Success } + if (successes.isNotEmpty()) { + // If at least one succeeds, sort by 'round trip time' and get median. + successes + .sortedBy { it.roundTripTimeMs } + .map { it.ntpTimeMs } + .elementAt(successes.size / 2) + } else { + // If all fail, throw 'AllRequestsFailure' exception. + val failures = results.mapNotNull { it as? Result.Failure } + val msgs = failures.joinToString("; ", prefix = "[", postfix = "]") { it.errorMsg } + val errorMsg = "All NTP requests failed: $msgs" + val cause = failures.firstOrNull()?.error + throw AllRequestsFailure(errorMsg, cause) } - .map { rawResults -> - val results = rawResults - .map { - when (it) { - is Result.Success -> when (it.roundTripTimeMs > maxRoundTripMs) { - true -> Result.Failure(null, "RoundTrip time exceeded allowed threshold:" + - " took ${it.roundTripTimeMs}, but max is $maxRoundTripMs") - else -> it - } - else -> it - } - } + } + + private fun turnSlowRequestsIntoFailure(rawResults: List): List = + rawResults.map { + when (it) { + is Result.Success -> when (it.roundTripTimeMs > maxRoundTripMs) { + true -> Result.Failure( + null, + "RoundTrip time exceeded allowed threshold:" + + " took ${it.roundTripTimeMs}, but max is $maxRoundTripMs" + ) + else -> it + } + else -> it + } + } + + private fun requestTimeToAddress(address: InetAddress) = + Single + .create { emitter -> + try { + val result = AndroidSntpClient.requestTime( + address, + AndroidSntpClient.NTP_PORT, + timeoutMs + ) - val successes = results.map { it as? Result.Success }.filterNotNull() - if (successes.isNotEmpty()) { - // If at least one succeeds, sort by 'round trip time' and get median. - successes - .sortedBy { it.roundTripTimeMs } - .map { it.ntpTimeMs } - .elementAt(successes.size / 2) - } else { - // If all fail, throw 'AllRequestsFailure' exception. - val failures = results.map { it as? Result.Failure }.filterNotNull() - val msgs = failures.map { it.errorMsg }.joinToString("; ", prefix = "[", postfix = "]") - val errorMsg = "All NTP requests failed: $msgs" - throw AllRequestsFailure(errorMsg, failures) + if (!emitter.isDisposed) { + emitter.onSuccess(result) } + } catch (t: Throwable) { + emitter.tryOnError(t) } - } + } + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .onErrorReturn { error -> + val msg = error.message ?: "Error requesting time source time." + Result.Failure(error, msg) + } } \ No newline at end of file diff --git a/tempo/src/test/java/io/tempo/internal/TempoInstanceTests.kt b/tempo/src/test/java/io/tempo/internal/TempoInstanceTests.kt index 4906bc6..312d368 100644 --- a/tempo/src/test/java/io/tempo/internal/TempoInstanceTests.kt +++ b/tempo/src/test/java/io/tempo/internal/TempoInstanceTests.kt @@ -41,12 +41,12 @@ import io.tempo.TimeSourceCache import io.tempo.TimeSourceConfig import io.tempo.TimeSourceWrapper import org.junit.Test -import java.util.* +import java.util.Random import java.util.concurrent.TimeUnit -class TempoInstanceTests { - fun defaultInstance(changeParams: Initializer.() -> Unit = {}): TempoInstance { +internal class TempoInstanceTests { + private fun defaultInstance(changeParams: Initializer.() -> Unit = {}): TempoInstance { val syncRetryStrategy = SyncRetryStrategy.ConstantInterval(10L, 10L, 3) val initializer = Initializer( timeSources = listOf(StubTimeSource()),