Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/main/java/com/lyft/kronos/demo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class MainActivity : Activity() {
}

private fun bindDeviceClock() {
findViewById<TextClock>(R.id.android_clock).clock = AndroidClockFactory.createDeviceClock()
findViewById<TextClock>(R.id.android_clock).clock = AndroidClockFactory.createDeviceClock(this)
}

private fun bindKronosClock() {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/lyft/kronos/demo/TextClock.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import java.util.concurrent.TimeUnit

class TextClock : TextView {

var clock : Clock = AndroidClockFactory.createDeviceClock()
var clock : Clock = AndroidClockFactory.createDeviceClock(context)

private val ticker = object : Runnable {
override fun run() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ object AndroidClockFactory {
* Create a device clock that uses the OS/device specific API to retrieve time
*/
@JvmStatic
fun createDeviceClock(): Clock = AndroidSystemClock()
fun createDeviceClock(context: Context): Clock = AndroidSystemClock(context)

@JvmStatic
@JvmOverloads
Expand All @@ -26,9 +26,9 @@ object AndroidClockFactory {
cacheExpirationMs: Long = CACHE_EXPIRATION_MS,
maxNtpResponseTimeMs: Long = MAX_NTP_RESPONSE_TIME_MS): KronosClock {

val deviceClock = createDeviceClock()
val deviceClock = createDeviceClock(context)
val cache = SharedPreferenceSyncResponseCache(context.getSharedPreferences(SharedPreferenceSyncResponseCache.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE))

return ClockFactory.createKronosClock(deviceClock, cache, syncListener, ntpHosts, requestTimeoutMs, minWaitTimeBetweenSyncMs, cacheExpirationMs, maxNtpResponseTimeMs)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package com.lyft.kronos.internal

import android.content.Context
import android.os.Build
import android.os.SystemClock
import android.provider.Settings
import com.lyft.kronos.Clock

internal class AndroidSystemClock : Clock {
internal class AndroidSystemClock(private val context: Context) : Clock {
override fun getCurrentTimeMs(): Long = System.currentTimeMillis()
override fun getElapsedTimeMs(): Long = SystemClock.elapsedRealtime()
}
override fun getBootCount(): Int? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT)
} else {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ internal class SharedPreferenceSyncResponseCache(private val sharedPreferences:
override var elapsedTime: Long
get() = sharedPreferences.getLong(KEY_ELAPSED_TIME, TIME_UNAVAILABLE)
set(value) = sharedPreferences.edit().putLong(KEY_ELAPSED_TIME, value).apply()
override var bootCount: Int?
get() =
if (sharedPreferences.contains(KEY_BOOT_COUNT))
sharedPreferences.getInt(KEY_BOOT_COUNT, 0)
else null
set(value) =
if (value == null)
sharedPreferences.edit().remove(KEY_BOOT_COUNT).apply()
else
sharedPreferences.edit().putInt(KEY_BOOT_COUNT, value).apply()
override var currentOffset: Long
get() = sharedPreferences.getLong(KEY_OFFSET, TIME_UNAVAILABLE)
set(value) = sharedPreferences.edit().putLong(KEY_OFFSET, value).apply()
Expand All @@ -23,6 +33,7 @@ internal class SharedPreferenceSyncResponseCache(private val sharedPreferences:
internal const val SHARED_PREFERENCES_NAME = "com.lyft.kronos.shared_preferences"
internal const val KEY_CURRENT_TIME = "com.lyft.kronos.cached_current_time"
internal const val KEY_ELAPSED_TIME = "com.lyft.kronos.cached_elapsed_time"
internal const val KEY_BOOT_COUNT = "com.lyft.kronos.cached_boot_count"
internal const val KEY_OFFSET = "com.lyft.kronos.cached_offset"
}
}
Expand Down
5 changes: 5 additions & 0 deletions kronos-java/src/main/java/com/lyft/kronos/Clock.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ interface Clock {
* @return milliseconds since boot, including time spent in sleep.
*/
fun getElapsedTimeMs(): Long

/**
* @return boot count. (optional)
*/
fun getBootCount(): Int?
}

data class KronosTime(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package com.lyft.kronos

interface SyncResponseCache {
var currentTime : Long
var elapsedTime : Long
var elapsedTime: Long
var bootCount: Int?
var currentOffset: Long
fun clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ internal class KronosClockImpl(private val ntpService: SntpService, private val

override fun getElapsedTimeMs(): Long = fallbackClock.getElapsedTimeMs()

override fun getBootCount(): Int? = fallbackClock.getBootCount()

override fun getCurrentTime(): KronosTime {
val currentTime = ntpService.currentTime()
return currentTime ?: KronosTime(posixTimeMs = fallbackClock.getCurrentTimeMs(), timeSinceLastNtpSyncMs = null)
}

override fun getCurrentNtpTimeMs() : Long? = ntpService.currentTime()?.posixTimeMs
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public Response requestTime(String host, Long timeout) throws IOException {

// use the times on this side of the network latency
// (response rather than request time)
return new Response(responseTime, responseTicks, clockOffset, deviceClock);
return new Response(responseTime, responseTicks, deviceClock.getBootCount(), clockOffset, deviceClock);
} finally {
if (socket != null) {
socket.close();
Expand Down Expand Up @@ -206,14 +206,18 @@ public static final class Response {
// Device system uptime (milliseconds since reboot)
private final long deviceElapsedTimestampMs;

// Device boot count. (optional)
private final Integer deviceBootCount;

// delta between NTP and device clock (milliseconds)
private final long offsetMs;

private final Clock deviceClock;

Response(long deviceCurrentTimestampMs, long deviceElapsedTimestampMs, long offsetMs, Clock deviceClock) {
Response(long deviceCurrentTimestampMs, long deviceElapsedTimestampMs, Integer bootCount, long offsetMs, Clock deviceClock) {
this.deviceCurrentTimestampMs = deviceCurrentTimestampMs;
this.deviceElapsedTimestampMs = deviceElapsedTimestampMs;
this.deviceBootCount = bootCount;
this.offsetMs = offsetMs;
this.deviceClock = deviceClock;
}
Expand Down Expand Up @@ -241,6 +245,10 @@ public long getCurrentTimeMs() {
return deviceCurrentTimestampMs + offsetMs + getResponseAge();
}

public Integer getDeviceBootCount() {
return deviceBootCount;
}

/**
* @return offsetMs between device wall clock time and NTP time (ntpTimestampMs - deviceTime)
*/
Expand All @@ -261,6 +269,9 @@ public long getResponseAge() {
* @return True if the system has not been rebooted since this was created, false otherwise.
*/
boolean isFromSameBoot() {
if (this.deviceBootCount != null) {
return this.deviceBootCount.equals(deviceClock.getBootCount());
}
final long bootTime = this.deviceCurrentTimestampMs - this.deviceElapsedTimestampMs;
final long systemCurrentTimeMs = deviceClock.getCurrentTimeMs();
final long systemElapsedTimeMs = deviceClock.getElapsedTimeMs();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ internal class SntpResponseCacheImpl(
val currentTime = syncResponseCache.currentTime
val elapsedTime = syncResponseCache.elapsedTime
val currentOffset = syncResponseCache.currentOffset
val bootCount = syncResponseCache.bootCount
return when (elapsedTime) {
TIME_UNAVAILABLE -> null
else -> SntpClient.Response(currentTime, elapsedTime, currentOffset, deviceClock)
else -> SntpClient.Response(currentTime, elapsedTime, bootCount, currentOffset, deviceClock)
}
}

override fun update(response: SntpClient.Response) {
synchronized(this) {
syncResponseCache.currentTime = response.deviceCurrentTimestampMs
syncResponseCache.elapsedTime = response.deviceElapsedTimestampMs
syncResponseCache.bootCount = response.deviceBootCount
syncResponseCache.currentOffset = response.offsetMs
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ private void setupDeviceClock(long current) {
when(deviceClock.getCurrentTimeMs()).thenReturn(current);
}

private void setupDeviceClock(long current, long elapsed) {
private void setupDeviceClock(long current, long elapsed, Integer bootCount) {
when(deviceClock.getCurrentTimeMs()).thenReturn(current);
when(deviceClock.getElapsedTimeMs()).thenReturn(elapsed);
when(deviceClock.getBootCount()).thenReturn(bootCount);
}

@Test
Expand Down Expand Up @@ -177,18 +178,36 @@ public void requestTimeShouldSetTimeout() throws IOException {

@Test
public void responseShouldNotAdjustWhenBootTimeSeemsEqual() {
final SntpClient.Response original = new SntpClient.Response(10_000_000L, 6_000L, 1L, deviceClock);
final SntpClient.Response original = new SntpClient.Response(10_000_000L, 6_000L, null, 1L, deviceClock);
// 10 seconds passed
setupDeviceClock(10_010_000L, 16_000L);
setupDeviceClock(10_010_000L, 16_000L, null);

assertThat(original.isFromSameBoot());
}

@Test
public void responseShouldAdjustWhenBootTimeChanged() {
final SntpClient.Response original = new SntpClient.Response(10_000_000L, 6_000L, 1L, deviceClock);
final SntpClient.Response original = new SntpClient.Response(10_000_000L, 6_000L, null, 1L, deviceClock);
// 10 seconds passed, but we rebooted 5 seconds ago
setupDeviceClock(10_010_000L, 5_000L);
setupDeviceClock(10_010_000L, 5_000L, null);

assertThat(original.isFromSameBoot()).isFalse();
}

@Test
public void responseShouldAdjustWhenBootCountSeemsEqual() {
final SntpClient.Response original = new SntpClient.Response(10_000_000L, 6_000L, 10, 1L, deviceClock);
// 10 seconds passed, but we changed system clock forward by 30 seconds.
setupDeviceClock(10_040_000L, 16_000L, 10);

assertThat(original.isFromSameBoot());
}

@Test
public void responseShouldAdjustWhenBootCountChanged() {
final SntpClient.Response original = new SntpClient.Response(10_000_000L, 5_000L, 10, 1L, deviceClock);
// 10 seconds passed, but we rebooted 5 seconds ago and changed system clock backward by 10 seconds
setupDeviceClock(10_000_000L, 5_000L, 11);

assertThat(original.isFromSameBoot()).isFalse();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class SntpResponseCacheTest {
init {
whenever(deviceClock.getCurrentTimeMs()).thenReturn(CURRENT_TIME_MS)
whenever(deviceClock.getElapsedTimeMs()).thenReturn(ELAPSED_MS)
whenever(deviceClock.getBootCount()).thenReturn(BOOT_COUNT)
cache = SntpResponseCacheImpl(syncResponseCache, deviceClock)
}

Expand All @@ -38,15 +39,18 @@ class SntpResponseCacheTest {
val currentTime = deviceClock.getCurrentTimeMs()
val elapsedTime = deviceClock.getElapsedTimeMs()
val currentOffset = TimeUnit.MINUTES.toMillis(5)
val bootCount = deviceClock.getBootCount()

whenever(syncResponseCache.currentTime).thenReturn(currentTime)
whenever(syncResponseCache.elapsedTime).thenReturn(elapsedTime)
whenever(syncResponseCache.bootCount).thenReturn(bootCount)
whenever(syncResponseCache.currentOffset).thenReturn(currentOffset)

val cachedResponse = cache.get()
assertThat(cachedResponse).isNotNull()
assertThat(cachedResponse!!.deviceCurrentTimestampMs).isEqualTo(currentTime)
assertThat(cachedResponse.deviceElapsedTimestampMs).isEqualTo(elapsedTime)
assertThat(cachedResponse.deviceBootCount).isEqualTo(bootCount)
assertThat(cachedResponse.offsetMs).isEqualTo(currentOffset)
}

Expand Down Expand Up @@ -75,11 +79,12 @@ class SntpResponseCacheTest {
@Throws(Exception::class)
fun testUpdate() {
val currentOffset = TimeUnit.HOURS.toMillis(5)
val response = SntpClient.Response(CURRENT_TIME_MS, ELAPSED_MS, currentOffset, deviceClock)
val response = SntpClient.Response(CURRENT_TIME_MS, ELAPSED_MS, BOOT_COUNT, currentOffset, deviceClock)
cache.update(response)

verify(syncResponseCache, times(1)).currentTime = CURRENT_TIME_MS
verify(syncResponseCache, times(1)).elapsedTime = ELAPSED_MS
verify(syncResponseCache, times(1)).bootCount = BOOT_COUNT
verify(syncResponseCache, times(1)).currentOffset = currentOffset
}

Expand All @@ -95,5 +100,6 @@ class SntpResponseCacheTest {

private val CURRENT_TIME_MS = 1522964196L
private val ELAPSED_MS = TimeUnit.HOURS.toMillis(8)
private val BOOT_COUNT = 10
}
}