Show connection status

This commit is contained in:
Philipp Heckel 2021-11-14 21:42:41 -05:00
parent f69d1f5ee1
commit cea43b3529
5 changed files with 67 additions and 15 deletions

View file

@ -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(

View file

@ -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<Long, ConnectionState>()
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<Set<Pair<Long, Boolean>>> {
@ -98,13 +101,15 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
private fun toSubscriptionList(list: List<SubscriptionWithMetadata>): List<Subscription> {
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 <T, K, R> LiveData<T>.combineWith(
liveData: LiveData<K>,
block: (T?, K?) -> R
): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(this) {
result.value = block(this.value, liveData.value)
}
result.addSource(liveData) {
result.value = block(this.value, liveData.value)
}
return result
}

View file

@ -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) {

View file

@ -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) {

View file

@ -16,7 +16,7 @@
<!-- Main activity: Action bar -->
<string name="main_action_bar_title">Subscribed topics</string>
<string name="main_menu_source_title">Report bugs</string>
<string name="main_menu_source_title">View source &amp; report bugs</string>
<string name="main_menu_source_url">https://heckel.io/ntfy-android</string>
<string name="main_menu_website_title">Visit ntfy.sh</string>
@ -29,6 +29,7 @@
<!-- Main activity: List and such -->
<string name="main_item_status_text_one">%1$d notification</string>
<string name="main_item_status_text_not_one">%1$d notifications</string>
<string name="main_item_status_reconnecting">reconnecting …</string>
<string name="main_add_button_description">Add subscription</string>
<string name="main_no_subscriptions_text">It looks like you don\'t have any subscriptions yet.</string>
<string name="main_how_to_intro">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.</string>