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,