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
|
package io.heckel.ntfy.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.NonNull
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
@ -15,9 +13,14 @@ data class Subscription(
|
||||||
@ColumnInfo(name = "topic") val topic: String,
|
@ColumnInfo(name = "topic") val topic: String,
|
||||||
@ColumnInfo(name = "instant") val instant: Boolean,
|
@ColumnInfo(name = "instant") val instant: Boolean,
|
||||||
@Ignore val notifications: Int,
|
@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(
|
data class SubscriptionWithMetadata(
|
||||||
|
|
|
@ -2,12 +2,13 @@ package io.heckel.ntfy.data
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.*
|
||||||
import androidx.lifecycle.asLiveData
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import androidx.lifecycle.map
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
class Repository(private val subscriptionDao: SubscriptionDao, private val notificationDao: NotificationDao) {
|
class Repository(private val subscriptionDao: SubscriptionDao, private val notificationDao: NotificationDao) {
|
||||||
|
private val connectionStates = ConcurrentHashMap<Long, ConnectionState>()
|
||||||
|
private val connectionStatesLiveData = MutableLiveData(connectionStates)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d(TAG, "Created $this")
|
Log.d(TAG, "Created $this")
|
||||||
}
|
}
|
||||||
|
@ -16,7 +17,9 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
||||||
return subscriptionDao
|
return subscriptionDao
|
||||||
.listFlow()
|
.listFlow()
|
||||||
.asLiveData()
|
.asLiveData()
|
||||||
.map { list -> toSubscriptionList(list) }
|
.combineWith(connectionStatesLiveData) { subscriptionsWithMetadata, _ ->
|
||||||
|
toSubscriptionList(subscriptionsWithMetadata.orEmpty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionIdsWithInstantStatusLiveData(): LiveData<Set<Pair<Long, Boolean>>> {
|
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> {
|
private fun toSubscriptionList(list: List<SubscriptionWithMetadata>): List<Subscription> {
|
||||||
return list.map { s ->
|
return list.map { s ->
|
||||||
|
val connectionState = connectionStates.getOrElse(s.id) { ConnectionState.NOT_APPLICABLE }
|
||||||
Subscription(
|
Subscription(
|
||||||
id = s.id,
|
id = s.id,
|
||||||
baseUrl = s.baseUrl,
|
baseUrl = s.baseUrl,
|
||||||
topic = s.topic,
|
topic = s.topic,
|
||||||
instant = s.instant,
|
instant = s.instant,
|
||||||
lastActive = s.lastActive,
|
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,
|
topic = s.topic,
|
||||||
instant = s.instant,
|
instant = s.instant,
|
||||||
lastActive = s.lastActive,
|
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 {
|
companion object {
|
||||||
private val TAG = "NtfyRepository"
|
private const val TAG = "NtfyRepository"
|
||||||
private var instance: Repository? = null
|
private var instance: Repository? = null
|
||||||
|
|
||||||
fun getInstance(subscriptionDao: SubscriptionDao, notificationDao: NotificationDao): Repository {
|
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 androidx.core.app.NotificationCompat
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
|
import io.heckel.ntfy.data.ConnectionState
|
||||||
import io.heckel.ntfy.data.Subscription
|
import io.heckel.ntfy.data.Subscription
|
||||||
import io.heckel.ntfy.data.topicUrl
|
import io.heckel.ntfy.data.topicUrl
|
||||||
import io.heckel.ntfy.ui.MainActivity
|
import io.heckel.ntfy.ui.MainActivity
|
||||||
|
@ -156,7 +157,10 @@ class SubscriberService : Service() {
|
||||||
onNotificationReceived(scope, subscription, n)
|
onNotificationReceived(scope, subscription, n)
|
||||||
}
|
}
|
||||||
val failed = AtomicBoolean(false)
|
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,
|
// Call /json subscribe endpoint and loop until the call fails, is canceled,
|
||||||
// or the job or service are cancelled/stopped
|
// 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)
|
val call = api.subscribe(subscription.id, subscription.baseUrl, subscription.topic, since, notify, fail)
|
||||||
calls[subscription.id] = call
|
calls[subscription.id] = call
|
||||||
while (!failed.get() && !call.isCanceled() && isActive && isServiceStarted) {
|
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")
|
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
|
delay(CONNECTION_LOOP_DELAY_MILLIS) // Resumes immediately if job is cancelled
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "[$url] Connection failed: ${e.message}", e)
|
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)
|
// 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")
|
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) {
|
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.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
|
import io.heckel.ntfy.data.ConnectionState
|
||||||
import io.heckel.ntfy.data.Subscription
|
import io.heckel.ntfy.data.Subscription
|
||||||
import io.heckel.ntfy.data.topicShortUrl
|
import io.heckel.ntfy.data.topicShortUrl
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -52,11 +53,14 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
|
||||||
|
|
||||||
fun bind(subscription: Subscription) {
|
fun bind(subscription: Subscription) {
|
||||||
this.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)
|
context.getString(R.string.main_item_status_text_one, subscription.notifications)
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.main_item_status_text_not_one, subscription.notifications)
|
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) {
|
val dateText = if (subscription.lastActive == 0L) {
|
||||||
""
|
""
|
||||||
} else if (System.currentTimeMillis()/1000 - subscription.lastActive < 24 * 60 * 60) {
|
} else if (System.currentTimeMillis()/1000 - subscription.lastActive < 24 * 60 * 60) {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<!-- Main activity: Action bar -->
|
<!-- Main activity: Action bar -->
|
||||||
<string name="main_action_bar_title">Subscribed topics</string>
|
<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_source_url">https://heckel.io/ntfy-android</string>
|
||||||
<string name="main_menu_website_title">Visit ntfy.sh</string>
|
<string name="main_menu_website_title">Visit ntfy.sh</string>
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@
|
||||||
<!-- Main activity: List and such -->
|
<!-- Main activity: List and such -->
|
||||||
<string name="main_item_status_text_one">%1$d notification</string>
|
<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_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_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_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>
|
<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