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