Skip to content

Commit d2afb9d

Browse files
committed
minimize connection test
1 parent a3e529c commit d2afb9d

File tree

7 files changed

+96
-44
lines changed

7 files changed

+96
-44
lines changed

app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ class SagerNet : Application(),
180180
"service-subscription",
181181
application.getText(R.string.service_subscription),
182182
NotificationManager.IMPORTANCE_DEFAULT
183+
), NotificationChannel(
184+
"connection-test",
185+
application.getText(R.string.connection_test),
186+
NotificationManager.IMPORTANCE_DEFAULT
183187
)
184188
)
185189
)

app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import io.nekohasekai.sagernet.ktx.int
1717
import io.nekohasekai.sagernet.ktx.long
1818
import io.nekohasekai.sagernet.ktx.parsePort
1919
import io.nekohasekai.sagernet.ktx.string
20-
import io.nekohasekai.sagernet.ktx.stringSet
2120
import io.nekohasekai.sagernet.ktx.stringToInt
2221
import io.nekohasekai.sagernet.ktx.stringToIntIfExists
2322
import moe.matsuri.nb4a.TempDatabase
@@ -41,6 +40,10 @@ object DataStore : OnPreferenceDataStoreChangeListener {
4140
var vpnService: VpnService? = null
4241
var baseService: BaseService.Interface? = null
4342

43+
// main
44+
45+
var runningTest = false
46+
4447
fun currentGroupId(): Long {
4548
val currentSelected = configurationStore.getLong(Key.PROFILE_GROUP, -1)
4649
if (currentSelected > 0L) return currentSelected

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

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package io.nekohasekai.sagernet.ui
22

3+
import android.annotation.SuppressLint
34
import android.content.Intent
45
import android.graphics.Color
5-
import android.net.Uri
66
import android.os.Bundle
77
import android.os.SystemClock
88
import android.provider.OpenableColumns
@@ -92,12 +92,12 @@ import io.nekohasekai.sagernet.ui.profile.WireGuardSettingsActivity
9292
import io.nekohasekai.sagernet.widget.QRCodeDialog
9393
import io.nekohasekai.sagernet.widget.UndoSnackbarManager
9494
import kotlinx.coroutines.DelicateCoroutinesApi
95+
import kotlinx.coroutines.Dispatchers
9596
import kotlinx.coroutines.Job
9697
import kotlinx.coroutines.delay
9798
import kotlinx.coroutines.isActive
9899
import kotlinx.coroutines.joinAll
99100
import kotlinx.coroutines.launch
100-
import kotlinx.coroutines.newFixedThreadPoolContext
101101
import kotlinx.coroutines.sync.Mutex
102102
import kotlinx.coroutines.sync.withLock
103103
import moe.matsuri.nb4a.Protocols
@@ -106,7 +106,6 @@ import moe.matsuri.nb4a.proxy.anytls.AnyTLSSettingsActivity
106106
import moe.matsuri.nb4a.proxy.config.ConfigSettingActivity
107107
import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSSettingsActivity
108108
import okhttp3.internal.closeQuietly
109-
import java.net.InetAddress
110109
import java.net.InetSocketAddress
111110
import java.net.Socket
112111
import java.net.UnknownHostException
@@ -115,6 +114,8 @@ import java.util.concurrent.ConcurrentLinkedQueue
115114
import java.util.concurrent.atomic.AtomicInteger
116115
import java.util.zip.ZipInputStream
117116
import kotlin.collections.set
117+
import androidx.core.net.toUri
118+
import moe.matsuri.nb4a.ui.ConnectionTestNotification
118119

119120
class ConfigurationFragment @JvmOverloads constructor(
120121
val select: Boolean = false, val selectedItem: ProxyEntity? = null, val titleRes: Int = 0
@@ -160,6 +161,7 @@ class ConfigurationFragment @JvmOverloads constructor(
160161

161162
override fun onQueryTextSubmit(query: String): Boolean = false
162163

164+
@SuppressLint("DetachAndAttachSameFragment")
163165
override fun onCreate(savedInstanceState: Bundle?) {
164166
super.onCreate(savedInstanceState)
165167

@@ -317,7 +319,7 @@ class ConfigurationFragment @JvmOverloads constructor(
317319
snackbar(getString(R.string.no_proxies_found_in_file)).show()
318320
} else import(proxies)
319321
} catch (e: SubscriptionFoundException) {
320-
(requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))
322+
(requireActivity() as MainActivity).importSubscription(e.link.toUri())
321323
} catch (e: Exception) {
322324
Logs.w(e)
323325
onMainDispatcher {
@@ -360,7 +362,7 @@ class ConfigurationFragment @JvmOverloads constructor(
360362
snackbar(getString(R.string.no_proxies_found_in_clipboard)).show()
361363
} else import(proxies)
362364
} catch (e: SubscriptionFoundException) {
363-
(requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))
365+
(requireActivity() as MainActivity).importSubscription(e.link.toUri())
364366
} catch (e: Exception) {
365367
Logs.w(e)
366368

@@ -597,15 +599,19 @@ class ConfigurationFragment @JvmOverloads constructor(
597599
inner class TestDialog {
598600
val binding = LayoutProgressListBinding.inflate(layoutInflater)
599601
val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
600-
.setNegativeButton(android.R.string.cancel) { _, _ ->
601-
cancel()
602+
.setPositiveButton(R.string.minimize) { _, _ ->
603+
minimize()
602604
}
603-
.setOnDismissListener {
605+
.setNegativeButton(android.R.string.cancel) { _, _ ->
604606
cancel()
605607
}
606608
.setCancelable(false)
607609

608610
lateinit var cancel: () -> Unit
611+
lateinit var minimize: () -> Unit
612+
613+
var notification: ConnectionTestNotification? = null
614+
609615
val fragment by lazy { getCurrentGroupFragment() }
610616
val results = Collections.synchronizedList(mutableListOf<ProxyEntity?>())
611617
var proxyN = 0
@@ -615,10 +621,10 @@ class ConfigurationFragment @JvmOverloads constructor(
615621
results.add(profile)
616622
}
617623

618-
suspend fun update(profile: ProxyEntity) {
619-
fragment?.configurationListView?.post {
620-
val context = context ?: return@post
621-
if (!isAdded) return@post
624+
fun update(profile: ProxyEntity) {
625+
runOnMainDispatcher {
626+
val context = context ?: return@runOnMainDispatcher
627+
if (!isAdded) return@runOnMainDispatcher
622628

623629
var profileStatusText: String? = null
624630
var profileStatusColor = 0
@@ -669,38 +675,31 @@ class ConfigurationFragment @JvmOverloads constructor(
669675
append("\n")
670676
}
671677

678+
val progress = finishedN.addAndGet(1)
672679
binding.nowTesting.text = text
673-
binding.progress.text = "${finishedN.addAndGet(1)} / $proxyN"
680+
binding.progress.text = "$progress / $proxyN"
681+
682+
notification?.updateNotification(progress, proxyN, progress >= proxyN)
674683
}
675684
}
676685

677686
}
678687

679-
fun stopService() {
680-
if (DataStore.serviceState.started) SagerNet.stopService()
681-
}
682-
683688
@OptIn(DelicateCoroutinesApi::class)
684689
@Suppress("EXPERIMENTAL_API_USAGE")
685690
fun pingTest(icmpPing: Boolean) {
691+
if (DataStore.runningTest) return else DataStore.runningTest = true
686692
val test = TestDialog()
687-
val testJobs = mutableListOf<Job>()
688693
val dialog = test.builder.show()
694+
val testJobs = mutableListOf<Job>()
695+
val group = DataStore.currentGroup()
696+
689697
val mainJob = runOnDefaultDispatcher {
690-
if (DataStore.serviceState.started) {
691-
stopService()
692-
delay(500) // wait for service stop
693-
}
694-
val group = DataStore.currentGroup()
695698
val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)
696699
test.proxyN = profilesUnfiltered.size
697700
val profiles = ConcurrentLinkedQueue(profilesUnfiltered)
698-
val testPool = newFixedThreadPoolContext(
699-
DataStore.connectionTestConcurrent,
700-
"pingTest"
701-
)
702701
repeat(DataStore.connectionTestConcurrent) {
703-
testJobs.add(launch(testPool) {
702+
testJobs.add(launch(Dispatchers.IO) {
704703
while (isActive) {
705704
val profile = profiles.poll() ?: break
706705

@@ -727,7 +726,7 @@ class ConfigurationFragment @JvmOverloads constructor(
727726
var address = profile.requireBean().serverAddress
728727
if (!address.isIpAddress()) {
729728
try {
730-
InetAddress.getAllByName(address).apply {
729+
SagerNet.underlyingNetwork!!.getAllByName(address).apply {
731730
if (isNotEmpty()) {
732731
address = this[0].hostAddress
733732
}
@@ -746,7 +745,9 @@ class ConfigurationFragment @JvmOverloads constructor(
746745
if (icmpPing) {
747746
// removed
748747
} else {
749-
val socket = Socket()
748+
val socket =
749+
SagerNet.underlyingNetwork?.socketFactory?.createSocket()
750+
?: Socket()
750751
try {
751752
socket.soTimeout = 3000
752753
socket.bind(InetSocketAddress(0))
@@ -802,13 +803,13 @@ class ConfigurationFragment @JvmOverloads constructor(
802803
}
803804

804805
testJobs.joinAll()
805-
testPool.close()
806806

807-
onMainDispatcher {
808-
dialog.dismiss()
807+
runOnMainDispatcher {
808+
test.cancel()
809809
}
810810
}
811811
test.cancel = {
812+
dialog.dismiss()
812813
runOnDefaultDispatcher {
813814
test.results.filterNotNull().forEach {
814815
try {
@@ -820,27 +821,32 @@ class ConfigurationFragment @JvmOverloads constructor(
820821
GroupManager.postReload(DataStore.currentGroupId())
821822
mainJob.cancel()
822823
testJobs.forEach { it.cancel() }
824+
DataStore.runningTest = false
823825
}
824826
}
827+
test.minimize = {
828+
test.notification = ConnectionTestNotification(
829+
dialog.context,
830+
"[${group.displayName()}] ${getString(R.string.connection_test)}"
831+
)
832+
dialog.hide()
833+
}
825834
}
826835

827836
@OptIn(DelicateCoroutinesApi::class)
828837
fun urlTest() {
838+
if (DataStore.runningTest) return else DataStore.runningTest = true
829839
val test = TestDialog()
830840
val dialog = test.builder.show()
831841
val testJobs = mutableListOf<Job>()
842+
val group = DataStore.currentGroup()
832843

833844
val mainJob = runOnDefaultDispatcher {
834-
val group = DataStore.currentGroup()
835845
val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)
836846
test.proxyN = profilesUnfiltered.size
837847
val profiles = ConcurrentLinkedQueue(profilesUnfiltered)
838-
val testPool = newFixedThreadPoolContext(
839-
DataStore.connectionTestConcurrent,
840-
"urlTest"
841-
)
842848
repeat(DataStore.connectionTestConcurrent) {
843-
testJobs.add(launch(testPool) {
849+
testJobs.add(launch(Dispatchers.IO) {
844850
val urlTest = UrlTest() // note: this is NOT in bg process
845851
while (isActive) {
846852
val profile = profiles.poll() ?: break
@@ -866,11 +872,12 @@ class ConfigurationFragment @JvmOverloads constructor(
866872

867873
testJobs.joinAll()
868874

869-
onMainDispatcher {
870-
dialog.dismiss()
875+
runOnMainDispatcher {
876+
test.cancel()
871877
}
872878
}
873879
test.cancel = {
880+
dialog.dismiss()
874881
runOnDefaultDispatcher {
875882
test.results.filterNotNull().forEach {
876883
try {
@@ -882,8 +889,16 @@ class ConfigurationFragment @JvmOverloads constructor(
882889
GroupManager.postReload(DataStore.currentGroupId())
883890
mainJob.cancel()
884891
testJobs.forEach { it.cancel() }
892+
DataStore.runningTest = false
885893
}
886894
}
895+
test.minimize = {
896+
test.notification = ConnectionTestNotification(
897+
dialog.context,
898+
"[${group.displayName()}] ${getString(R.string.connection_test)}"
899+
)
900+
dialog.hide()
901+
}
887902
}
888903

889904
inner class GroupPagerAdapter : FragmentStateAdapter(this),
@@ -1412,7 +1427,6 @@ class ConfigurationFragment @JvmOverloads constructor(
14121427

14131428
fun reloadProfiles() {
14141429
var newProfiles = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)
1415-
val subscription = proxyGroup.subscription
14161430
when (proxyGroup.order) {
14171431
GroupOrder.BY_NAME -> {
14181432
newProfiles = newProfiles.sortedBy { it.displayName() }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package moe.matsuri.nb4a.ui
2+
3+
import android.content.Context
4+
import androidx.core.app.NotificationCompat
5+
import io.nekohasekai.sagernet.R
6+
import io.nekohasekai.sagernet.SagerNet
7+
import io.nekohasekai.sagernet.ktx.Logs
8+
9+
class ConnectionTestNotification(val context: Context, val title: String) {
10+
private val channelId = "connection-test"
11+
private val notificationId = 1001
12+
13+
fun updateNotification(progress: Int, max: Int, finished: Boolean) {
14+
try {
15+
if (finished) {
16+
SagerNet.notification.cancel(notificationId)
17+
return
18+
}
19+
val builder = NotificationCompat.Builder(context, channelId)
20+
.setSmallIcon(R.drawable.ic_service_active)
21+
.setContentTitle(title)
22+
.setOnlyAlertOnce(true)
23+
.setContentText("$progress / $max").setProgress(max, progress, false)
24+
SagerNet.notification.notify(notificationId, builder.build())
25+
} catch (e: Exception) {
26+
Logs.w(e)
27+
}
28+
}
29+
}

app/src/main/res/values-zh-rCN/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,4 +494,5 @@
494494
<string name="check_update_no">检查成功,但没有更新。</string>
495495
<string name="reset_settings">恢复默认设置</string>
496496
<string name="reset_settings_message">恢复默认设置,但节点、分组等数据将保留。如需完全清除数据,请在系统设置中直接清除应用数据。</string>
497+
<string name="minimize">最小化</string>
497498
</resources>

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,4 +574,5 @@
574574
<string name="check_update_no">Check successful, but no updates.</string>
575575
<string name="reset_settings">Restore default settings</string>
576576
<string name="reset_settings_message">Restore default settings, but data such as nodes and groups will be retained. To completely clear data, clear application data directly in the system settings.</string>
577+
<string name="minimize">Minimize</string>
577578
</resources>

nb4a.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
PACKAGE_NAME=moe.nb4a
22
VERSION_NAME=1.3.9
3-
PRE_VERSION_NAME=pre-1.4.0-20250904-1
3+
PRE_VERSION_NAME=pre-1.4.0-20250904-2
44
VERSION_CODE=43

0 commit comments

Comments
 (0)