mirror of
https://github.com/MatsuriDayo/NekoBoxForAndroid.git
synced 2025-12-18 22:20:06 +08:00
minimize connection test
This commit is contained in:
parent
a3e529cb19
commit
d2afb9df0e
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 testJobs = mutableListOf<Job>()
|
||||
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<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() }
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user