From d2afb9df0ecea5c0d72a0d3800ea3bcc43eb552a Mon Sep 17 00:00:00 2001 From: armv9 <48624112+arm64v8a@users.noreply.github.com> Date: Thu, 4 Sep 2025 16:23:02 +0900 Subject: [PATCH] minimize connection test --- .../java/io/nekohasekai/sagernet/SagerNet.kt | 4 + .../sagernet/database/DataStore.kt | 5 +- .../sagernet/ui/ConfigurationFragment.kt | 98 +++++++++++-------- .../nb4a/ui/ConnectionTestNotification.kt | 29 ++++++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + nb4a.properties | 2 +- 7 files changed, 96 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt diff --git a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt index 3e5f1cd..2379b1a 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt @@ -180,6 +180,10 @@ class SagerNet : Application(), "service-subscription", application.getText(R.string.service_subscription), NotificationManager.IMPORTANCE_DEFAULT + ), NotificationChannel( + "connection-test", + application.getText(R.string.connection_test), + NotificationManager.IMPORTANCE_DEFAULT ) ) ) diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt index 4c2b4e0..06bb501 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt @@ -17,7 +17,6 @@ import io.nekohasekai.sagernet.ktx.int import io.nekohasekai.sagernet.ktx.long import io.nekohasekai.sagernet.ktx.parsePort import io.nekohasekai.sagernet.ktx.string -import io.nekohasekai.sagernet.ktx.stringSet import io.nekohasekai.sagernet.ktx.stringToInt import io.nekohasekai.sagernet.ktx.stringToIntIfExists import moe.matsuri.nb4a.TempDatabase @@ -41,6 +40,10 @@ object DataStore : OnPreferenceDataStoreChangeListener { var vpnService: VpnService? = null var baseService: BaseService.Interface? = null + // main + + var runningTest = false + fun currentGroupId(): Long { val currentSelected = configurationStore.getLong(Key.PROFILE_GROUP, -1) if (currentSelected > 0L) return currentSelected diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt index d915244..9d1fb3c 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt @@ -1,8 +1,8 @@ package io.nekohasekai.sagernet.ui +import android.annotation.SuppressLint import android.content.Intent import android.graphics.Color -import android.net.Uri import android.os.Bundle import android.os.SystemClock import android.provider.OpenableColumns @@ -92,12 +92,12 @@ import io.nekohasekai.sagernet.ui.profile.WireGuardSettingsActivity import io.nekohasekai.sagernet.widget.QRCodeDialog import io.nekohasekai.sagernet.widget.UndoSnackbarManager import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import kotlinx.coroutines.newFixedThreadPoolContext import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import moe.matsuri.nb4a.Protocols @@ -106,7 +106,6 @@ import moe.matsuri.nb4a.proxy.anytls.AnyTLSSettingsActivity import moe.matsuri.nb4a.proxy.config.ConfigSettingActivity import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSSettingsActivity import okhttp3.internal.closeQuietly -import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket import java.net.UnknownHostException @@ -115,6 +114,8 @@ import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicInteger import java.util.zip.ZipInputStream import kotlin.collections.set +import androidx.core.net.toUri +import moe.matsuri.nb4a.ui.ConnectionTestNotification class ConfigurationFragment @JvmOverloads constructor( val select: Boolean = false, val selectedItem: ProxyEntity? = null, val titleRes: Int = 0 @@ -160,6 +161,7 @@ class ConfigurationFragment @JvmOverloads constructor( override fun onQueryTextSubmit(query: String): Boolean = false + @SuppressLint("DetachAndAttachSameFragment") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -317,7 +319,7 @@ class ConfigurationFragment @JvmOverloads constructor( snackbar(getString(R.string.no_proxies_found_in_file)).show() } else import(proxies) } catch (e: SubscriptionFoundException) { - (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link)) + (requireActivity() as MainActivity).importSubscription(e.link.toUri()) } catch (e: Exception) { Logs.w(e) onMainDispatcher { @@ -360,7 +362,7 @@ class ConfigurationFragment @JvmOverloads constructor( snackbar(getString(R.string.no_proxies_found_in_clipboard)).show() } else import(proxies) } catch (e: SubscriptionFoundException) { - (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link)) + (requireActivity() as MainActivity).importSubscription(e.link.toUri()) } catch (e: Exception) { Logs.w(e) @@ -597,15 +599,19 @@ class ConfigurationFragment @JvmOverloads constructor( inner class TestDialog { val binding = LayoutProgressListBinding.inflate(layoutInflater) val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root) - .setNegativeButton(android.R.string.cancel) { _, _ -> - cancel() + .setPositiveButton(R.string.minimize) { _, _ -> + minimize() } - .setOnDismissListener { + .setNegativeButton(android.R.string.cancel) { _, _ -> cancel() } .setCancelable(false) lateinit var cancel: () -> Unit + lateinit var minimize: () -> Unit + + var notification: ConnectionTestNotification? = null + val fragment by lazy { getCurrentGroupFragment() } val results = Collections.synchronizedList(mutableListOf()) var proxyN = 0 @@ -615,10 +621,10 @@ class ConfigurationFragment @JvmOverloads constructor( results.add(profile) } - suspend fun update(profile: ProxyEntity) { - fragment?.configurationListView?.post { - val context = context ?: return@post - if (!isAdded) return@post + fun update(profile: ProxyEntity) { + runOnMainDispatcher { + val context = context ?: return@runOnMainDispatcher + if (!isAdded) return@runOnMainDispatcher var profileStatusText: String? = null var profileStatusColor = 0 @@ -669,38 +675,31 @@ class ConfigurationFragment @JvmOverloads constructor( append("\n") } + val progress = finishedN.addAndGet(1) binding.nowTesting.text = text - binding.progress.text = "${finishedN.addAndGet(1)} / $proxyN" + binding.progress.text = "$progress / $proxyN" + + notification?.updateNotification(progress, proxyN, progress >= proxyN) } } } - fun stopService() { - if (DataStore.serviceState.started) SagerNet.stopService() - } - @OptIn(DelicateCoroutinesApi::class) @Suppress("EXPERIMENTAL_API_USAGE") fun pingTest(icmpPing: Boolean) { + if (DataStore.runningTest) return else DataStore.runningTest = true val test = TestDialog() - val testJobs = mutableListOf() val dialog = test.builder.show() + val testJobs = mutableListOf() + val group = DataStore.currentGroup() + val mainJob = runOnDefaultDispatcher { - if (DataStore.serviceState.started) { - stopService() - delay(500) // wait for service stop - } - val group = DataStore.currentGroup() val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id) test.proxyN = profilesUnfiltered.size val profiles = ConcurrentLinkedQueue(profilesUnfiltered) - val testPool = newFixedThreadPoolContext( - DataStore.connectionTestConcurrent, - "pingTest" - ) repeat(DataStore.connectionTestConcurrent) { - testJobs.add(launch(testPool) { + testJobs.add(launch(Dispatchers.IO) { while (isActive) { val profile = profiles.poll() ?: break @@ -727,7 +726,7 @@ class ConfigurationFragment @JvmOverloads constructor( var address = profile.requireBean().serverAddress if (!address.isIpAddress()) { try { - InetAddress.getAllByName(address).apply { + SagerNet.underlyingNetwork!!.getAllByName(address).apply { if (isNotEmpty()) { address = this[0].hostAddress } @@ -746,7 +745,9 @@ class ConfigurationFragment @JvmOverloads constructor( if (icmpPing) { // removed } else { - val socket = Socket() + val socket = + SagerNet.underlyingNetwork?.socketFactory?.createSocket() + ?: Socket() try { socket.soTimeout = 3000 socket.bind(InetSocketAddress(0)) @@ -802,13 +803,13 @@ class ConfigurationFragment @JvmOverloads constructor( } testJobs.joinAll() - testPool.close() - onMainDispatcher { - dialog.dismiss() + runOnMainDispatcher { + test.cancel() } } test.cancel = { + dialog.dismiss() runOnDefaultDispatcher { test.results.filterNotNull().forEach { try { @@ -820,27 +821,32 @@ class ConfigurationFragment @JvmOverloads constructor( GroupManager.postReload(DataStore.currentGroupId()) mainJob.cancel() testJobs.forEach { it.cancel() } + DataStore.runningTest = false } } + test.minimize = { + test.notification = ConnectionTestNotification( + dialog.context, + "[${group.displayName()}] ${getString(R.string.connection_test)}" + ) + dialog.hide() + } } @OptIn(DelicateCoroutinesApi::class) fun urlTest() { + if (DataStore.runningTest) return else DataStore.runningTest = true val test = TestDialog() val dialog = test.builder.show() val testJobs = mutableListOf() + val group = DataStore.currentGroup() val mainJob = runOnDefaultDispatcher { - val group = DataStore.currentGroup() val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id) test.proxyN = profilesUnfiltered.size val profiles = ConcurrentLinkedQueue(profilesUnfiltered) - val testPool = newFixedThreadPoolContext( - DataStore.connectionTestConcurrent, - "urlTest" - ) repeat(DataStore.connectionTestConcurrent) { - testJobs.add(launch(testPool) { + testJobs.add(launch(Dispatchers.IO) { val urlTest = UrlTest() // note: this is NOT in bg process while (isActive) { val profile = profiles.poll() ?: break @@ -866,11 +872,12 @@ class ConfigurationFragment @JvmOverloads constructor( testJobs.joinAll() - onMainDispatcher { - dialog.dismiss() + runOnMainDispatcher { + test.cancel() } } test.cancel = { + dialog.dismiss() runOnDefaultDispatcher { test.results.filterNotNull().forEach { try { @@ -882,8 +889,16 @@ class ConfigurationFragment @JvmOverloads constructor( GroupManager.postReload(DataStore.currentGroupId()) mainJob.cancel() testJobs.forEach { it.cancel() } + DataStore.runningTest = false } } + test.minimize = { + test.notification = ConnectionTestNotification( + dialog.context, + "[${group.displayName()}] ${getString(R.string.connection_test)}" + ) + dialog.hide() + } } inner class GroupPagerAdapter : FragmentStateAdapter(this), @@ -1412,7 +1427,6 @@ class ConfigurationFragment @JvmOverloads constructor( fun reloadProfiles() { var newProfiles = SagerDatabase.proxyDao.getByGroup(proxyGroup.id) - val subscription = proxyGroup.subscription when (proxyGroup.order) { GroupOrder.BY_NAME -> { newProfiles = newProfiles.sortedBy { it.displayName() } diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt b/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt new file mode 100644 index 0000000..31bc7a1 --- /dev/null +++ b/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt @@ -0,0 +1,29 @@ +package moe.matsuri.nb4a.ui + +import android.content.Context +import androidx.core.app.NotificationCompat +import io.nekohasekai.sagernet.R +import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.ktx.Logs + +class ConnectionTestNotification(val context: Context, val title: String) { + private val channelId = "connection-test" + private val notificationId = 1001 + + fun updateNotification(progress: Int, max: Int, finished: Boolean) { + try { + if (finished) { + SagerNet.notification.cancel(notificationId) + return + } + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_service_active) + .setContentTitle(title) + .setOnlyAlertOnce(true) + .setContentText("$progress / $max").setProgress(max, progress, false) + SagerNet.notification.notify(notificationId, builder.build()) + } catch (e: Exception) { + Logs.w(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 13399d3..cd92b7e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -494,4 +494,5 @@ 检查成功,但没有更新。 恢复默认设置 恢复默认设置,但节点、分组等数据将保留。如需完全清除数据,请在系统设置中直接清除应用数据。 + 最小化 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aebdd70..a30c703 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -574,4 +574,5 @@ Check successful, but no updates. Restore default settings 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. + Minimize \ No newline at end of file diff --git a/nb4a.properties b/nb4a.properties index e542a71..5c52ff2 100644 --- a/nb4a.properties +++ b/nb4a.properties @@ -1,4 +1,4 @@ PACKAGE_NAME=moe.nb4a VERSION_NAME=1.3.9 -PRE_VERSION_NAME=pre-1.4.0-20250904-1 +PRE_VERSION_NAME=pre-1.4.0-20250904-2 VERSION_CODE=43