minimize connection test

This commit is contained in:
armv9 2025-09-04 16:23:02 +09:00
parent a3e529cb19
commit d2afb9df0e
7 changed files with 96 additions and 44 deletions

View File

@ -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
)
)
)

View File

@ -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

View File

@ -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<ProxyEntity?>())
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<Job>()
val dialog = test.builder.show()
val mainJob = runOnDefaultDispatcher {
if (DataStore.serviceState.started) {
stopService()
delay(500) // wait for service stop
}
val testJobs = mutableListOf<Job>()
val group = DataStore.currentGroup()
val mainJob = runOnDefaultDispatcher {
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<Job>()
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() }

View File

@ -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)
}
}
}

View File

@ -494,4 +494,5 @@
<string name="check_update_no">检查成功,但没有更新。</string>
<string name="reset_settings">恢复默认设置</string>
<string name="reset_settings_message">恢复默认设置,但节点、分组等数据将保留。如需完全清除数据,请在系统设置中直接清除应用数据。</string>
<string name="minimize">最小化</string>
</resources>

View File

@ -574,4 +574,5 @@
<string name="check_update_no">Check successful, but no updates.</string>
<string name="reset_settings">Restore default settings</string>
<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>
<string name="minimize">Minimize</string>
</resources>

View File

@ -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