Show connection status
This commit is contained in:
parent
f69d1f5ee1
commit
cea43b3529
5 changed files with 67 additions and 15 deletions
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 & 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>
|
||||
|
|
Loading…
Reference in a new issue