Introduce RealPingEvent and refactor ping flow

Add RealPingEvent sealed class and refactor real-ping flow to use event-driven communication. RealPingWorkerService now emits Progress/Result/Finish events instead of direct callbacks/MessageUtil calls. CoreTestService reorganized into handlers (handleMeasureStart/handleMeasureCancel/handleWorkerEvent) to process events, update notifications, persist delays via MmkvManager, and notify UI. MainViewModel adjusted to read the result content as a string when handling success messages. Overall change decouples worker logic from service responsibilities and simplifies progress/result handling.
This commit is contained in:
2dust
2026-05-03 16:46:40 +08:00
parent d606453b83
commit fc4ab733c8
4 changed files with 91 additions and 78 deletions
@@ -0,0 +1,14 @@
package com.v2ray.ang.dto
sealed class RealPingEvent {
/** Periodic progress update while the batch is still running. */
data class Progress(val text: String) : RealPingEvent()
/** A single server result is available. */
data class Result(val guid: String, val delayMillis: Long) : RealPingEvent()
/** The entire batch has finished or been cancelled. */
data class Finish(val status: String) : RealPingEvent()
}
@@ -5,6 +5,7 @@ import android.content.Intent
import android.os.IBinder
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.RealPingEvent
import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.handler.MmkvManager
@@ -65,73 +66,75 @@ class CoreTestService : Service() {
}
when (message.key) {
AppConfig.MSG_MEASURE_CONFIG_START -> {
LogUtil.i(AppConfig.TAG, "CoreTestService starting worker subscription ${message.subscriptionId}")
// start foreground immediately to satisfy startForegroundService timing requirement
NotificationHelper.startForeground(
this,
NotificationChannelType.CORE_TEST,
getString(R.string.app_name),
getString(R.string.title_real_ping_all_server)
)
val guidsList = if (message.serverGuids.isNotEmpty()) {
message.serverGuids
} else if (message.subscriptionId.isNotEmpty()) {
MmkvManager.decodeServerList(message.subscriptionId)
} else {
MmkvManager.decodeAllServerList()
}
if (guidsList.isNotEmpty()) {
lateinit var worker: RealPingWorkerService
worker = RealPingWorkerService(
context = this,
guids = guidsList,
onProgress = { progress ->
NotificationHelper.updateNotification(
channelType = NotificationChannelType.CORE_TEST,
context = this,
content = getString(R.string.connection_runing_task_left, progress)
)
// notify UI about progress updates
MessageUtil.sendMsg2UI(this@CoreTestService, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, progress)
},
onFinish = { status ->
// notify UI and remove the worker from active list when finished
MessageUtil.sendMsg2UI(this@CoreTestService, AppConfig.MSG_MEASURE_CONFIG_FINISH, status)
activeWorkers.remove(worker)
if (activeWorkers.isEmpty()) {
NotificationHelper.stopForeground(this@CoreTestService)
stopSelf()
}
}
)
activeWorkers.add(worker)
worker.start()
} else {
NotificationHelper.stopForeground(this)
stopSelf(startId)
}
}
AppConfig.MSG_MEASURE_CONFIG_CANCEL -> {
LogUtil.i(AppConfig.TAG, "CoreTestService received cancel message, cancelling ${activeWorkers.size} active workers")
// cancel all running batch workers independently
val snapshot = ArrayList(activeWorkers)
snapshot.forEach { it.cancel() }
activeWorkers.clear()
NotificationHelper.stopForeground(this)
stopSelf()
}
else -> {
NotificationHelper.stopForeground(this)
stopSelf(startId)
}
AppConfig.MSG_MEASURE_CONFIG_START -> handleMeasureStart(message, startId)
AppConfig.MSG_MEASURE_CONFIG_CANCEL -> handleMeasureCancel()
else -> { NotificationHelper.stopForeground(this); stopSelf(startId) }
}
return START_NOT_STICKY
}
private fun handleMeasureStart(message: TestServiceMessage, startId: Int) {
LogUtil.i(AppConfig.TAG, "CoreTestService starting worker subscription ${message.subscriptionId}")
NotificationHelper.startForeground(
this,
NotificationChannelType.CORE_TEST,
getString(R.string.app_name),
getString(R.string.title_real_ping_all_server)
)
val guidsList = when {
message.serverGuids.isNotEmpty() -> message.serverGuids
message.subscriptionId.isNotEmpty() -> MmkvManager.decodeServerList(message.subscriptionId)
else -> MmkvManager.decodeAllServerList()
}
if (guidsList.isNotEmpty()) {
lateinit var worker: RealPingWorkerService
worker = RealPingWorkerService(
context = this,
guids = guidsList,
onEvent = { event -> handleWorkerEvent(event) { activeWorkers.remove(worker) } }
)
activeWorkers.add(worker)
worker.start()
} else {
NotificationHelper.stopForeground(this)
stopSelf(startId)
}
}
private fun handleWorkerEvent(event: RealPingEvent, onWorkerDone: () -> Unit) {
when (event) {
is RealPingEvent.Progress -> {
NotificationHelper.updateNotification(
channelType = NotificationChannelType.CORE_TEST,
context = this,
content = getString(R.string.connection_runing_task_left, event.text)
)
MessageUtil.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, event.text)
}
is RealPingEvent.Result -> {
MmkvManager.encodeServerTestDelayMillis(event.guid, event.delayMillis)
MessageUtil.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, event.guid)
}
is RealPingEvent.Finish -> {
MessageUtil.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_FINISH, event.status)
onWorkerDone()
if (activeWorkers.isEmpty()) {
NotificationHelper.stopForeground(this)
stopSelf()
}
}
}
}
private fun handleMeasureCancel() {
LogUtil.i(AppConfig.TAG, "CoreTestService received cancel message, cancelling ${activeWorkers.size} active workers")
val snapshot = ArrayList(activeWorkers)
snapshot.forEach { it.cancel() }
activeWorkers.clear()
NotificationHelper.stopForeground(this)
stopSelf()
}
}
@@ -1,11 +1,10 @@
package com.v2ray.ang.service
import android.content.Context
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.RealPingEvent
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.core.CoreNativeManager
import com.v2ray.ang.core.CoreConfigManager
import com.v2ray.ang.util.MessageUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -23,8 +22,7 @@ import java.util.concurrent.atomic.AtomicInteger
class RealPingWorkerService(
private val context: Context,
private val guids: List<String>,
private val onProgress: (progress: String) -> Unit = {},
private val onFinish: (status: String) -> Unit = {}
private val onEvent: (RealPingEvent) -> Unit = {}
) {
private val job = SupervisorJob()
private val concurrency = SettingsManager.getRealPingConcurrency()
@@ -41,13 +39,13 @@ class RealPingWorkerService(
runningCount.incrementAndGet()
try {
val result = startRealPing(guid)
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
onEvent(RealPingEvent.Result(guid, result))
} catch (_: Throwable) {
// ignore
} finally {
val count = totalCount.decrementAndGet()
val left = runningCount.decrementAndGet()
onProgress("$left / $count")
onEvent(RealPingEvent.Progress("$left / $count"))
}
}
}
@@ -55,9 +53,9 @@ class RealPingWorkerService(
scope.launch {
try {
joinAll(*jobs.toTypedArray())
onFinish("0")
onEvent(RealPingEvent.Finish("0"))
} catch (_: CancellationException) {
onFinish("-1")
onEvent(RealPingEvent.Finish("-1"))
} finally {
close()
}
@@ -85,4 +83,3 @@ class RealPingWorkerService(
return CoreNativeManager.measureOutboundDelay(configResult.content, SettingsManager.getDelayTestUrl())
}
}
@@ -470,9 +470,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
updateListAction.value = getPosition(resultPair.first)
val content = intent.getStringExtra("content")
updateListAction.value = getPosition(content?: "")
}
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {