From c39bc5e4855a2823d4c12dea52b92ef9aaef5172 Mon Sep 17 00:00:00 2001 From: Jens Reinemann Date: Mon, 18 May 2026 13:45:06 +0200 Subject: [PATCH] feat: foreground service for background message notifications --- app/src/main/AndroidManifest.xml | 7 ++ .../bollwerk/app/service/MessagingService.kt | 88 +++++++++++++++++++ .../java/de/bollwerk/app/ui/MainViewModel.kt | 7 ++ .../app/ui/settings/SettingsViewModel.kt | 2 + 4 files changed, 104 insertions(+) create mode 100644 app/src/main/java/de/bollwerk/app/service/MessagingService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad866f8..a6ea6c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + + + diff --git a/app/src/main/java/de/bollwerk/app/service/MessagingService.kt b/app/src/main/java/de/bollwerk/app/service/MessagingService.kt new file mode 100644 index 0000000..f24643f --- /dev/null +++ b/app/src/main/java/de/bollwerk/app/service/MessagingService.kt @@ -0,0 +1,88 @@ +package de.bollwerk.app.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.IBinder +import androidx.core.app.NotificationCompat +import de.bollwerk.app.MainActivity +import de.bollwerk.app.R + +/** + * Foreground Service der den Prozess am Leben hält, damit die WebSocket-Verbindung + * auch im Hintergrund bestehen bleibt und Nachrichten empfangen werden können. + */ +class MessagingService : Service() { + + override fun onCreate() { + super.onCreate() + createServiceChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + ACTION_STOP -> { + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + return START_NOT_STICKY + } + } + startForeground(NOTIFICATION_ID, buildNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + return START_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null + + private fun createServiceChannel() { + val channel = NotificationChannel( + CHANNEL_ID, + "Verbindung", + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "Hält die Verbindung zum Server für Nachrichten aufrecht" + setShowBadge(false) + } + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(channel) + } + + private fun buildNotification(): Notification { + val openIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification_message) + .setContentTitle("Bollwerk") + .setContentText("Verbunden – Nachrichten werden empfangen") + .setOngoing(true) + .setContentIntent(openIntent) + .build() + } + + companion object { + private const val CHANNEL_ID = "bollwerk_service" + private const val NOTIFICATION_ID = 9001 + const val ACTION_STOP = "de.bollwerk.app.STOP_MESSAGING_SERVICE" + + fun start(context: Context) { + val intent = Intent(context, MessagingService::class.java) + context.startForegroundService(intent) + } + + fun stop(context: Context) { + val intent = Intent(context, MessagingService::class.java).apply { + action = ACTION_STOP + } + context.startService(intent) + } + } +} diff --git a/app/src/main/java/de/bollwerk/app/ui/MainViewModel.kt b/app/src/main/java/de/bollwerk/app/ui/MainViewModel.kt index 92208fd..323aa4d 100644 --- a/app/src/main/java/de/bollwerk/app/ui/MainViewModel.kt +++ b/app/src/main/java/de/bollwerk/app/ui/MainViewModel.kt @@ -1,9 +1,11 @@ package de.bollwerk.app.ui +import android.content.Context import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import de.bollwerk.app.data.sync.WebSocketClient import de.bollwerk.app.data.sync.WebSocketEvent import de.bollwerk.app.domain.AuthEventBus @@ -11,6 +13,7 @@ import de.bollwerk.app.domain.model.SettingsKey.StringKey import de.bollwerk.app.domain.repository.ImportExportRepository import de.bollwerk.app.domain.repository.SettingsRepository import de.bollwerk.app.domain.repository.SyncService +import de.bollwerk.app.service.MessagingService import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -25,6 +28,7 @@ internal class MainViewModel @Inject constructor( private val webSocketClient: WebSocketClient, private val settingsRepository: SettingsRepository, private val importExportRepository: ImportExportRepository, + @ApplicationContext private val context: Context ) : ViewModel() { private val _navigateToSettings = MutableSharedFlow(extraBufferCapacity = 1) @@ -45,6 +49,7 @@ internal class MainViewModel @Inject constructor( if (token.isNotBlank() && serverUrl.isNotBlank()) { Log.i(TAG, "App-Start: Token vorhanden – verbinde WebSocket") webSocketClient.connect(serverUrl, token) + MessagingService.start(context) } else { Log.d(TAG, "App-Start: Kein Token – kein WebSocket") } @@ -57,6 +62,7 @@ internal class MainViewModel @Inject constructor( authEventBus.loginSuccess.collect { (serverUrl, token) -> Log.i(TAG, "Login erfolgreich – verbinde WebSocket") webSocketClient.connect(serverUrl, token) + MessagingService.start(context) } } } @@ -68,6 +74,7 @@ internal class MainViewModel @Inject constructor( Log.w(TAG, "Session abgelaufen – Forced-Logout") syncService.logout() webSocketClient.disconnect() + MessagingService.stop(context) _navigateToSettings.emit(Unit) } } diff --git a/app/src/main/java/de/bollwerk/app/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/bollwerk/app/ui/settings/SettingsViewModel.kt index 5e52629..7d4d7b9 100644 --- a/app/src/main/java/de/bollwerk/app/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/bollwerk/app/ui/settings/SettingsViewModel.kt @@ -21,6 +21,7 @@ import de.bollwerk.app.domain.model.toJson import de.bollwerk.app.domain.repository.ImportExportRepository import de.bollwerk.app.domain.repository.SettingsRepository import de.bollwerk.app.domain.repository.SyncService +import de.bollwerk.app.service.MessagingService import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -230,6 +231,7 @@ internal class SettingsViewModel @Inject constructor( viewModelScope.launch { syncService.logout() webSocketClient.disconnect() + MessagingService.stop(context) _uiState.update { it.copy( isLoggedIn = false,