Fix space-to-space switching, hide space rooms at Home, harden ntfy reconnect
Sonar / Sonar Quality Checks (push) Has been cancelled
Code Quality Checks / Search for forbidden patterns (push) Has been cancelled
Code Quality Checks / Search for invalid screenshot files (push) Has been cancelled
Code Quality Checks / Search for invalid dependencies (push) Has been cancelled
Code Quality Checks / Konsist tests (push) Has been cancelled
Code Quality Checks / Compose tests (push) Has been cancelled
Code Quality Checks / Android lint check (push) Has been cancelled
Code Quality Checks / Detekt checks (push) Has been cancelled
Code Quality Checks / Ktlint checks (push) Has been cancelled
Code Quality Checks / Doc checks (push) Has been cancelled
Code Quality Checks / Check shell scripts (push) Has been cancelled
Code Quality Checks / Run zizmor (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle (Gplay) (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle Enterprise (push) Has been cancelled
Create release App Bundle and APKs / Create APKs (FDroid) (push) Has been cancelled
Test / Runs unit tests (push) Has been cancelled
Code Quality Checks / Project Check Suite (push) Has been cancelled
Sonar / Sonar Quality Checks (push) Has been cancelled
Code Quality Checks / Search for forbidden patterns (push) Has been cancelled
Code Quality Checks / Search for invalid screenshot files (push) Has been cancelled
Code Quality Checks / Search for invalid dependencies (push) Has been cancelled
Code Quality Checks / Konsist tests (push) Has been cancelled
Code Quality Checks / Compose tests (push) Has been cancelled
Code Quality Checks / Android lint check (push) Has been cancelled
Code Quality Checks / Detekt checks (push) Has been cancelled
Code Quality Checks / Ktlint checks (push) Has been cancelled
Code Quality Checks / Doc checks (push) Has been cancelled
Code Quality Checks / Check shell scripts (push) Has been cancelled
Code Quality Checks / Run zizmor (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle (Gplay) (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle Enterprise (push) Has been cancelled
Create release App Bundle and APKs / Create APKs (FDroid) (push) Has been cancelled
Test / Runs unit tests (push) Has been cancelled
Code Quality Checks / Project Check Suite (push) Has been cancelled
Space rail: selecting a space while another is selected kept serving the old filter (unkeyed remember in SpaceFiltersPresenter) — key it on the space id. Home: track child lists of all spaces (not just the selected one) and hide space-owned rooms at Home, Discord-style. Filter chips still search everything; the owned set is persisted so Home is clean on cold start. Voice rooms now classify in every space without visiting it first. ntfy: reconnect immediately when the default network returns instead of waiting out up to 5 minutes of backoff, and surface the socket state (Connected/Reconnecting) in the foreground-service notification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+50
-11
@@ -14,6 +14,8 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Base64
|
||||
@@ -35,10 +37,11 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -76,6 +79,12 @@ class NtfySubscriberService : Service() {
|
||||
private var connectionJob: Job? = null
|
||||
private var currentWebSocket: WebSocket? = null
|
||||
|
||||
// Fired when the default network (re)appears, so the connect loop skips whatever is
|
||||
// left of its backoff delay — a Doze exit or wifi↔cellular switch otherwise leaves
|
||||
// push dead for up to MAX_BACKOFF_MS.
|
||||
private val reconnectKick = Channel<Unit>(Channel.CONFLATED)
|
||||
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
private val okHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
@@ -90,6 +99,20 @@ class NtfySubscriberService : Service() {
|
||||
super.onCreate()
|
||||
bindings<NtfySubscriberServiceBindings>().inject(this)
|
||||
serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
reconnectKick.trySend(Unit)
|
||||
// A socket that survived the network change keeps working; a half-dead one
|
||||
// gets torn down here so the loop reconnects immediately (since= resumes).
|
||||
currentWebSocket?.close(NORMAL_CLOSURE, "network changed")
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
getSystemService<ConnectivityManager>()?.registerDefaultNetworkCallback(callback)
|
||||
networkCallback = callback
|
||||
}.onFailure {
|
||||
Timber.tag(loggerTag.value).w("Could not register network callback: ${it.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -104,6 +127,9 @@ class NtfySubscriberService : Service() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
networkCallback?.let { callback ->
|
||||
runCatching { getSystemService<ConnectivityManager>()?.unregisterNetworkCallback(callback) }
|
||||
}
|
||||
currentWebSocket?.close(NORMAL_CLOSURE, "service destroyed")
|
||||
serviceScope?.cancel()
|
||||
super.onDestroy()
|
||||
@@ -128,8 +154,10 @@ class NtfySubscriberService : Service() {
|
||||
}
|
||||
Timber.tag(loggerTag.value).d("Connecting to ntfy (${topics.size} topics)")
|
||||
val hadMessages = runWebSocket(url)
|
||||
updateNotification("Reconnecting…")
|
||||
backoffMs = if (hadMessages) INITIAL_BACKOFF_MS else (backoffMs * 2).coerceAtMost(MAX_BACKOFF_MS)
|
||||
delay(backoffMs)
|
||||
// A network-available kick cuts the wait short so Doze exits reconnect instantly.
|
||||
withTimeoutOrNull(backoffMs) { reconnectKick.receive() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +169,7 @@ class NtfySubscriberService : Service() {
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
opened = true
|
||||
Timber.tag(loggerTag.value).d("Connected")
|
||||
updateNotification("Connected to ${ntfyStore.getServerUrl().removePrefix("https://").removePrefix("http://")}")
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
@@ -203,6 +232,24 @@ class NtfySubscriberService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(contentText: String): Notification {
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_blap_push)
|
||||
.setContentTitle("Blap")
|
||||
.setContentText(contentText)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.build()
|
||||
}
|
||||
|
||||
/** Surfaces the socket state in the (minimized) FGS notification — the only way to see
|
||||
* on-device whether push is actually connected without pulling logs. */
|
||||
private fun updateNotification(contentText: String) {
|
||||
getSystemService<NotificationManager>()?.notify(NOTIFICATION_ID, buildNotification(contentText))
|
||||
}
|
||||
|
||||
private fun startAsForeground() {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
@@ -213,20 +260,12 @@ class NtfySubscriberService : Service() {
|
||||
setShowBadge(false)
|
||||
}
|
||||
getSystemService<NotificationManager>()?.createNotificationChannel(channel)
|
||||
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_blap_push)
|
||||
.setContentTitle("Blap")
|
||||
.setContentText("Listening for messages")
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.build()
|
||||
val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
|
||||
} else {
|
||||
0
|
||||
}
|
||||
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, type)
|
||||
ServiceCompat.startForeground(this, NOTIFICATION_ID, buildNotification("Connecting…"), type)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
Reference in New Issue
Block a user