我每秒更新一条通知。
@AndroidEntryPoint
class StopwatchService : Service() {
@Inject
lateinit var stopwatch: Stopwatch
// scope
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job) // which dispatcher to use?
// notifications
private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
private val notificationBuilder by lazy {
createNotificationBuilder(title = stopwatch.laps.size.let { if (it > 0) "Lap ${it + 1}" else "" })
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.action.let {
when (it) {
Actions.START_SERVICE.name -> {
startForeground(
ONGOING_NOTIFICATION_ID, notificationBuilder.build()
)
scope.launch {
Log.d("stopwatch-service", "state LAUNCH")
stopwatch.eventFlow.collectLatest {
Log.d("stopwatch-service", "state ${it.toString()}")
when (it) {
Stopwatch.Event.START -> {
notificationBuilder.clearActions()
...
}
Stopwatch.Event.STOP -> {
notificationBuilder.clearActions()
...
}
Stopwatch.Event.RESET -> {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
}
}
scope.launch {
Log.d("stopwatch-service", "update LAUNCH")
stopwatch.elapsedTimeFlow(
refreshRate = 100,
precision = 1000,
format = ::formatNotification,
uiScope = scope,
updateUI = false
).collectLatest {
Log.d("stopwatch-service", "update $it")
notificationManager.notify(
ONGOING_NOTIFICATION_ID,
notificationBuilder.apply { setContentText(it) }.build()
)
}
}
}
}
}
Log.d("stopwatch-service", "onStartCommand ${intent.toString()}")
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
Log.d("stopwatch-service", "onDestroy")
job.cancel()
}
state LAUNCH
onStartCommand Intent { act=START_SERVICE cmp=com.uzential.stopwatch/.services.StopwatchService }
update LAUNCH
state START
update 00:01
update 00:02
update 00:03
...
update 00:28
update 00:29
update 00:30
update 00:31
update 01:48
update 01:49
update 01:50
update 01:51
...
update 02:06
update 02:07
update 02:08
update 02:09
update 04:08
update 04:09
update 04:10
update 04:11
即使通知可见,协程也会暂停一分钟(直到我按下操作按钮)。
基于this评论,我在应用程序信息->电池使用中启用了“允许后台活动”,这似乎解决了问题。我很惊讶这必须为前台服务完成。更新通知费用高吗? (
notificationManager.notify
只是更改内容文本字段,但在通知可见之前不会执行任何 UI 计算)
我使用
Dispatchers.Default
来避免使用主线程。
fun Stopwatch.elapsedTimeFlow(
uiScope: CoroutineScope,
format: (millis: Long) -> String,
refreshRate: Long = 100,
precision: Long = 1000,
updateUI : Boolean = true,
dispatcher: CoroutineDispatcher = Dispatchers.Default
): Flow<String> {
// flow
val elapsedTimeFlow = MutableStateFlow(elapsedTime)
suspend fun refreshLoop() {
while (true) {
elapsedTimeFlow.value = elapsedTime
delay(refreshRate)
}
}
// listen to changes to isRunning
uiScope.launch(dispatcher) {
val job = AtomicReference<Job?>(null)
eventFlow.collectLatest {
when (it) {
// launch refresh loop
Stopwatch.Event.START -> {
launch {
refreshLoop()
}.also(job::set)
}
// cancel refresh loop
Stopwatch.Event.STOP, Stopwatch.Event.RESET -> {
job.get()?.cancel()
// update UI one last time
if(updateUI) elapsedTimeFlow.emit(elapsedTime)
}
}
}
}
// UI flow
return elapsedTimeFlow
.map { it / precision }
.distinctUntilChanged()
.map { format(elapsedTime) }
.flowOn(dispatcher)
}
电池优化无法以编程方式禁用。是否还有其他可以做的事情,例如仅在通知不可见时才允许暂停协程?
编辑:简单的日志也会发生同样的事情。
Actions.START_SERVICE.name -> {
startForeground(
ONGOING_NOTIFICATION_ID, notificationBuilder.build()
)
scope.launch {
while(true){
Log.d("stopwatch-service", "LOG ${stopwatch.elapsedTime}")
delay(1000)
}
LOG 28559
LOG 29565
LOG 161978 (after clicking on the notification)
编辑 2:计时器也停止。
val timerTask = object : TimerTask() {
override fun run() {
Log.d("stopwatch-service", "LOG ${stopwatch.elapsedTime}")
}
}
timer.schedule(timerTask, 0, 1000)
LOG 28816
LOG 29817
LOG 46124 (clicked on notification)
前台服务本身似乎无论如何都会继续运行,所以我做了这样的解决方案:
override fun onCreate() {
...
Thread {
run {
// Hack to attempt to restart cancelled routines due to Android's unconfigurable battery management
while (true) {
Thread.sleep(10000)
routineMap.forEach { (_, job) ->
if(job.isCancelled) {
job.start()
}
}
}
}
}.start();
...
}
前台服务启动一个线程,该线程只是重新启动被取消的作业。看起来效果很好。唯一需要注意的是,您需要跟踪您希望继续运行的作业。