feat(notification): Turn off action and tap-to-open on background notification (#1408)

This commit is contained in:
m57 authored and GitHub committed 2026-09-30 13:30:55 +03:00
1 parent 48de25985a
commit 05519ba1ff
9 files changed
+139 -9

No files matched your search

+4
View File
@@ -71,6 +71,10 @@
android:name=".notification.NotificationReplyReceiver"
android:exported="false" />
<receiver
android:name=".notification.StopKeepConnectedReceiver"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@@ -98,6 +98,17 @@ class BackgroundConnectionController(
}
}
/** The persistent opt-in was turned off while backgrounded (e.g. from the notification action). */
fun onKeepConnectedDisabled() {
val snapshot = snapshotProvider(false)
if (!snapshot.appInForeground) {
releaseLease()
}
// Force a re-post if the service stays up (pending reply) so the action button disappears.
currentNotificationState = BackgroundNotificationState.None
reconcileState()
}
fun reconcileState() {
val snapshot = snapshotProvider(false)
val decision = BackgroundConnectionPolicy.evaluate(snapshot)
@@ -13,6 +13,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.RemoteInput
import com.m57.hermescontrol.MainActivity
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.NetworkMonitor
import com.m57.hermescontrol.data.session.ActiveSessionHolder
import com.m57.hermescontrol.data.ws.HermesWsClient
@@ -383,15 +384,48 @@ class ChatNotificationService : Service() {
)
}
private fun buildForegroundNotification(text: String): Notification =
NotificationCompat
.Builder(this, SERVICE_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(getString(R.string.notif_title))
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setOngoing(true)
.build()
private fun buildForegroundNotification(text: String): Notification {
val openIntent =
Intent(this, MainActivity::class.java).apply {
setPackage(packageName)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val builder =
NotificationCompat
.Builder(this, SERVICE_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(getString(R.string.notif_title))
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setOngoing(true)
.setContentIntent(
PendingIntent.getActivity(
this,
0,
openIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
),
)
if (AuthManager.isKeepConnectedInBackground()) {
val stopIntent =
Intent(this, StopKeepConnectedReceiver::class.java).apply {
action = StopKeepConnectedReceiver.ACTION_STOP_KEEP_CONNECTED
setPackage(packageName)
}
builder.addAction(
R.drawable.ic_notification,
getString(R.string.notif_action_turn_off),
PendingIntent.getBroadcast(
this,
0,
stopIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
),
)
}
return builder.build()
}
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -0,0 +1,26 @@
package com.m57.hermescontrol.notification
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.m57.hermescontrol.data.local.AuthManager
/**
* Handles the "Turn off" action on the ongoing background notification:
* disables "Stay connected in background" and lets the controller retire the
* lease/service.
*/
class StopKeepConnectedReceiver : BroadcastReceiver() {
companion object {
const val ACTION_STOP_KEEP_CONNECTED = "com.m57.hermescontrol.ACTION_STOP_KEEP_CONNECTED"
}
override fun onReceive(
context: Context,
intent: Intent,
) {
if (intent.action != ACTION_STOP_KEEP_CONNECTED) return
AuthManager.setKeepConnectedInBackground(false)
BackgroundConnectionController.default.onKeepConnectedDisabled()
}
}
+1
View File
@@ -129,6 +129,7 @@
<string name="notif_title">Hermes</string>
<string name="notif_waiting_replies">بانتظار ردود Hermes</string>
<string name="notif_connected_in_background">متصل في الخلفية</string>
<string name="notif_action_turn_off">إيقاف</string>
<string name="notif_connecting">جارٍ الاتصال…</string>
<string name="notif_waiting_network">في انتظار الشبكة</string>
<string name="notif_reconnecting">جارٍ إعادة الاتصال</string>
+1
View File
@@ -126,6 +126,7 @@
<string name="notif_title">헤르메스</string>
<string name="notif_waiting_replies">헤르메스 응답 대기 중</string>
<string name="notif_connected_in_background">백그라운드에서 연결됨</string>
<string name="notif_action_turn_off">끄기</string>
<string name="notif_connecting">연결 중…</string>
<string name="notif_waiting_network">네트워크 대기 중</string>
<string name="notif_reconnecting">재연결 중</string>
+1
View File
@@ -129,6 +129,7 @@
<string name="notif_title">Hermes</string>
<string name="notif_waiting_replies">等待 Hermes 回复</string>
<string name="notif_connected_in_background">在后台保持连接</string>
<string name="notif_action_turn_off">关闭</string>
<string name="notif_connecting">正在连接…</string>
<string name="notif_waiting_network">等待网络连接</string>
<string name="notif_reconnecting">正在重新连接</string>
+1
View File
@@ -152,6 +152,7 @@
<string name="notif_title">Hermes</string>
<string name="notif_waiting_replies">Waiting for Hermes replies</string>
<string name="notif_connected_in_background">Connected in background</string>
<string name="notif_action_turn_off">Turn off</string>
<string name="notif_connecting">Connecting…</string>
<string name="notif_waiting_network">Waiting for network</string>
<string name="notif_reconnecting">Reconnecting</string>
@@ -407,4 +407,55 @@ class BackgroundConnectionControllerTest {
unmockkObject(NetworkMonitor)
}
}
@Test
fun testOnKeepConnectedDisabled_whenBackgrounded_releasesLeaseAndStopsService() {
var leaseReleased = false
var serviceStopped = false
val snapshot =
BackgroundConnectionSnapshot(
appInForeground = false,
keepConnectedOptIn = false,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
)
val controller =
BackgroundConnectionController(
snapshotProvider = { snapshot },
releaseLease = { leaseReleased = true },
requestServiceStop = { serviceStopped = true },
)
controller.onKeepConnectedDisabled()
assertTrue(leaseReleased)
assertTrue(serviceStopped)
}
@Test
fun testOnKeepConnectedDisabled_whenReplyPending_keepsServiceAndRefreshesNotification() {
var serviceStopped = false
var refreshed: BackgroundNotificationState? = null
val snapshot =
BackgroundConnectionSnapshot(
appInForeground = false,
keepConnectedOptIn = false,
pendingReply = true,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
)
val controller =
BackgroundConnectionController(
snapshotProvider = { snapshot },
releaseLease = {},
requestServiceStop = { serviceStopped = true },
onNotificationStateChanged = { refreshed = it },
)
controller.onKeepConnectedDisabled()
assertFalse(serviceStopped)
assertEquals(BackgroundNotificationState.WaitingForReplies, refreshed)
}
}