Newer
Older
navi-1 / android-client / app / src / main / java / com / navi / client / BackgroundService.kt
package com.navi.client

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat

/**
 * Foreground service that holds the process (and its WebView WebSocket)
 * alive while the app is minimized: partial wake lock + dataSync FGS
 * priority prevent Android from freezing the renderer and dropping the
 * socket. The WebView keeps owning the connection itself — the service
 * only protects the process.
 */
class BackgroundService : Service() {

    private var wakeLock: PowerManager.WakeLock? = null

    override fun onCreate() {
        super.onCreate()
        val nm = getSystemService(NotificationManager::class.java)
        nm.createNotificationChannel(
            NotificationChannel(
                CHANNEL_BG,
                "Navi · фоновый режим",
                NotificationManager.IMPORTANCE_LOW  // silent presence in the shade
            ).apply {
                description = "Держит соединение с сервером открытым, когда приложение свёрнуто"
            }
        )
        nm.createNotificationChannel(
            NotificationChannel(
                CHANNEL_MSG,
                "Navi · сообщения",
                NotificationManager.IMPORTANCE_HIGH
            ).apply {
                description = "Ответы Navi и реакции Synapse"
            }
        )
        startForeground(NOTIF_ID, buildNotification())
        val pm = getSystemService(PowerManager::class.java)
        wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "navi:ws").apply {
            setReferenceCounted(false)
            acquire()
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (intent?.action == ACTION_STOP) {
            stopForeground(STOP_FOREGROUND_REMOVE)
            stopSelf()
            return START_NOT_STICKY
        }
        return START_STICKY
    }

    override fun onDestroy() {
        wakeLock?.takeIf { it.isHeld }?.release()
        wakeLock = null
        super.onDestroy()
    }

    override fun onBind(intent: Intent?): IBinder? = null

    private fun buildNotification(): Notification =
        NotificationCompat.Builder(this, CHANNEL_BG)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle("Navi активна")
            .setContentText("Соединение с сервером удерживается открытым")
            .setOngoing(true)
            // Stop action: tapping kills the service without the activity.
            .addAction(
                0, "Отключить фон",
                android.app.PendingIntent.getService(
                    this, 0,
                    Intent(this, BackgroundService::class.java).setAction(ACTION_STOP),
                    android.app.PendingIntent.FLAG_IMMUTABLE or
                        android.app.PendingIntent.FLAG_UPDATE_CURRENT
                )
            )
            .build()

    companion object {
        const val CHANNEL_BG = "navi_background"
        const val CHANNEL_MSG = "navi_messages"
        const val ACTION_STOP = "com.navi.client.BACKGROUND_STOP"
        private const val NOTIF_ID = 1

        fun start(context: Context) {
            val intent = Intent(context, BackgroundService::class.java)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                context.startForegroundService(intent)
            } else {
                context.startService(intent)
            }
        }

        fun stop(context: Context) {
            context.startService(
                Intent(context, BackgroundService::class.java).setAction(ACTION_STOP)
            )
        }
    }
}