8000 Auto Stop Service when there's no client connected for x minutes · Issue #382 · plainhub/plain-app · GitHub
[go: up one dir, main page]

Skip to content

Auto Stop Service when there's no client connected for x minutes #382

Open

Description

@kingp0dd

can we have Stop Service when no clients are connected for a certain period of time?

diff --git a/shared/src/androidMain/kotlin/com/ismartcoding/plain/app/DesktopAccessIdleStopper.kt b/shared/src/androidMain/kotlin/com/ismartcoding/plain/app/DesktopAccessIdleStopper.kt
new file mode 100644
--- /dev/null
+++ b/shared/src/androidMain/kotlin/com/ismartcoding/plain/app/DesktopAccessIdleStopper.kt
@@ -0,0 +1,80 @@
+package com.ismartcoding.plain.app
+
+import android.annotation.SuppressLint
+import com.ismartcoding.plain.HttpServerManager
+import com.ismartcoding.plain.Prefs
+import com.ismartcoding.plain.appContext
+import com.ismartcoding.plain.features.sessions.onlineClientIds
+import com.ismartcoding.plain.lib.logcat.LogCat
+import com.ismartcoding.plain.platform.system.setDesktopAccessEnabled
+import com.ismartcoding.plain.ui.platform.notifications.NotificationHelper
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.minutes
+
+/**
+ * Turns Desktop Access off after it has had no connected web clients for
+ * [IDLE_TIMEOUT_MINUTES] minutes. Turning Desktop Access off closes any open
+ * websocket sessions and refuses new logins until it is switched back on.
+ *
+ * The timer starts when Desktop Access is on and no client is online
+ * (including right after enabling it), and resets whenever a client connects.
+ */
+internal object DesktopAccessIdleStopper {
+    /** Minutes without any online client before Desktop Access is turned off. 0 disables the feature. */
+    private const val IDLE_TIMEOUT_MINUTES = 10
+
+    /** Also turn off "Keep online in background" so the foreground service and wake lock are released. */
+    private const val ALSO_STOP_BACKGROUND_SERVICE = true
+
+    /** Post a notification when the idle stop happens. */
+    private const val NOTIFY_ON_STOP = true
+
+    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+
+    fun observe() {
+        if (IDLE_TIMEOUT_MINUTES <= 0) return
+        scope.launch {
+            combine(Prefs.desktopAccess, onlineClientIds) { enabled, clients -> enabled && clients.isEmpty() }
+                .distinctUntilChanged()
+                .collectLatest { idle ->
+                    // collectLatest cancels this delay as soon as a client connects
+                    // or Desktop Access is switched off manually.
+                    if (!idle) return@collectLatest
+                    delay(IDLE_TIMEOUT_MINUTES.minutes)
+                    stopForIdle()
+                }
+        }
+    }
+
+    private suspend fun stopForIdle() {
+        try {
+            LogCat.d("Desktop access idle for $IDLE_TIMEOUT_MINUTES min with no clients, turning it off")
+            setDesktopAccessEnabled(false)
+            if (ALSO_STOP_BACKGROUND_SERVICE && Prefs.backgroundServiceEnabled.value) {
+                HttpServerManager.setBackgroundServiceEnabled(false).join()
+            }
+            if (NOTIFY_ON_STOP) notifyStopped()
+        } catch (cancelled: CancellationException) {
+            throw cancelled
+        } catch (error: Exception) {
+            LogCat.e("Desktop access idle stop", error)
+        }
+    }
+
+    @SuppressLint("MissingPermission") // postWebLoginNotification checks POST_NOTIFICATIONS itself
+    private fun notifyStopped() {
+        NotificationHelper.postWebLoginNotification(
+            appContext,
+            "Desktop access turned off",
+            "No web clients were connected for $IDLE_TIMEOUT_MINUTES minutes.",
+        )
+    }
+}
diff --git a/shared/src/androidMain/kotlin/com/ismartcoding/plain/platform/system/AppInitialization.android.kt b/shared/src/androidMain/kotlin/com/ismartcoding/plain/platform/system/AppInitialization.android.kt
--- a/shared/src/androidMain/kotlin/com/ismartcoding/plain/platform/system/AppInitialization.android.kt
+++ b/shared/src/androidMain/kotlin/com/ismartcoding/plain/platform/system/AppInitialization.android.kt
@@ -1,6 +1,7 @@
 package com.ismartcoding.plain.platform.system
 
 import com.ismartcoding.plain.NetworkMonitor
+import com.ismartcoding.plain.app.DesktopAccessIdleStopper
 import com.ismartcoding.plain.app.HttpServerForegroundServiceManager
 import com.ismartcoding.plain.appContext
 import com.ismartcoding.plain.Prefs
@@ -11,4 +12,5 @@ internal actual fun initializePlatformApp() {
     setDarkMode(Prefs.darkTheme.value)
     HttpServerForegroundServiceManager.observe()
     HttpServerResources.observe()
+    DesktopAccessIdleStopper.observe()
 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    0