跳转至

apps\hub\src\main\java\cn\nfarms\hub\HubService.kt

源码路径:apps\hub\src\main\java\cn\nfarms\hub\HubService.kt · 123 行

package cn.nfarms.hub

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.IBinder
import androidx.core.app.NotificationCompat
import cn.nfarms.hub.task.TaskState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

/**
 * 前台服务:让「下载 17 GB 权重」「跑一个小时的转码」在切换到别的 App 之后继续跑。
 *
 * # 为什么必须有它
 *
 * Android 8+ 对后台进程有严格限制:Activity 一进后台,普通线程随时可能被挂起。
 * 长任务不挂前台服务,症状是「切出去再回来,进度停在那里不动」——
 * 而且**不会报错**,只会静默变慢。所以只要 HUB 在干活,就一定要有这条通知。
 *
 * # 和 UI 的关系
 *
 * 服务不持有 ViewModel,只观察 [TaskScheduler.tasks] 并把「当前队列概况」
 * 同步到通知上。这样即使 UI 被系统回收,任务照跑、通知照更新。
 */
class HubService : Service() {

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
    private lateinit var container: Container

    override fun onCreate() {
        super.onCreate()
        container = (application as HubApp).container
        createChannel()
        startForeground(NOTIFICATION_ID, buildNotification("准备中", "启动中", -1f))
        scope.launch {
            container.scheduler.tasks.collect { list ->
                val running = list.firstOrNull { it.state == TaskState.Running }
                val queued = list.count { it.state == TaskState.Queued }
                val done = list.count { it.state == TaskState.Done }
                val failed = list.count { it.state == TaskState.Failed }
                val title = when {
                    running != null -> running.title
                    queued > 0 -> "排队中($queued)"
                    else -> "空闲"
                }
                val text = buildString {
                    if (running != null) {
                        if (running.progress >= 0f) append("${(running.progress * 100).toInt()}% · ")
                        append(running.detail)
                    } else {
                        append("已完成 $done")
                    }
                    if (failed > 0) append(" · 失败 $failed")
                }
                val progress = running?.progress ?: -1f
                notify(title, text, progress)
            }
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // START_STICKY:被系统杀掉后重启。任务历史在盘上,重启后 UI 能重新看到它。
        return START_STICKY
    }

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

    override fun onDestroy() {
        scope.cancel()
        super.onDestroy()
    }

    private fun createChannel() {
        val mgr = getSystemService(NotificationManager::class.java)
        val ch = NotificationChannel(
            CHANNEL_ID,
            "HUB 任务",
            NotificationManager.IMPORTANCE_LOW, // LOW:不出声、不弹窗,只挂在通知栏
        ).apply { description = "下载、转码、harness 的进度" }
        mgr.createNotificationChannel(ch)
    }

    private fun buildNotification(title: String, text: String, progress: Float): Notification {
        val builder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle(title)
            .setContentText(text)
            .setSmallIcon(android.R.drawable.stat_sys_download)
            .setOngoing(true)
            .setOnlyAlertOnce(true)
        if (progress in 0f..1f) {
            builder.setProgress(100, (progress * 100).toInt(), false)
        } else {
            builder.setProgress(0, 0, true)
        }
        return builder.build()
    }

    private fun notify(title: String, text: String, progress: Float) {
        getSystemService(NotificationManager::class.java)
            .notify(NOTIFICATION_ID, buildNotification(title, text, progress))
    }

    companion object {
        private const val CHANNEL_ID = "nfarms-hub-tasks"
        private const val NOTIFICATION_ID = 4101

        fun start(ctx: Context) {
            ctx.startForegroundService(Intent(ctx, HubService::class.java))
        }

        fun stop(ctx: Context) {
            ctx.stopService(Intent(ctx, HubService::class.java))
        }
    }
}