Add implementation for BackendIdleRefreshManager

This commit is contained in:
cketti 2021-06-16 14:53:50 +02:00
parent a8cf420741
commit a74c206a0c
8 changed files with 356 additions and 6 deletions

View file

@ -0,0 +1,17 @@
package com.fsck.k9.backends
import com.fsck.k9.backend.imap.SystemAlarmManager
class AndroidAlarmManager : SystemAlarmManager {
override fun setAlarm(triggerTime: Long, callback: () -> Unit) {
TODO("implement")
}
override fun cancelAlarm() {
TODO("implement")
}
override fun now(): Long {
TODO("implement")
}
}

View file

@ -2,6 +2,7 @@ package com.fsck.k9.backends
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.backend.imap.BackendIdleRefreshManager
import com.fsck.k9.backend.imap.SystemAlarmManager
import com.fsck.k9.backend.jmap.JmapAccountDiscovery
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import org.koin.dsl.module
@ -26,7 +27,8 @@ val backendsModule = module {
trustedSocketFactory = get()
)
}
single<IdleRefreshManager> { BackendIdleRefreshManager() }
single<SystemAlarmManager> { AndroidAlarmManager() }
single<IdleRefreshManager> { BackendIdleRefreshManager(alarmManager = get()) }
single { Pop3BackendFactory(get(), get()) }
single { WebDavBackendFactory(get(), get(), get()) }
single { JmapBackendFactory(get(), get()) }

View file

@ -0,0 +1,17 @@
package com.fsck.k9.backends
import com.fsck.k9.backend.imap.SystemAlarmManager
class AndroidAlarmManager : SystemAlarmManager {
override fun setAlarm(triggerTime: Long, callback: () -> Unit) {
TODO("implement")
}
override fun cancelAlarm() {
TODO("implement")
}
override fun now(): Long {
TODO("implement")
}
}

View file

@ -2,6 +2,7 @@ package com.fsck.k9.backends
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.backend.imap.BackendIdleRefreshManager
import com.fsck.k9.backend.imap.SystemAlarmManager
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import org.koin.dsl.module
@ -24,7 +25,8 @@ val backendsModule = module {
trustedSocketFactory = get()
)
}
single<IdleRefreshManager> { BackendIdleRefreshManager() }
single<SystemAlarmManager> { AndroidAlarmManager() }
single<IdleRefreshManager> { BackendIdleRefreshManager(alarmManager = get()) }
single { Pop3BackendFactory(get(), get()) }
single { WebDavBackendFactory(get(), get(), get()) }
}

View file

@ -15,6 +15,7 @@ dependencies {
testImplementation project(":mail:testing")
testImplementation "junit:junit:${versions.junit}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "com.google.truth:truth:${versions.truth}"
}
android {

View file

@ -3,12 +3,134 @@ package com.fsck.k9.backend.imap
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import com.fsck.k9.mail.store.imap.IdleRefreshTimer
class BackendIdleRefreshManager : IdleRefreshManager {
override fun startTimer(timeout: Long, callback: () -> Unit): IdleRefreshTimer {
TODO("implement")
private typealias Callback = () -> Unit
private const val MIN_TIMER_DELTA = 1 * 60 * 1000L
private const val NO_TRIGGER_TIME = 0L
/**
* Timer mechanism to refresh IMAP IDLE connections.
*
* Triggers timers early if necessary to reduce the number of times the device has to be woken up.
*/
class BackendIdleRefreshManager(private val alarmManager: SystemAlarmManager) : IdleRefreshManager {
private var timers = mutableSetOf<BackendIdleRefreshTimer>()
private var currentTriggerTime = NO_TRIGGER_TIME
private var minTimeout = Long.MAX_VALUE
private var minTimeoutTimestamp = 0L
@Synchronized
override fun startTimer(timeout: Long, callback: Callback): IdleRefreshTimer {
require(timeout > MIN_TIMER_DELTA) { "Timeout needs to be greater than $MIN_TIMER_DELTA ms" }
val now = alarmManager.now()
val triggerTime = now + timeout
updateMinTimeout(timeout, now)
setOrUpdateAlarm(triggerTime)
return BackendIdleRefreshTimer(triggerTime, callback).also { timer ->
timers.add(timer)
}
}
override fun resetTimers() {
TODO("implement")
synchronized(this) {
cancelAlarm()
}
onTimeout()
}
private fun updateMinTimeout(timeout: Long, now: Long) {
if (minTimeoutTimestamp + minTimeout * 2 < now) {
minTimeout = Long.MAX_VALUE
}
if (timeout <= minTimeout) {
minTimeout = timeout
minTimeoutTimestamp = now
}
}
private fun setOrUpdateAlarm(triggerTime: Long) {
if (currentTriggerTime == NO_TRIGGER_TIME) {
setAlarm(triggerTime)
} else if (currentTriggerTime - triggerTime > MIN_TIMER_DELTA) {
adjustAlarm(triggerTime)
}
}
private fun setAlarm(triggerTime: Long) {
currentTriggerTime = triggerTime
alarmManager.setAlarm(triggerTime, ::onTimeout)
}
private fun adjustAlarm(triggerTime: Long) {
currentTriggerTime = triggerTime
alarmManager.cancelAlarm()
alarmManager.setAlarm(triggerTime, ::onTimeout)
}
private fun cancelAlarm() {
currentTriggerTime = NO_TRIGGER_TIME
alarmManager.cancelAlarm()
}
private fun onTimeout() {
val triggerTimers = synchronized(this) {
currentTriggerTime = NO_TRIGGER_TIME
if (timers.isEmpty()) return
val now = alarmManager.now()
val minNextTriggerTime = now + minTimeout
val triggerTimers = timers.filter { it.triggerTime < minNextTriggerTime - MIN_TIMER_DELTA }
timers.removeAll(triggerTimers)
timers.minOfOrNull { it.triggerTime }?.let { nextTriggerTime ->
setAlarm(nextTriggerTime)
}
triggerTimers
}
for (timer in triggerTimers) {
timer.onTimeout()
}
}
@Synchronized
private fun removeTimer(timer: BackendIdleRefreshTimer) {
timers.remove(timer)
if (timers.isEmpty()) {
cancelAlarm()
}
}
internal inner class BackendIdleRefreshTimer(
val triggerTime: Long,
val callback: Callback
) : IdleRefreshTimer {
override var isWaiting: Boolean = true
private set
@Synchronized
override fun cancel() {
if (isWaiting) {
isWaiting = false
removeTimer(this)
}
}
internal fun onTimeout() {
synchronized(this) {
isWaiting = false
}
callback.invoke()
}
}
}

View file

@ -0,0 +1,7 @@
package com.fsck.k9.backend.imap
interface SystemAlarmManager {
fun setAlarm(triggerTime: Long, callback: () -> Unit)
fun cancelAlarm()
fun now(): Long
}

View file

@ -0,0 +1,182 @@
package com.fsck.k9.backend.imap
import com.google.common.truth.Truth.assertThat
import org.junit.Test
private const val START_TIME = 100_000_000L
class BackendIdleRefreshManagerTest {
val alarmManager = MockSystemAlarmManager(START_TIME)
val idleRefreshManager = BackendIdleRefreshManager(alarmManager)
@Test
fun `single timer`() {
val timeout = 15 * 60 * 1000L
val callback = RecordingCallback()
idleRefreshManager.startTimer(timeout, callback::alarm)
alarmManager.advanceTime(timeout)
assertThat(alarmManager.alarmTimes).isEqualTo(listOf(START_TIME + timeout))
assertThat(callback.wasCalled).isTrue()
}
@Test
fun `starting two timers in quick succession`() {
val timeout = 15 * 60 * 1000L
val callback1 = RecordingCallback()
val callback2 = RecordingCallback()
idleRefreshManager.startTimer(timeout, callback1::alarm)
// Advance clock less than MIN_TIMER_DELTA
alarmManager.advanceTime(100)
idleRefreshManager.startTimer(timeout, callback2::alarm)
alarmManager.advanceTime(timeout)
assertThat(alarmManager.alarmTimes).isEqualTo(listOf(START_TIME + timeout))
assertThat(callback1.wasCalled).isTrue()
assertThat(callback2.wasCalled).isTrue()
}
@Test
fun `starting second timer some time after first should trigger both at initial trigger time`() {
val timeout = 15 * 60 * 1000L
val waitTime = 10 * 60 * 1000L
val callback1 = RecordingCallback()
val callback2 = RecordingCallback()
idleRefreshManager.startTimer(timeout, callback1::alarm)
// Advance clock by more than MIN_TIMER_DELTA but less than 'timeout'
alarmManager.advanceTime(waitTime)
assertThat(callback1.wasCalled).isFalse()
idleRefreshManager.startTimer(timeout, callback2::alarm)
alarmManager.advanceTime(timeout - waitTime)
assertThat(alarmManager.alarmTimes).isEqualTo(listOf(START_TIME + timeout))
assertThat(callback1.wasCalled).isTrue()
assertThat(callback2.wasCalled).isTrue()
}
@Test
fun `second timer with lower timeout should reschedule alarm`() {
val timeout1 = 15 * 60 * 1000L
val timeout2 = 10 * 60 * 1000L
val callback1 = RecordingCallback()
val callback2 = RecordingCallback()
idleRefreshManager.startTimer(timeout1, callback1::alarm)
assertThat(alarmManager.triggerTime).isEqualTo(START_TIME + timeout1)
idleRefreshManager.startTimer(timeout2, callback2::alarm)
alarmManager.advanceTime(timeout2)
assertThat(alarmManager.alarmTimes).isEqualTo(listOf(START_TIME + timeout1, START_TIME + timeout2))
assertThat(callback1.wasCalled).isTrue()
assertThat(callback2.wasCalled).isTrue()
}
@Test
fun `do not trigger timers earlier than necessary`() {
val timeout1 = 10 * 60 * 1000L
val timeout2 = 23 * 60 * 1000L
val callback1 = RecordingCallback()
val callback2 = RecordingCallback()
val callback3 = RecordingCallback()
idleRefreshManager.startTimer(timeout1, callback1::alarm)
idleRefreshManager.startTimer(timeout2, callback2::alarm)
alarmManager.advanceTime(timeout1)
assertThat(callback1.wasCalled).isTrue()
assertThat(callback2.wasCalled).isFalse()
idleRefreshManager.startTimer(timeout1, callback3::alarm)
alarmManager.advanceTime(timeout1)
assertThat(alarmManager.alarmTimes).isEqualTo(
listOf(START_TIME + timeout1, START_TIME + timeout2, START_TIME + timeout1 + timeout1)
)
assertThat(callback2.wasCalled).isTrue()
assertThat(callback3.wasCalled).isTrue()
}
@Test
fun `reset timers`() {
val timeout = 10 * 60 * 1000L
val callback = RecordingCallback()
idleRefreshManager.startTimer(timeout, callback::alarm)
alarmManager.advanceTime(5 * 60 * 1000L)
assertThat(callback.wasCalled).isFalse()
idleRefreshManager.resetTimers()
assertThat(alarmManager.triggerTime).isEqualTo(NO_TRIGGER_TIME)
assertThat(callback.wasCalled).isTrue()
}
@Test
fun `cancel timer`() {
val timeout = 10 * 60 * 1000L
val callback = RecordingCallback()
val timer = idleRefreshManager.startTimer(timeout, callback::alarm)
alarmManager.advanceTime(5 * 60 * 1000L)
timer.cancel()
assertThat(alarmManager.triggerTime).isEqualTo(NO_TRIGGER_TIME)
assertThat(callback.wasCalled).isFalse()
}
}
class RecordingCallback {
var wasCalled = false
private set
fun alarm() {
wasCalled = true
}
}
typealias Callback = () -> Unit
private const val NO_TRIGGER_TIME = -1L
class MockSystemAlarmManager(startTime: Long) : SystemAlarmManager {
var now = startTime
var triggerTime = NO_TRIGGER_TIME
var callback: Callback? = null
val alarmTimes = mutableListOf<Long>()
override fun setAlarm(triggerTime: Long, callback: () -> Unit) {
this.triggerTime = triggerTime
this.callback = callback
alarmTimes.add(triggerTime)
}
override fun cancelAlarm() {
this.triggerTime = NO_TRIGGER_TIME
this.callback = null
}
override fun now(): Long = now
fun advanceTime(delta: Long) {
now += delta
if (now >= triggerTime) {
trigger()
}
}
private fun trigger() {
callback?.invoke().also {
triggerTime = NO_TRIGGER_TIME
callback = null
}
}
}