Skip to content

Commit 82d3a0b

Browse files
committed
Feat: Added a speed test progress bar and completion node count functionality; optimized menu order.
1 parent d279204 commit 82d3a0b

7 files changed

Lines changed: 58 additions & 29 deletions

File tree

app/src/main/java/io/nekohasekai/sagernet/bg/proto/SpeedTestRunner.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ data class SpeedTestSnapshot(
3333
val done: Boolean = false,
3434
)
3535

36+
internal fun completedSpeedTestCount(index: Int, total: Int, done: Boolean): Int {
37+
if (total <= 0) return 0
38+
return (index + if (done) 1 else 0).coerceIn(0, total)
39+
}
40+
3641
interface SpeedTestNodeSession : AutoCloseable {
3742
suspend fun run(onSample: (SpeedTestSnapshot) -> Unit): SpeedTestSnapshot
3843
fun cancel()

app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import io.nekohasekai.sagernet.bg.proto.AndroidSpeedTestSession
5757
import io.nekohasekai.sagernet.bg.proto.SpeedTestQueueRunner
5858
import io.nekohasekai.sagernet.bg.proto.SpeedTestSnapshot
5959
import io.nekohasekai.sagernet.bg.proto.UrlTest
60+
import io.nekohasekai.sagernet.bg.proto.completedSpeedTestCount
6061
import io.nekohasekai.sagernet.database.DataStore
6162
import io.nekohasekai.sagernet.database.GroupManager
6263
import io.nekohasekai.sagernet.database.ProfileManager
@@ -863,6 +864,10 @@ class ConfigurationFragment @JvmOverloads constructor(
863864
if (DataStore.runningTest) return else DataStore.runningTest = true
864865
val group = DataStore.currentGroup()
865866
val binding = LayoutProgressListBinding.inflate(layoutInflater)
867+
binding.progressCircular.isGone = true
868+
binding.progressLinear.isVisible = true
869+
binding.progressLinear.max = 1
870+
binding.progressLinear.setProgressCompat(0, false)
866871
val builder = MaterialAlertDialogBuilder(requireContext())
867872
.setTitle(R.string.speed_test_group)
868873
.setView(binding.root)
@@ -909,14 +914,17 @@ class ConfigurationFragment @JvmOverloads constructor(
909914
try {
910915
val profiles = SagerDatabase.proxyDao.getByGroup(group.id)
911916
if (profiles.isEmpty()) {
912-
runOnMainDispatcher {
913-
binding.nowTesting.text = getString(R.string.speed_test_finished_summary, 0, 0, 0)
914-
binding.progress.text = "0 / 0"
915-
binding.progressCircular.isGone = true
917+
onMainDispatcher {
918+
dialog.dismiss()
916919
}
917920
return@runOnDefaultDispatcher
918921
}
919-
val results = runner.run(profiles) { index, total, sample ->
922+
onMainDispatcher {
923+
binding.progressLinear.max = profiles.size
924+
binding.progressLinear.setProgressCompat(0, false)
925+
binding.progress.text = "0 / ${profiles.size}"
926+
}
927+
runner.run(profiles) { index, total, sample ->
920928
val outcome = SpeedTestOutcome.completedOrNull(
921929
mode = sample.mode,
922930
stage = sample.stage,
@@ -943,34 +951,20 @@ class ConfigurationFragment @JvmOverloads constructor(
943951
val detail = formatSpeedTestSnapshot(sample)
944952
speedTestNotification?.updateNotification(index + 1, total, false, detail)
945953
if (!speedTestHidden && isAdded) {
954+
val completed = completedSpeedTestCount(index, total, sample.done)
946955
binding.nowTesting.text = detail
947-
binding.progress.text = "${index + 1} / $total"
956+
binding.progress.text = "$completed / $total"
957+
binding.progressLinear.setProgressCompat(completed, true)
948958
}
949959
}
950960
}
951-
runOnMainDispatcher {
952-
if (!isAdded) return@runOnMainDispatcher
953-
val failed = results.count { it.error.isNotBlank() }
954-
val cancelled = results.count { it.cancelled }
955-
binding.nowTesting.text = buildString {
956-
append(
957-
getString(
958-
R.string.speed_test_finished_summary,
959-
results.size,
960-
failed,
961-
cancelled,
962-
)
963-
)
964-
results.forEach { append("\n\n").append(formatSpeedTestSnapshot(it)) }
965-
}
966-
binding.progress.text = "${results.size} / ${profiles.size}"
967-
binding.progressCircular.isGone = true
961+
onMainDispatcher {
962+
dialog.dismiss()
968963
}
969964
} catch (_: CancellationException) {
970965
runOnMainDispatcher {
971966
if (!speedTestHidden && isAdded) {
972967
binding.nowTesting.text = getString(R.string.speed_test_stage_cancelled)
973-
binding.progressCircular.isGone = true
974968
}
975969
}
976970
} finally {

app/src/main/res/layout/layout_progress_list.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
android:indeterminate="true"
1414
app:indicatorColor="?colorPrimary" />
1515

16+
<com.google.android.material.progressindicator.LinearProgressIndicator
17+
android:id="@+id/progress_linear"
18+
android:layout_width="match_parent"
19+
android:layout_height="wrap_content"
20+
android:indeterminate="false"
21+
android:visibility="gone"
22+
app:indicatorColor="?colorPrimary" />
23+
1624
<TextView
1725
android:id="@+id/now_testing"
1826
android:layout_width="match_parent"
@@ -25,4 +33,4 @@
2533
android:layout_height="wrap_content"
2634
android:textAlignment="center" />
2735

28-
</LinearLayout>
36+
</LinearLayout>

app/src/main/res/menu/add_profile_menu.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,12 @@
104104
<item
105105
android:id="@+id/action_remove_duplicate"
106106
android:title="@string/remove_duplicate" />
107-
<item
108-
android:id="@+id/action_connection_tcp_ping"
109-
android:title="@string/speed_test_group" />
110107
<item
111108
android:id="@+id/action_connection_url_test"
112109
android:title="@string/connection_test_url_test" />
110+
<item
111+
android:id="@+id/action_connection_tcp_ping"
112+
android:title="@string/speed_test_group" />
113113
<item
114114
android:id="@+id/action_connection_test_clear_results"
115115
android:title="@string/connection_test_clear_results" />

app/src/test/java/io/nekohasekai/sagernet/SpeedTestAndroidContractTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class SpeedTestAndroidContractTest {
4646
assertTrue(speedTest.contains("ConnectionTestNotification("))
4747
assertTrue(speedTest.contains("SpeedTestOutcome.completedOrNull("))
4848
assertTrue(speedTest.contains("updateSpeedTestResult("))
49+
assertTrue(speedTest.contains("completedSpeedTestCount(index, total, sample.done)"))
50+
assertTrue(speedTest.contains("binding.progressLinear.setProgressCompat(completed, true)"))
51+
assertTrue(speedTest.contains("dialog.dismiss()"))
52+
assertFalse(speedTest.contains("speed_test_finished_summary"))
53+
assertFalse(speedTest.contains("results.forEach"))
4954
listOf(
5055
"sample.downloadBitsPerSecond",
5156
"sample.uploadBitsPerSecond",
@@ -93,9 +98,16 @@ class SpeedTestAndroidContractTest {
9398
@Test
9499
fun menuIsRewiredWithDataUsageConfirmation() {
95100
val menu = source("main/res/menu/add_profile_menu.xml")
101+
val layout = source("main/res/layout/layout_progress_list.xml")
96102
val strings = source("main/res/values/strings.xml")
103+
val urlTestPosition = menu.indexOf("android:id=\"@+id/action_connection_url_test\"")
104+
val speedTestPosition = menu.indexOf("android:id=\"@+id/action_connection_tcp_ping\"")
97105
assertTrue(menu.contains("android:id=\"@+id/action_connection_tcp_ping\""))
98106
assertTrue(menu.contains("android:title=\"@string/speed_test_group\""))
107+
assertTrue(urlTestPosition >= 0)
108+
assertTrue(speedTestPosition > urlTestPosition)
109+
assertTrue(layout.contains("android:id=\"@+id/progress_linear\""))
110+
assertTrue(layout.contains("android:visibility=\"gone\""))
99111
assertTrue(strings.contains("name=\"speed_test_confirm_message\""))
100112
}
101113

app/src/test/java/io/nekohasekai/sagernet/bg/proto/SpeedTestQueueRunnerTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ import org.junit.Test
88

99
class SpeedTestQueueRunnerTest {
1010

11+
@Test
12+
fun completedCountAdvancesOnlyWhenCurrentNodeFinishes() {
13+
assertEquals(0, completedSpeedTestCount(index = 0, total = 3, done = false))
14+
assertEquals(1, completedSpeedTestCount(index = 0, total = 3, done = true))
15+
assertEquals(1, completedSpeedTestCount(index = 1, total = 3, done = false))
16+
assertEquals(2, completedSpeedTestCount(index = 1, total = 3, done = true))
17+
assertEquals(3, completedSpeedTestCount(index = 2, total = 3, done = true))
18+
assertEquals(0, completedSpeedTestCount(index = 0, total = 0, done = true))
19+
}
20+
1121
@Test
1222
fun nodesRunSeriallyAndFailureDoesNotStopQueue() = runBlocking {
1323
val profiles = listOf(1L, 2L, 3L)

openspec/changes/align-connection-and-speed-tests/tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
- [x] 3.2 实现“速度测试本组”进度 UI/通知,明确当前节点与下载/上传阶段,展示实时吞吐、服务器/延迟、失败和取消;使用独立于 `ProxyEntity.ping/status` 的 profile 关联持久化字段或实体保存成功结果,并在节点卡片右下角现有测速信息区域以主题灰色辅助文字按模式展示最终上下行吞吐。结果在关闭临时 UI、列表重绑/刷新及重新进入页面后仍保留,直到清除测试结果或下一次成功测速原子覆盖;失败/取消不得写入伪造成功值。
2121
- [x] 3.3 将原 TCPing 菜单位置重新接线到速度测试组入口,并增加耗流量提示或明确确认流程;保留 URL 测试互斥状态,防止两类批量测试同时运行。
2222
- [x] 3.4 增加 Android 单元/组件测试,验证节点串行顺序、四种模式参数、采样到 UI 映射、单节点失败后队列策略、取消、Fragment 销毁和通知最小化恢复;覆盖专用速度结果的持久化读写、按模式格式化、节点卡片右下角主题灰色绑定、列表重绑/重新进入恢复、下一次成功结果覆盖、清除测试结果,以及失败/取消不写入成功吞吐值。
23-
- [ ] 3.5 精简组测速 UI 并修正菜单顺序:测速弹窗仅展示当前正在测速的节点及其实时阶段/吞吐,不在弹窗内累积已完成节点详情,队列完成后不再显示结果汇总弹窗;增加按“已完成节点数/总节点数”更新的组队列进度条,最终结果继续仅在各节点卡片现有测速信息区域逐条展示。将“速度测试本组”固定排列在“URL 测试本组”下方。补充 Android 单元/组件测试,覆盖菜单顺序、进度条更新、弹窗切换节点时替换而非追加内容、完成后无汇总弹窗,以及节点卡片结果持久显示不受影响。
23+
- [x] 3.5 精简组测速 UI 并修正菜单顺序:测速弹窗仅展示当前正在测速的节点及其实时阶段/吞吐,不在弹窗内累积已完成节点详情,队列完成后不再显示结果汇总弹窗;增加按“已完成节点数/总节点数”更新的组队列进度条,最终结果继续仅在各节点卡片现有测速信息区域逐条展示。将“速度测试本组”固定排列在“URL 测试本组”下方。补充 Android 单元/组件测试,覆盖菜单顺序、进度条更新、弹窗切换节点时替换而非追加内容、完成后无汇总弹窗,以及节点卡片结果持久显示不受影响。
2424
- [ ] 3.6 提交本批次并运行 GitHub Actions Android unit/lint/build 与 libcore 集成 job;预期所有模式参数、生命周期、持久化、节点卡片结果、精简测速弹窗、队列进度条和菜单顺序测试通过。随后在至少两个节点的真机组中执行下载+上传、仅上传和简单下载,确认流量经过各节点、节点间不并发、弹窗只显示当前节点、进度条持续更新、完成后无汇总弹窗且取消及时;确认“速度测试本组”位于“URL 测试本组”下方,每个成功结果以灰色辅助文字保留在对应节点卡片右下角,关闭对话框、刷新列表和重新进入页面后仍在,下一次测速可覆盖且“清除测试结果”会移除;回传 run 链接、提交 SHA、录屏/截图、测试服务器日志或节点出口 IP/流量证据和失败日志。
2525

2626
## 4. 移除旧 TCP/ICMP Ping 专属代码

0 commit comments

Comments
 (0)