diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt index 0e177366c5a88df8c232ee76b00fdf85c73ca083..d2750638fb5354066350c1f7a414ae76ad5c64a4 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt @@ -35,7 +35,9 @@ import de.kuschku.libquassel.session.ISession import de.kuschku.libquassel.session.Session import de.kuschku.libquassel.session.SessionManager import de.kuschku.libquassel.util.compatibility.LoggingHandler +import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.INFO +import de.kuschku.libquassel.util.helpers.clampOf import de.kuschku.libquassel.util.helpers.value import de.kuschku.malheur.CrashHandler import de.kuschku.quasseldroid.BuildConfig @@ -354,14 +356,17 @@ class QuasselService : DaggerLifecycleService(), sessionManager.state .distinctUntilChanged() - .delay(200, TimeUnit.MILLISECONDS) - .throttleFirst(1, TimeUnit.SECONDS) .toLiveData() .observe( this, Observer { handlerService.backend { - LoggingHandler.log(INFO, "QuasselService", "Autoreconnect: State Changed") - sessionManager.autoReconnect() + if (it == ConnectionState.HANDSHAKE) { + backoff = BACKOFF_MIN + } + + if (it == ConnectionState.CLOSED) { + scheduleReconnect() + } } }) @@ -380,6 +385,23 @@ class QuasselService : DaggerLifecycleService(), updateNotificationStatus(this.progress) } + // Set default backoff to 5ms + private var backoff = BACKOFF_MIN + private var scheduled = false + private fun scheduleReconnect() { + if (!scheduled) { + log(INFO, "QuasselService", "Reconnect: Scheduling backoff in ${backoff / 1_000} seconds") + scheduled = true + handlerService.backendDelayed(backoff) { + log(INFO, "QuasselService", "Reconnect: Scheduled backoff happened") + scheduled = false + + backoff = clampOf(backoff * 2, BACKOFF_MIN, BACKOFF_MAX) + sessionManager.autoReconnect(true) + } + } + } + override fun onDestroy() { sharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE) { unregisterOnSharedPreferenceChangeListener(this@QuasselService) @@ -412,6 +434,11 @@ class QuasselService : DaggerLifecycleService(), } companion object { + // default backoff is 5 seconds + const val BACKOFF_MIN = 5_000L + // max is 30 minutes + const val BACKOFF_MAX = 1_800_000L + fun launch( context: Context, disconnect: Boolean? = null, diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/IntRangeHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/IntRangeHelper.kt index 8ea855a1869c5878b0fcaf3c841c3c0cbb93bfa2..15040669dbe1d9b5ee8c544df4196c85b78c28a2 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/helper/IntRangeHelper.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/IntRangeHelper.kt @@ -19,11 +19,11 @@ package de.kuschku.quasseldroid.util.helper -import de.kuschku.libquassel.util.helpers.clamp +import de.kuschku.libquassel.util.helpers.clampOf infix fun IntRange.without(other: IntRange): Iterable<IntRange> { - val otherStart = minOf(other.start, other.last + 1).clamp(this.start, this.last + 1) - val otherLast = maxOf(other.start, other.last + 1).clamp(this.start, this.last + 1) + val otherStart = clampOf(minOf(other.start, other.last + 1), this.start, this.last + 1) + val otherLast = clampOf(maxOf(other.start, other.last + 1), this.start, this.last + 1) val startingFragment: IntRange = this.start until otherStart val endingFragment: IntRange = otherLast + 1 until this.last + 1 diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferViewConfig.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferViewConfig.kt index 85f8df480ddfcd2a9eec2d535ee53b2fec2faef7..7168fff078ac30586b5753e4fa6cf15de70b6403 100644 --- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferViewConfig.kt +++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferViewConfig.kt @@ -25,7 +25,7 @@ import de.kuschku.libquassel.quassel.BufferInfo import de.kuschku.libquassel.quassel.syncables.interfaces.IBufferViewConfig import de.kuschku.libquassel.session.SignalProxy import de.kuschku.libquassel.util.flag.hasFlag -import de.kuschku.libquassel.util.helpers.clamp +import de.kuschku.libquassel.util.helpers.clampOf import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject @@ -126,7 +126,7 @@ class BufferViewConfig constructor( return val currentPos = _buffers.indexOf(bufferId) - val targetPos = pos.clamp(0, _buffers.size - 1) + val targetPos = clampOf(pos, 0, _buffers.size - 1) if (currentPos > targetPos) { _buffers.removeAt(currentPos) diff --git a/lib/src/main/java/de/kuschku/libquassel/session/HeartBeatThread.kt b/lib/src/main/java/de/kuschku/libquassel/session/HeartBeatThread.kt new file mode 100644 index 0000000000000000000000000000000000000000..13436adaddd0fb57a094b8de4bd945760a3e521c --- /dev/null +++ b/lib/src/main/java/de/kuschku/libquassel/session/HeartBeatThread.kt @@ -0,0 +1,34 @@ +package de.kuschku.libquassel.session + +import de.kuschku.libquassel.protocol.message.SignalProxyMessage +import org.threeten.bp.Duration +import org.threeten.bp.Instant + +class HeartBeatThread(private val session: Session) : Thread() { + private var running = true + private var lastHeartBeatReply: Instant = Instant.now() + override fun run() { + while (running) { + Thread.sleep(30_000) + val now = Instant.now() + if (Duration.between(lastHeartBeatReply, now).toMillis() > TIMEOUT) { + session.close() + } else { + session.dispatch(SignalProxyMessage.HeartBeat(now)) + } + } + } + + fun end() { + running = false + } + + fun setLastHeartBeatReply(time: Instant) { + this.lastHeartBeatReply = time + } + + companion object { + // Timeout, set to 2 minutes + const val TIMEOUT = 120_000L + } +} diff --git a/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt b/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt index 8a0a55988296b36ac876055bbc865538619c224a..7255cf2fb810be9f5241c2410599627e734b4f7d 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt @@ -29,7 +29,6 @@ import de.kuschku.libquassel.quassel.syncables.interfaces.ISyncableObject import de.kuschku.libquassel.quassel.syncables.interfaces.invokers.Invokers import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.DEBUG -import org.threeten.bp.Instant import java.io.Closeable @Suppress("LeakingThis") @@ -148,7 +147,6 @@ abstract class ProtocolHandler( override fun handle(f: SignalProxyMessage.HeartBeat): Boolean { dispatch(SignalProxyMessage.HeartBeatReply(f.timestamp)) - dispatch(SignalProxyMessage.HeartBeat(Instant.now())) return true } diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt index def9966bfc1d19782c33bcff6ce3ed617a92c44f..361c206705f9025a7206bd875659e354d6159e33 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt @@ -90,6 +90,8 @@ class Session( override val lag = BehaviorSubject.createDefault(0L) + private val heartBeatThread = HeartBeatThread(this) + init { coreConnection.start() } @@ -211,10 +213,10 @@ class Session( synchronize(networkConfig, true) synchronize(backlogManager) - - dispatch(SignalProxyMessage.HeartBeat(Instant.now())) } + heartBeatThread.start() + return true } @@ -231,11 +233,11 @@ class Session( } notificationManager?.init(this) coreConnection.setState(ConnectionState.CONNECTED) - dispatch(SignalProxyMessage.HeartBeat(Instant.now())) } override fun handle(f: SignalProxyMessage.HeartBeatReply): Boolean { val now = Instant.now() + heartBeatThread.setLastHeartBeatReply(f.timestamp) val latency = now.toEpochMilli() - f.timestamp.toEpochMilli() lag.onNext(latency) return true @@ -257,6 +259,8 @@ class Session( override fun close() { super.close() + heartBeatThread.end() + coreConnection.close() certManagers.clear() diff --git a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt index a0b87ceaf92620cb6df1195664ef168e861bc02d..a56e736cd83bca37daefb6859ba5edb96811f284 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt @@ -138,19 +138,23 @@ class SessionManager( ) } - fun autoReconnect(forceReconnect: Boolean = false) { - if (!hasErrored) { - state.or(ConnectionState.DISCONNECTED).let { - if (it == ConnectionState.CLOSED) { - log(INFO, "SessionManager", "Autoreconnect triggered") - reconnect(forceReconnect) - } else { - log(INFO, "SessionManager", "Autoreconnect failed: state is $it") - } + /** + * @return if an autoreconnect has been necessary + */ + fun autoReconnect(forceReconnect: Boolean = false) = if (!hasErrored) { + state.or(ConnectionState.DISCONNECTED).let { + if (it == ConnectionState.CLOSED) { + log(INFO, "SessionManager", "Autoreconnect triggered") + reconnect(forceReconnect) + true + } else { + log(INFO, "SessionManager", "Autoreconnect failed: state is $it") + false } - } else { - log(INFO, "SessionManager", "Autoreconnect failed: hasErrored") } + } else { + log(INFO, "SessionManager", "Autoreconnect failed: hasErrored") + false } fun reconnect(forceReconnect: Boolean = false) { diff --git a/lib/src/main/java/de/kuschku/libquassel/util/helpers/MathHelper.kt b/lib/src/main/java/de/kuschku/libquassel/util/helpers/MathHelper.kt index 6e6f989552d6ffe1c9aa1e98f64845d9fe208fdc..20e01930ad7b7a9fc8c5ced507f13cc295953e51 100644 --- a/lib/src/main/java/de/kuschku/libquassel/util/helpers/MathHelper.kt +++ b/lib/src/main/java/de/kuschku/libquassel/util/helpers/MathHelper.kt @@ -19,5 +19,8 @@ package de.kuschku.libquassel.util.helpers -inline fun Int.clamp(lowerBound: Int, upperBound: Int): Int = - maxOf(lowerBound, minOf(this, upperBound)) +inline fun clampOf(value: Int, lowerBound: Int, upperBound: Int): Int = + maxOf(lowerBound, minOf(value, upperBound)) + +inline fun clampOf(value: Long, lowerBound: Long, upperBound: Long): Long = + maxOf(lowerBound, minOf(value, upperBound))