ntfy-android/app/src/main/java/io/heckel/ntfy/service/JsonConnection.kt

114 lines
5.4 KiB
Kotlin
Raw Normal View History

package io.heckel.ntfy.service
2021-11-17 08:08:52 +13:00
2022-01-28 13:57:43 +13:00
import io.heckel.ntfy.db.*
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.msg.ApiService
2021-11-28 10:18:09 +13:00
import io.heckel.ntfy.util.topicUrl
2021-11-17 08:08:52 +13:00
import kotlinx.coroutines.*
import okhttp3.Call
import java.util.concurrent.atomic.AtomicBoolean
2022-01-16 07:31:34 +13:00
class JsonConnection(
2022-01-28 13:57:43 +13:00
private val connectionId: ConnectionId,
2022-01-16 07:31:34 +13:00
private val scope: CoroutineScope,
private val repository: Repository,
2021-11-17 08:08:52 +13:00
private val api: ApiService,
2022-01-28 13:57:43 +13:00
private val user: User?,
private val sinceId: String?,
private val stateChangeListener: (Collection<Long>, ConnectionState) -> Unit,
2021-11-17 08:08:52 +13:00
private val notificationListener: (Subscription, Notification) -> Unit,
private val serviceActive: () -> Boolean
2022-01-16 07:31:34 +13:00
) : Connection {
2022-01-28 13:57:43 +13:00
private val baseUrl = connectionId.baseUrl
private val topicsToSubscriptionIds = connectionId.topicsToSubscriptionIds
2023-03-05 19:10:32 +13:00
private val topicIsUnifiedPush = connectionId.topicIsUnifiedPush
private val subscriptionIds = topicsToSubscriptionIds.values
private val topicsStr = topicsToSubscriptionIds.keys.joinToString(separator = ",")
2023-03-05 19:10:32 +13:00
private val unifiedPushTopicsStr = topicIsUnifiedPush.filter { entry -> entry.value }.keys.joinToString(separator = ",")
2021-11-17 08:08:52 +13:00
private val url = topicUrl(baseUrl, topicsStr)
private var since: String? = sinceId
2021-11-17 08:08:52 +13:00
private lateinit var call: Call
private lateinit var job: Job
2022-01-16 07:31:34 +13:00
override fun start() {
2021-11-17 08:08:52 +13:00
job = scope.launch(Dispatchers.IO) {
Log.d(TAG, "[$url] Starting connection for subscriptions: $topicsToSubscriptionIds")
2021-11-17 08:08:52 +13:00
// Retry-loop: if the connection fails, we retry unless the job or service is cancelled/stopped
var retryMillis = 0L
while (isActive && serviceActive()) {
Log.d(TAG, "[$url] (Re-)starting connection for subscriptions: $topicsToSubscriptionIds")
2021-11-17 08:08:52 +13:00
val startTime = System.currentTimeMillis()
val notify = notify@ { topic: String, notification: Notification ->
since = notification.id
val subscriptionId = topicsToSubscriptionIds[topic] ?: return@notify
val subscription = repository.getSubscription(subscriptionId) ?: return@notify
2021-11-17 08:08:52 +13:00
val notificationWithSubscriptionId = notification.copy(subscriptionId = subscription.id)
notificationListener(subscription, notificationWithSubscriptionId)
}
val failed = AtomicBoolean(false)
val fail = { _: Exception ->
2021-11-17 08:08:52 +13:00
failed.set(true)
if (isActive && serviceActive()) { // Avoid UI update races if we're restarting a connection
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
}
2021-11-17 08:08:52 +13:00
}
// Call /json subscribe endpoint and loop until the call fails, is canceled,
// or the job or service are cancelled/stopped
try {
2023-03-05 19:10:32 +13:00
call = api.subscribe(baseUrl, topicsStr, unifiedPushTopicsStr, since, user, notify, fail)
2021-11-17 08:08:52 +13:00
while (!failed.get() && !call.isCanceled() && isActive && serviceActive()) {
stateChangeListener(subscriptionIds, ConnectionState.CONNECTED)
2021-11-17 08:08:52 +13:00
Log.d(TAG,"[$url] Connection is active (failed=$failed, callCanceled=${call.isCanceled()}, jobActive=$isActive, serviceStarted=${serviceActive()}")
delay(CONNECTION_LOOP_DELAY_MILLIS) // Resumes immediately if job is cancelled
}
} catch (e: Exception) {
Log.e(TAG, "[$url] Connection failed: ${e.message}", e)
if (isActive && serviceActive()) { // Avoid UI update races if we're restarting a connection
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
2021-11-17 08:08:52 +13:00
}
}
// If we're not cancelled yet, wait little before retrying (incremental back-off)
if (isActive && serviceActive()) {
retryMillis = nextRetryMillis(retryMillis, startTime)
Log.d(TAG, "[$url] Connection failed, retrying connection in ${retryMillis / 1000}s ...")
delay(retryMillis)
}
}
Log.d(TAG, "[$url] Connection job SHUT DOWN")
// FIXME: Do NOT update state here as this can lead to races; this leaks the subscription state map
}
}
override fun since(): String? {
2021-11-17 08:08:52 +13:00
return since
}
2022-01-16 13:20:30 +13:00
override fun close() {
2021-11-17 08:08:52 +13:00
Log.d(TAG, "[$url] Cancelling connection")
if (this::job.isInitialized) job.cancel()
if (this::call.isInitialized) call.cancel()
2021-11-17 08:08:52 +13:00
}
private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long {
val connectionDurationMillis = System.currentTimeMillis() - startTime
if (connectionDurationMillis > RETRY_RESET_AFTER_MILLIS) {
return RETRY_STEP_MILLIS
} else if (retryMillis + RETRY_STEP_MILLIS >= RETRY_MAX_MILLIS) {
return RETRY_MAX_MILLIS
}
return retryMillis + RETRY_STEP_MILLIS
}
companion object {
private const val TAG = "NtfySubscriberConn"
private const val CONNECTION_LOOP_DELAY_MILLIS = 30_000L
private const val RETRY_STEP_MILLIS = 5_000L
private const val RETRY_MAX_MILLIS = 60_000L
private const val RETRY_RESET_AFTER_MILLIS = 60_000L // Must be larger than CONNECTION_LOOP_DELAY_MILLIS
}
}