From cea43b3529feb4bc361cd9d5d544015ff838ed19 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 14 Nov 2021 21:42:41 -0500 Subject: [PATCH] Show connection status --- .../main/java/io/heckel/ntfy/data/Database.kt | 11 ++-- .../java/io/heckel/ntfy/data/Repository.kt | 53 ++++++++++++++++--- .../io/heckel/ntfy/msg/SubscriberService.kt | 9 +++- .../java/io/heckel/ntfy/ui/MainAdapter.kt | 6 ++- app/src/main/res/values/strings.xml | 3 +- 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/data/Database.kt b/app/src/main/java/io/heckel/ntfy/data/Database.kt index 38f06e1..7e48f6a 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt @@ -1,8 +1,6 @@ package io.heckel.ntfy.data import android.content.Context -import androidx.annotation.NonNull -import androidx.lifecycle.LiveData import androidx.room.* import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase @@ -15,9 +13,14 @@ data class Subscription( @ColumnInfo(name = "topic") val topic: String, @ColumnInfo(name = "instant") val instant: Boolean, @Ignore val notifications: Int, - @Ignore val lastActive: Long = 0 // Unix timestamp + @Ignore val lastActive: Long = 0, // Unix timestamp + @Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE ) { - constructor(id: Long, baseUrl: String, topic: String, instant: Boolean) : this(id, baseUrl, topic, instant, 0, 0) + constructor(id: Long, baseUrl: String, topic: String, instant: Boolean) : this(id, baseUrl, topic, instant, 0, 0, ConnectionState.NOT_APPLICABLE) +} + +enum class ConnectionState { + NOT_APPLICABLE, RECONNECTING, CONNECTED } data class SubscriptionWithMetadata( diff --git a/app/src/main/java/io/heckel/ntfy/data/Repository.kt b/app/src/main/java/io/heckel/ntfy/data/Repository.kt index e93dc3e..ec78fef 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Repository.kt @@ -2,12 +2,13 @@ package io.heckel.ntfy.data import android.util.Log import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData -import androidx.lifecycle.map -import kotlinx.coroutines.flow.map +import androidx.lifecycle.* +import java.util.concurrent.ConcurrentHashMap class Repository(private val subscriptionDao: SubscriptionDao, private val notificationDao: NotificationDao) { + private val connectionStates = ConcurrentHashMap() + private val connectionStatesLiveData = MutableLiveData(connectionStates) + init { Log.d(TAG, "Created $this") } @@ -16,7 +17,9 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif return subscriptionDao .listFlow() .asLiveData() - .map { list -> toSubscriptionList(list) } + .combineWith(connectionStatesLiveData) { subscriptionsWithMetadata, _ -> + toSubscriptionList(subscriptionsWithMetadata.orEmpty()) + } } fun getSubscriptionIdsWithInstantStatusLiveData(): LiveData>> { @@ -98,13 +101,15 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif private fun toSubscriptionList(list: List): List { return list.map { s -> + val connectionState = connectionStates.getOrElse(s.id) { ConnectionState.NOT_APPLICABLE } Subscription( id = s.id, baseUrl = s.baseUrl, topic = s.topic, instant = s.instant, lastActive = s.lastActive, - notifications = s.notifications + notifications = s.notifications, + state = connectionState ) } } @@ -119,12 +124,29 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif topic = s.topic, instant = s.instant, lastActive = s.lastActive, - notifications = s.notifications + notifications = s.notifications, + state = getState(s.id) ) } + fun updateStateIfChanged(subscriptionId: Long, newState: ConnectionState) { + val state = connectionStates.getOrElse(subscriptionId) { ConnectionState.NOT_APPLICABLE } + if (state !== newState) { + if (newState == ConnectionState.NOT_APPLICABLE) { + connectionStates.remove(subscriptionId) + } else { + connectionStates[subscriptionId] = newState + } + connectionStatesLiveData.postValue(connectionStates) + } + } + + private fun getState(subscriptionId: Long): ConnectionState { + return connectionStatesLiveData.value!!.getOrElse(subscriptionId) { ConnectionState.NOT_APPLICABLE } + } + companion object { - private val TAG = "NtfyRepository" + private const val TAG = "NtfyRepository" private var instance: Repository? = null fun getInstance(subscriptionDao: SubscriptionDao, notificationDao: NotificationDao): Repository { @@ -136,3 +158,18 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif } } } + +/* https://stackoverflow.com/a/57079290/1440785 */ +fun LiveData.combineWith( + liveData: LiveData, + block: (T?, K?) -> R +): LiveData { + val result = MediatorLiveData() + result.addSource(this) { + result.value = block(this.value, liveData.value) + } + result.addSource(liveData) { + result.value = block(this.value, liveData.value) + } + return result +} diff --git a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt index 2f8022d..2539a22 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt @@ -12,6 +12,7 @@ import android.util.Log import androidx.core.app.NotificationCompat import io.heckel.ntfy.R import io.heckel.ntfy.app.Application +import io.heckel.ntfy.data.ConnectionState import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.topicUrl import io.heckel.ntfy.ui.MainActivity @@ -156,7 +157,10 @@ class SubscriberService : Service() { onNotificationReceived(scope, subscription, n) } val failed = AtomicBoolean(false) - val fail = { e: Exception -> failed.set(true) } + val fail = { e: Exception -> + failed.set(true) + repository.updateStateIfChanged(subscription.id, ConnectionState.RECONNECTING) + } // Call /json subscribe endpoint and loop until the call fails, is canceled, // or the job or service are cancelled/stopped @@ -164,11 +168,13 @@ class SubscriberService : Service() { val call = api.subscribe(subscription.id, subscription.baseUrl, subscription.topic, since, notify, fail) calls[subscription.id] = call while (!failed.get() && !call.isCanceled() && isActive && isServiceStarted) { + repository.updateStateIfChanged(subscription.id, ConnectionState.CONNECTED) Log.d(TAG, "[$url] Connection is active (failed=$failed, callCanceled=${call.isCanceled()}, jobActive=$isActive, serviceStarted=$isServiceStarted") delay(CONNECTION_LOOP_DELAY_MILLIS) // Resumes immediately if job is cancelled } } catch (e: Exception) { Log.e(TAG, "[$url] Connection failed: ${e.message}", e) + repository.updateStateIfChanged(subscription.id, ConnectionState.RECONNECTING) } // If we're not cancelled yet, wait little before retrying (incremental back-off) @@ -179,6 +185,7 @@ class SubscriberService : Service() { } } Log.d(TAG, "[$url] Connection job SHUT DOWN") + repository.updateStateIfChanged(subscription.id, ConnectionState.NOT_APPLICABLE) } private fun onNotificationReceived(scope: CoroutineScope, subscription: Subscription, n: io.heckel.ntfy.data.Notification) { diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt index 315b4e7..bc22b6b 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt @@ -9,6 +9,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.R +import io.heckel.ntfy.data.ConnectionState import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.topicShortUrl import java.text.SimpleDateFormat @@ -52,11 +53,14 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon fun bind(subscription: Subscription) { this.subscription = subscription - val statusMessage = if (subscription.notifications == 1) { + var statusMessage = if (subscription.notifications == 1) { context.getString(R.string.main_item_status_text_one, subscription.notifications) } else { context.getString(R.string.main_item_status_text_not_one, subscription.notifications) } + if (subscription.instant && subscription.state == ConnectionState.RECONNECTING) { + statusMessage += ", " + context.getString(R.string.main_item_status_reconnecting) + } val dateText = if (subscription.lastActive == 0L) { "" } else if (System.currentTimeMillis()/1000 - subscription.lastActive < 24 * 60 * 60) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9993a60..3ceb8ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ Subscribed topics - Report bugs + View source & report bugs https://heckel.io/ntfy-android Visit ntfy.sh @@ -29,6 +29,7 @@ %1$d notification %1$d notifications + reconnecting … Add subscription It looks like you don\'t have any subscriptions yet. Click the button below to create or subscribe to a topic. After that, you can send messages via PUT or POST and you\'ll receive notifications on your phone.