From 2fdcae6ae334821281690ff24032d4fbe424d6ef Mon Sep 17 00:00:00 2001 From: OneWay Date: Sun, 11 Oct 2026 22:22:08 +0800 Subject: [PATCH] fix(notifications): throttle dictionary progress reposts A fast download reposted the ongoing notification several times per second. Publish stage changes immediately and rate-limit intermediate updates to the latest snapshot. --- .../progress/DictionaryProgressPresenter.kt | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/example/research/common/progress/DictionaryProgressPresenter.kt b/app/src/main/java/com/example/research/common/progress/DictionaryProgressPresenter.kt index 5d8c8e8..96cb82a 100644 --- a/app/src/main/java/com/example/research/common/progress/DictionaryProgressPresenter.kt +++ b/app/src/main/java/com/example/research/common/progress/DictionaryProgressPresenter.kt @@ -1,37 +1,86 @@ package com.example.research.common.progress +import android.os.SystemClock import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds class DictionaryProgressPresenter( - scope: CoroutineScope, + private val scope: CoroutineScope, private val progressSnapshot: StateFlow, private val notifications: NotificationPort, - mainDispatcher: CoroutineDispatcher = Dispatchers.Main, + private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main, ) : PipelineTerminalUi { + + private companion object { + val MIN_NOTIFICATION_INTERVAL = 500.milliseconds + } + @Volatile private var terminalActive = false + private var lastShownTitleRes: Int = 0 + private var lastEmitMs: Long = 0L + private var pendingSnapshot: DictionaryProgressModel.Snapshot? = null + private var flushJob: Job? = null + init { scope.launch(mainDispatcher) { progressSnapshot.collect { snapshot -> when { snapshot == null -> { + resetThrottle() if (!terminalActive && notifications.hasActiveProgressNotification()) { notifications.cancelProgressNotification() } } - !terminalActive -> notifications.showProgress(snapshot.percent, snapshot.titleRes) + !terminalActive -> emitThrottled(snapshot) } } } } + private fun emitThrottled(snapshot: DictionaryProgressModel.Snapshot) { + val now = SystemClock.elapsedRealtime() + val stageChanged = snapshot.titleRes != lastShownTitleRes + if (stageChanged || (now - lastEmitMs).milliseconds >= MIN_NOTIFICATION_INTERVAL) { + flushJob?.cancel() + pendingSnapshot = null + publish(snapshot, now) + } else { + pendingSnapshot = snapshot + if (flushJob?.isActive != true) { + flushJob = scope.launch(mainDispatcher) { + delay(MIN_NOTIFICATION_INTERVAL - (now - lastEmitMs).milliseconds) + pendingSnapshot?.let { publish(it, SystemClock.elapsedRealtime()) } + pendingSnapshot = null + } + } + } + } + + private fun publish(snapshot: DictionaryProgressModel.Snapshot, now: Long) { + if (terminalActive) return + lastShownTitleRes = snapshot.titleRes + lastEmitMs = now + notifications.showProgress(snapshot.percent, snapshot.titleRes) + } + + private fun resetThrottle() { + flushJob?.cancel() + pendingSnapshot = null + lastShownTitleRes = 0 + lastEmitMs = 0L + } + fun beginFlow() { terminalActive = false + resetThrottle() notifications.cancelTerminalNotification() } @@ -52,6 +101,7 @@ class DictionaryProgressPresenter( override fun cancelProgress() { terminalActive = false + resetThrottle() notifications.cancelProgressNotification() } }