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))
}
}
}