@@ -49,6 +49,8 @@ import io.nekohasekai.sagernet.GroupType
4949import io.nekohasekai.sagernet.Key
5050import io.nekohasekai.sagernet.R
5151import io.nekohasekai.sagernet.SagerNet
52+ import io.nekohasekai.sagernet.SpeedTestDirection
53+ import io.nekohasekai.sagernet.SpeedTestOutcome
5254import io.nekohasekai.sagernet.aidl.TrafficData
5355import io.nekohasekai.sagernet.bg.BaseService
5456import io.nekohasekai.sagernet.bg.proto.AndroidSpeedTestSession
@@ -705,19 +707,7 @@ class ConfigurationFragment @JvmOverloads constructor(
705707
706708 R .id.action_connection_test_clear_results -> {
707709 runOnDefaultDispatcher {
708- val profiles = SagerDatabase .proxyDao.getByGroup(DataStore .currentGroupId())
709- val toClear = mutableListOf<ProxyEntity >()
710- if (profiles.isNotEmpty()) for (profile in profiles) {
711- if (profile.status != 0 ) {
712- profile.status = 0
713- profile.ping = 0
714- profile.error = null
715- toClear.add(profile)
716- }
717- }
718- if (toClear.isNotEmpty()) {
719- ProfileManager .updateProfile(toClear)
720- }
710+ SagerDatabase .proxyDao.clearTestResults(DataStore .currentGroupId())
721711 onMainDispatcher {
722712 getCurrentGroupFragment()?.adapter?.clearTestResults()
723713 }
@@ -927,6 +917,28 @@ class ConfigurationFragment @JvmOverloads constructor(
927917 return @runOnDefaultDispatcher
928918 }
929919 val results = runner.run (profiles) { index, total, sample ->
920+ val outcome = SpeedTestOutcome .completedOrNull(
921+ mode = sample.mode,
922+ stage = sample.stage,
923+ done = sample.done,
924+ cancelled = sample.cancelled,
925+ error = sample.error,
926+ downloadBitsPerSecond = sample.downloadBitsPerSecond,
927+ uploadBitsPerSecond = sample.uploadBitsPerSecond,
928+ )
929+ if (outcome != null && SagerDatabase .proxyDao.updateSpeedTestResult(
930+ proxyId = sample.profileId,
931+ mode = outcome.mode,
932+ downloadBitsPerSecond = outcome.downloadBitsPerSecond,
933+ uploadBitsPerSecond = outcome.uploadBitsPerSecond,
934+ ) > 0
935+ ) {
936+ runOnMainDispatcher {
937+ adapter.groupFragments.values.forEach { fragment ->
938+ fragment.adapter?.updateSpeedTestResult(sample.profileId, outcome)
939+ }
940+ }
941+ }
930942 runOnMainDispatcher {
931943 val detail = formatSpeedTestSnapshot(sample)
932944 speedTestNotification?.updateNotification(index + 1 , total, false , detail)
@@ -2096,15 +2108,25 @@ class ConfigurationFragment @JvmOverloads constructor(
20962108
20972109 fun clearTestResults () {
20982110 for (profile in configurationList.values) {
2099- if (profile.status != 0 ) {
2100- profile.status = 0
2101- profile.ping = 0
2102- profile.error = null
2103- }
2111+ profile.status = 0
2112+ profile.ping = 0
2113+ profile.error = null
2114+ profile.speedTestMode = " "
2115+ profile.speedTestDownloadBitsPerSecond = 0
2116+ profile.speedTestUploadBitsPerSecond = 0
21042117 }
21052118 notifyDataSetChanged()
21062119 }
21072120
2121+ fun updateSpeedTestResult (profileId : Long , outcome : SpeedTestOutcome ) {
2122+ val profile = configurationList[profileId] ? : return
2123+ profile.speedTestMode = outcome.mode
2124+ profile.speedTestDownloadBitsPerSecond = outcome.downloadBitsPerSecond
2125+ profile.speedTestUploadBitsPerSecond = outcome.uploadBitsPerSecond
2126+ val index = configurationIdList.indexOf(profileId)
2127+ if (index >= 0 ) notifyItemChanged(index)
2128+ }
2129+
21082130 fun remove (pos : Int ) {
21092131 if (pos < 0 ) return
21102132 configurationIdList.removeAt(pos)
@@ -2485,6 +2507,67 @@ class ConfigurationFragment @JvmOverloads constructor(
24852507 }
24862508 }
24872509
2510+ private fun speedTestResultText (proxyEntity : ProxyEntity ): String? {
2511+ val outcome = SpeedTestOutcome (
2512+ mode = proxyEntity.speedTestMode,
2513+ downloadBitsPerSecond = proxyEntity.speedTestDownloadBitsPerSecond,
2514+ uploadBitsPerSecond = proxyEntity.speedTestUploadBitsPerSecond,
2515+ )
2516+ val rates = outcome.rates()
2517+ if (rates.isEmpty()) return null
2518+ return rates.joinToString(" " ) { rate ->
2519+ val direction = when (rate.direction) {
2520+ SpeedTestDirection .DOWNLOAD -> " ↓"
2521+ SpeedTestDirection .UPLOAD -> " ↑"
2522+ }
2523+ " $direction ${getString(R .string.speed_test_rate_mbps, rate.bitsPerSecond / 1_000_000.0 )} "
2524+ }
2525+ }
2526+
2527+ private fun bindTestResult (
2528+ proxyEntity : ProxyEntity ,
2529+ showTraffic : Boolean ,
2530+ speedTestText : String? ,
2531+ ) {
2532+ val text = SpannableStringBuilder ()
2533+ fun appendPart (value : CharSequence? , color : Int ) {
2534+ if (value.isNullOrEmpty()) return
2535+ if (text.isNotEmpty()) text.append(' \n ' )
2536+ val start = text.length
2537+ text.append(value)
2538+ text.setSpan(ForegroundColorSpan (color), start, text.length, SPAN_EXCLUSIVE_EXCLUSIVE )
2539+ }
2540+
2541+ val secondary = requireContext().getColorAttr(android.R .attr.textColorSecondary)
2542+ when (proxyEntity.status) {
2543+ 1 -> appendPart(
2544+ getString(R .string.available, proxyEntity.ping),
2545+ requireContext().getColour(R .color.material_green_500),
2546+ )
2547+
2548+ 2 -> appendPart(
2549+ proxyEntity.error,
2550+ requireContext().getColour(R .color.material_red_500),
2551+ )
2552+
2553+ 3 -> {
2554+ val error = proxyEntity.error ? : " <?>"
2555+ val friendly = Protocols .genFriendlyMsg(error)
2556+ appendPart(
2557+ if (friendly != error) friendly else getString(R .string.unavailable),
2558+ requireContext().getColour(R .color.material_red_500),
2559+ )
2560+ }
2561+
2562+ else -> if (speedTestText == null && showTraffic) {
2563+ appendPart(trafficText.text, secondary)
2564+ trafficText.text = " "
2565+ }
2566+ }
2567+ appendPart(speedTestText, secondary)
2568+ profileStatus.text = text
2569+ }
2570+
24882571 fun bind (proxyEntity : ProxyEntity ) {
24892572 val pf = parentFragment as ? ConfigurationFragment ? : return
24902573
@@ -2497,6 +2580,7 @@ class ConfigurationFragment @JvmOverloads constructor(
24972580
24982581 val rx = proxyEntity.rx
24992582 val tx = proxyEntity.tx
2583+ val speedTestText = speedTestResultText(proxyEntity)
25002584
25012585 val showTraffic = rx + tx != 0L
25022586 trafficText.isVisible = showTraffic
@@ -2525,29 +2609,7 @@ class ConfigurationFragment @JvmOverloads constructor(
25252609 }
25262610 lastSelfHasMiddleRow = ! trafficRowEmpty
25272611
2528- if (proxyEntity.status <= 0 ) {
2529- if (showTraffic) {
2530- profileStatus.text = trafficText.text
2531- profileStatus.setTextColor(requireContext().getColorAttr(android.R .attr.textColorSecondary))
2532- trafficText.text = " "
2533- } else {
2534- profileStatus.text = " "
2535- }
2536- } else if (proxyEntity.status == 1 ) {
2537- profileStatus.text = getString(R .string.available, proxyEntity.ping)
2538- profileStatus.setTextColor(requireContext().getColour(R .color.material_green_500))
2539- } else {
2540- profileStatus.setTextColor(requireContext().getColour(R .color.material_red_500))
2541- if (proxyEntity.status == 2 ) {
2542- profileStatus.text = proxyEntity.error
2543- }
2544- }
2545-
2546- if (proxyEntity.status == 3 ) {
2547- val err = proxyEntity.error ? : " <?>"
2548- val msg = Protocols .genFriendlyMsg(err)
2549- profileStatus.text = if (msg != err) msg else getString(R .string.unavailable)
2550- }
2612+ bindTestResult(proxyEntity, showTraffic, speedTestText)
25512613
25522614 val selectOrChain = select || proxyEntity.type == ProxyEntity .TYPE_CHAIN
25532615 val isDoubleColumn = layoutManager is FixedGridLayoutManager
@@ -2614,9 +2676,12 @@ class ConfigurationFragment @JvmOverloads constructor(
26142676 Formatter .formatFileSize(view.context, proxyEntity.tx),
26152677 Formatter .formatFileSize(view.context, proxyEntity.rx)
26162678 )
2617- if (proxyEntity.status <= 0 ) {
2679+ if (proxyEntity.status <= 0 && speedTestResultText(proxyEntity) == null ) {
26182680 if (profileStatus.text?.toString() != traffic) {
26192681 profileStatus.text = traffic
2682+ profileStatus.setTextColor(
2683+ requireContext().getColorAttr(android.R .attr.textColorSecondary)
2684+ )
26202685 }
26212686 } else if (trafficText.text?.toString() != traffic) {
26222687 trafficText.text = traffic
0 commit comments