diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index b962782..08bace4 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -12,8 +12,8 @@ android {
applicationId = "com.devlab.app"
minSdk = 26
targetSdk = 37
- versionCode = 11
- versionName = "1.7.2"
+ versionCode = 12
+ versionName = "1.8.0"
}
buildTypes {
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 97e81d7..43c2fba 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,6 +6,8 @@
+
+
+
+
when (event) {
@@ -136,12 +135,6 @@ fun AppRoot() {
var isLoggedIn by remember { mutableStateOf(false) }
var unreadCount by remember { mutableIntStateOf(0) }
- LaunchedEffect(isLoggedIn) {
- if (isLoggedIn) {
- NotificationWorker.checkNow(context)
- }
- }
-
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
diff --git a/android/app/src/main/java/com/devlab/app/ServerManagerApp.kt b/android/app/src/main/java/com/devlab/app/ServerManagerApp.kt
index 7ff58b8..79511f5 100644
--- a/android/app/src/main/java/com/devlab/app/ServerManagerApp.kt
+++ b/android/app/src/main/java/com/devlab/app/ServerManagerApp.kt
@@ -1,6 +1,7 @@
package com.devlab.app
import android.app.Application
+import com.devlab.app.service.NotificationForegroundService
import com.devlab.app.util.NotificationHelper
import com.devlab.app.util.NotificationPoller
import com.devlab.app.worker.NotificationWorker
@@ -14,6 +15,7 @@ class ServerManagerApp : Application() {
instance = this
NotificationHelper.createChannels(this)
NotificationWorker.schedule(this)
+ NotificationForegroundService.start(this)
notificationPoller = NotificationPoller(this)
}
diff --git a/android/app/src/main/java/com/devlab/app/service/NotificationForegroundService.kt b/android/app/src/main/java/com/devlab/app/service/NotificationForegroundService.kt
new file mode 100644
index 0000000..d4e02b0
--- /dev/null
+++ b/android/app/src/main/java/com/devlab/app/service/NotificationForegroundService.kt
@@ -0,0 +1,148 @@
+package com.devlab.app.service
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import com.devlab.app.MainActivity
+import com.devlab.app.R
+import com.devlab.app.data.api.RetrofitClient
+import com.devlab.app.util.NotificationHelper
+import com.devlab.app.util.PreferencesManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import java.util.concurrent.TimeUnit
+
+class NotificationForegroundService : android.app.Service() {
+
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+ private var pollingJob: Job? = null
+ private var lastCheck: Long = 0L
+
+ override fun onCreate() {
+ super.onCreate()
+ Log.d(TAG, "Service created")
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d(TAG, "onStartCommand")
+
+ val notification = buildPersistentNotification()
+ startForeground(NOTIFICATION_ID, notification)
+
+ if (pollingJob?.isActive != true) {
+ lastCheck = System.currentTimeMillis()
+ pollingJob = scope.launch {
+ while (isActive) {
+ try {
+ pollNotifications()
+ } catch (_: Exception) { }
+ delay(POLL_INTERVAL_MS)
+ }
+ }
+ Log.d(TAG, "Polling started every ${POLL_INTERVAL_MS / 1000}s")
+ }
+
+ return START_STICKY
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onDestroy() {
+ super.onDestroy()
+ pollingJob?.cancel()
+ scope.cancel()
+ Log.d(TAG, "Service destroyed")
+ }
+
+ private fun buildPersistentNotification(): Notification {
+ val openIntent = Intent(this, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
+ }
+ val pendingOpen = PendingIntent.getActivity(
+ this, 0, openIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ return NotificationCompat.Builder(this, CHANNEL_SERVICE)
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText("Monitoring for notifications")
+ .setContentIntent(pendingOpen)
+ .setOngoing(true)
+ .setSilent(true)
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .build()
+ }
+
+ private suspend fun pollNotifications() {
+ val prefs = PreferencesManager(this)
+ val token = prefs.getToken()
+ if (token.isBlank()) return
+
+ val now = System.currentTimeMillis()
+
+ RetrofitClient.setToken(token)
+ val service = RetrofitClient.getApiService()
+
+ val response = service.getNotifications(page = 1, perPage = 10)
+ if (!response.success) return
+
+ val newNotifications = response.data.filter { note ->
+ val noteTime = parseTime(note.created_at)
+ noteTime > lastCheck && note.is_read == 0
+ }
+
+ if (newNotifications.isNotEmpty()) {
+ Log.d(TAG, "Found ${newNotifications.size} new notifications")
+ for (notification in newNotifications) {
+ NotificationHelper.showNotification(this, notification)
+ }
+ lastCheck = newNotifications.maxOf { parseTime(it.created_at) }
+ }
+ }
+
+ private fun parseTime(time: String?): Long {
+ if (time == null) return 0L
+ return try {
+ val sdf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US).apply {
+ timeZone = java.util.TimeZone.getTimeZone("UTC")
+ }
+ sdf.parse(time)?.time ?: 0L
+ } catch (_: Exception) {
+ 0L
+ }
+ }
+
+ companion object {
+ private const val TAG = "NotificationFgSvc"
+ private const val NOTIFICATION_ID = 1001
+ const val CHANNEL_SERVICE = "service_status"
+ private const val POLL_INTERVAL_MS = 30_000L
+
+ fun start(context: Context) {
+ val intent = Intent(context, NotificationForegroundService::class.java)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ Log.d(TAG, "Start command sent")
+ }
+
+ fun stop(context: Context) {
+ context.stopService(Intent(context, NotificationForegroundService::class.java))
+ Log.d(TAG, "Stop command sent")
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/devlab/app/util/NotificationHelper.kt b/android/app/src/main/java/com/devlab/app/util/NotificationHelper.kt
index 877f4b2..ae44543 100644
--- a/android/app/src/main/java/com/devlab/app/util/NotificationHelper.kt
+++ b/android/app/src/main/java/com/devlab/app/util/NotificationHelper.kt
@@ -14,11 +14,13 @@ import androidx.core.content.ContextCompat
import com.devlab.app.MainActivity
import com.devlab.app.R
import com.devlab.app.data.model.AppNotification
+import com.devlab.app.service.NotificationForegroundService
object NotificationHelper {
private const val CHANNEL_GENERAL = "notifications_general"
private const val CHANNEL_ALERTS = "notifications_alerts"
+ private const val CHANNEL_SERVICE = "service_status"
fun createChannels(context: Context) {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -37,8 +39,17 @@ object NotificationHelper {
description = "Important alerts requiring attention"
}
+ val service = NotificationChannel(
+ CHANNEL_SERVICE, "Service Status",
+ NotificationManager.IMPORTANCE_MIN
+ ).apply {
+ description = "Background monitoring service indicator"
+ setShowBadge(false)
+ }
+
manager.createNotificationChannel(general)
manager.createNotificationChannel(alerts)
+ manager.createNotificationChannel(service)
}
fun showNotification(context: Context, notification: AppNotification) {
diff --git a/public/sysadmin.apk b/public/sysadmin.apk
index 53888b6..6ce0c72 100644
Binary files a/public/sysadmin.apk and b/public/sysadmin.apk differ