mirror of
https://github.com/2dust/v2rayNG.git
synced 2026-10-30 06:56:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19b0c9a2c3 | ||
|
|
db0755cf0c | ||
|
|
dc6a7eae93 | ||
|
|
3ae8db972e | ||
|
|
a899d7f444 | ||
|
|
c8f78621bf | ||
|
|
0cf95ac8a3 |
No files matched your search
@@ -13,8 +13,8 @@ android {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 24
|
||||
targetSdk = 37
|
||||
versionCode = 741
|
||||
versionName = "2.3.1"
|
||||
versionCode = 742
|
||||
versionName = "2.3.2"
|
||||
|
||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||
splits {
|
||||
|
||||
@@ -915,9 +915,14 @@ object CoreConfigManager {
|
||||
val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
||||
if (userHosts.isNotNullEmpty()) {
|
||||
val userHostsMap = userHosts?.split(",").orEmpty()
|
||||
.filter { it.isNotEmpty() }
|
||||
.filter { it.contains(":") }
|
||||
.associate { it.split(":").let { (k, v) -> k to v } }
|
||||
.filter { it.isNotBlank() && it.contains(":") }
|
||||
.associate {
|
||||
// Use limit = 2 to split only at the first colon.
|
||||
// This ensures that IPv6 addresses (which contain multiple colons)
|
||||
// are preserved entirely in the second part.
|
||||
val parts = it.split(":", limit = 2)
|
||||
parts[0].trim() to parts[1].trim()
|
||||
}
|
||||
hosts.putAll(userHostsMap)
|
||||
}
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ object CoreServiceManager {
|
||||
if (!isRunning()) return false
|
||||
|
||||
return try {
|
||||
val tunFd = tunFdForCore()
|
||||
val tunFd = currentVpnInterface
|
||||
|
||||
isReloading = true
|
||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core reload start...")
|
||||
@@ -270,23 +270,6 @@ object CoreServiceManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tun descriptor to hand to the core on a reload.
|
||||
*
|
||||
* With hev-socks5-tunnel the core is started without a tun and never touches the descriptor.
|
||||
* Otherwise it closes the one it was given when it stops, which would take the VPN interface
|
||||
* down, so it gets a duplicate to close instead.
|
||||
*/
|
||||
private fun tunFdForCore(): ParcelFileDescriptor? {
|
||||
val vpnInterface = currentVpnInterface ?: return null
|
||||
return try {
|
||||
if (SettingsManager.isUsingHevTun()) vpnInterface else vpnInterface.dup()
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to duplicate VPN interface", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries and resets all outbound traffic counters in one core call.
|
||||
* Go side format: tag,direction,value;tag,direction,value;
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.v2ray.ang.dto.RealPingEvent
|
||||
import com.v2ray.ang.dto.TestServiceMessage
|
||||
import com.v2ray.ang.enums.NotificationChannelType
|
||||
import com.v2ray.ang.extension.serializable
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.helper.MessageHelper
|
||||
import com.v2ray.ang.helper.NotificationHelper
|
||||
@@ -96,7 +97,7 @@ class CoreTestService : Service() {
|
||||
context = this,
|
||||
guids = guidsList,
|
||||
onlyTcp = message.onlyTcp,
|
||||
onEvent = { event -> handleWorkerEvent(event) { activeWorkers.remove(worker) } }
|
||||
onEvent = { event -> handleWorkerEvent(event, message) { activeWorkers.remove(worker) } }
|
||||
)
|
||||
activeWorkers.add(worker)
|
||||
worker.start()
|
||||
@@ -106,7 +107,7 @@ class CoreTestService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWorkerEvent(event: RealPingEvent, onWorkerDone: () -> Unit) {
|
||||
private fun handleWorkerEvent(event: RealPingEvent, message: TestServiceMessage, onWorkerDone: () -> Unit) {
|
||||
when (event) {
|
||||
is RealPingEvent.Progress -> {
|
||||
NotificationHelper.updateNotification(
|
||||
@@ -124,6 +125,16 @@ class CoreTestService : Service() {
|
||||
}
|
||||
|
||||
is RealPingEvent.Finish -> {
|
||||
if(message.subscriptionId.isNotEmpty()){
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_REMOVE_INVALID_AFTER_TEST, false)) {
|
||||
AngConfigManager.removeInvalidServer(message.subscriptionId)
|
||||
}
|
||||
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_SORT_AFTER_TEST, false)) {
|
||||
AngConfigManager.sortByTestResultsForSub(message.subscriptionId)
|
||||
}
|
||||
}
|
||||
|
||||
MessageHelper.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_FINISH, event.status)
|
||||
onWorkerDone()
|
||||
if (activeWorkers.isEmpty()) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.v2ray.ang.ui.compose
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
@@ -9,6 +8,8 @@ import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -22,6 +23,9 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.currentStateAsState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@@ -43,8 +47,9 @@ private fun <T> rememberScrollbarAlpha(
|
||||
key: Any,
|
||||
config: ScrollbarConfig,
|
||||
position: () -> T,
|
||||
): Animatable<Float, AnimationVector1D> {
|
||||
): State<Float> {
|
||||
val alpha = remember(key) { Animatable(0f) }
|
||||
val lifecycleState = LocalLifecycleOwner.current.lifecycle.currentStateAsState()
|
||||
LaunchedEffect(key, config.fadeOutDurationMs, config.fadeAnimDurationMs) {
|
||||
snapshotFlow(position).collectLatest {
|
||||
alpha.snapTo(1f)
|
||||
@@ -52,7 +57,11 @@ private fun <T> rememberScrollbarAlpha(
|
||||
alpha.animateTo(0f, tween(config.fadeAnimDurationMs))
|
||||
}
|
||||
}
|
||||
return alpha
|
||||
return remember(alpha, lifecycleState) {
|
||||
derivedStateOf {
|
||||
if (lifecycleState.value == Lifecycle.State.RESUMED) alpha.value else 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -154,9 +154,19 @@ abstract class BaseServerActivity : BaseComponentActivity() {
|
||||
else -> R.string.server_lab_head_type
|
||||
}
|
||||
),
|
||||
state.headerType,
|
||||
when (state.network) {
|
||||
NetworkType.GRPC.type -> state.mode
|
||||
NetworkType.XHTTP.type -> state.xhttpMode
|
||||
else -> state.headerType
|
||||
},
|
||||
headerOptions,
|
||||
{ state.headerType = it }
|
||||
{
|
||||
when (state.network) {
|
||||
NetworkType.GRPC.type -> state.mode = it
|
||||
NetworkType.XHTTP.type -> state.xhttpMode = it
|
||||
else -> state.headerType = it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -173,25 +183,26 @@ abstract class BaseServerActivity : BaseComponentActivity() {
|
||||
else -> R.string.server_lab_request_host6
|
||||
}
|
||||
),
|
||||
state.host,
|
||||
{ state.host = it }
|
||||
if (state.network == NetworkType.GRPC.type) state.authority else state.host,
|
||||
{ if (state.network == NetworkType.GRPC.type) state.authority = it else state.host = it }
|
||||
)
|
||||
|
||||
FormTextField(
|
||||
stringResource(
|
||||
when (state.network) {
|
||||
NetworkType.KCP.type -> R.string.server_lab_path_kcp
|
||||
NetworkType.WS.type -> R.string.server_lab_path_ws
|
||||
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
|
||||
NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
|
||||
NetworkType.H2.type -> R.string.server_lab_path_h2
|
||||
NetworkType.GRPC.type -> R.string.server_lab_path_grpc
|
||||
else -> R.string.server_lab_path
|
||||
}
|
||||
),
|
||||
state.path,
|
||||
{ state.path = it }
|
||||
)
|
||||
if (state.network != NetworkType.KCP.type) {
|
||||
FormTextField(
|
||||
stringResource(
|
||||
when (state.network) {
|
||||
NetworkType.WS.type -> R.string.server_lab_path_ws
|
||||
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
|
||||
NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
|
||||
NetworkType.H2.type -> R.string.server_lab_path_h2
|
||||
NetworkType.GRPC.type -> R.string.server_lab_path_grpc
|
||||
else -> R.string.server_lab_path
|
||||
}
|
||||
),
|
||||
if (state.network == NetworkType.GRPC.type) state.serviceName else state.path,
|
||||
{ if (state.network == NetworkType.GRPC.type) state.serviceName = it else state.path = it }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.network == NetworkType.XHTTP.type) {
|
||||
FormTextField(
|
||||
@@ -201,6 +212,11 @@ abstract class BaseServerActivity : BaseComponentActivity() {
|
||||
)
|
||||
}
|
||||
if (state.network == NetworkType.KCP.type) {
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_path_kcp),
|
||||
state.seed,
|
||||
{ state.seed = it }
|
||||
)
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_kcp_mtu),
|
||||
state.kcpMtu,
|
||||
|
||||
@@ -37,10 +37,15 @@ class ServerUiState(
|
||||
bandwidthUp: String = "",
|
||||
network: String = NetworkType.TCP.type,
|
||||
headerType: String = "none",
|
||||
mode: String = "",
|
||||
xhttpMode: String = "",
|
||||
serviceName: String = "",
|
||||
authority: String = "",
|
||||
host: String = "",
|
||||
path: String = "",
|
||||
xhttpExtra: String = "",
|
||||
finalMask: String = "",
|
||||
seed: String = "",
|
||||
kcpMtu: String = "",
|
||||
kcpTti: String = "",
|
||||
browserDialerMode: String = "",
|
||||
@@ -80,10 +85,15 @@ class ServerUiState(
|
||||
var bandwidthUp by mutableStateOf(bandwidthUp)
|
||||
var network by mutableStateOf(network)
|
||||
var headerType by mutableStateOf(headerType)
|
||||
var mode by mutableStateOf(mode)
|
||||
var xhttpMode by mutableStateOf(xhttpMode)
|
||||
var serviceName by mutableStateOf(serviceName)
|
||||
var authority by mutableStateOf(authority)
|
||||
var host by mutableStateOf(host)
|
||||
var path by mutableStateOf(path)
|
||||
var xhttpExtra by mutableStateOf(xhttpExtra)
|
||||
var finalMask by mutableStateOf(finalMask)
|
||||
var seed by mutableStateOf(seed)
|
||||
var kcpMtu by mutableStateOf(kcpMtu)
|
||||
var kcpTti by mutableStateOf(kcpTti)
|
||||
var browserDialerMode by mutableStateOf(browserDialerMode)
|
||||
@@ -139,10 +149,15 @@ class ServerUiState(
|
||||
bandwidthUp = if (isHysteria2) bandwidthUp else null,
|
||||
network = network,
|
||||
headerType = headerType,
|
||||
mode = mode.nullIfBlank(),
|
||||
xhttpMode = xhttpMode.nullIfBlank(),
|
||||
serviceName = serviceName.nullIfBlank(),
|
||||
authority = authority.nullIfBlank(),
|
||||
host = host,
|
||||
path = path,
|
||||
xhttpExtra = xhttpExtra.nullIfBlank(),
|
||||
finalMask = finalMask.nullIfBlank(),
|
||||
seed = seed.nullIfBlank(),
|
||||
kcpMtu = kcpMtu.toIntOrNull(),
|
||||
kcpTti = kcpTti.toIntOrNull(),
|
||||
browserDialerMode = if (network in listOf(NetworkType.WS.type, NetworkType.XHTTP.type)) {
|
||||
@@ -191,10 +206,15 @@ class ServerUiState(
|
||||
bandwidthUp = initialConfig.bandwidthUp ?: "",
|
||||
network = initialConfig.network ?: NetworkType.TCP.type,
|
||||
headerType = initialConfig.headerType ?: "none",
|
||||
mode = initialConfig.mode ?: "",
|
||||
xhttpMode = initialConfig.xhttpMode ?: "",
|
||||
serviceName = initialConfig.serviceName ?: "",
|
||||
authority = initialConfig.authority ?: "",
|
||||
host = initialConfig.host ?: "",
|
||||
path = initialConfig.path ?: "",
|
||||
xhttpExtra = initialConfig.xhttpExtra ?: "",
|
||||
finalMask = initialConfig.finalMask ?: "",
|
||||
seed = initialConfig.seed ?: "",
|
||||
kcpMtu = initialConfig.kcpMtu?.toString() ?: "",
|
||||
kcpTti = initialConfig.kcpTti?.toString() ?: "",
|
||||
browserDialerMode = initialConfig.browserDialerMode ?: "",
|
||||
|
||||
@@ -7,14 +7,19 @@ import com.v2ray.ang.dto.SubscriptionUpdateMessage
|
||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.extension.moveItem
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.helper.MessageHelper
|
||||
import com.v2ray.ang.ui.base.BaseViewModel
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SubscriptionsViewModel(application: Application) : BaseViewModel(application) {
|
||||
private val subscriptions: MutableList<SubscriptionCache> =
|
||||
@@ -59,6 +64,45 @@ class SubscriptionsViewModel(application: Application) : BaseViewModel(applicati
|
||||
}
|
||||
|
||||
fun updateSubscriptions() {
|
||||
val updateSubscription = MmkvManager.decodeSettingsBool(AppConfig.PREF_UPDATE_SUBSCRIPTION, false)
|
||||
val autoTestAfterUpdateSubscription = MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_TEST_AFTER_UPDATE_SUBSCRIPTION, false)
|
||||
|
||||
when {
|
||||
// If auto test is enabled, trigger background service for long-running task
|
||||
autoTestAfterUpdateSubscription -> updateSubscriptionsMore()
|
||||
// If only update is enabled, perform local update with UI loading state
|
||||
updateSubscription -> updateSubscriptionsOnly()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSubscriptionsOnly() {
|
||||
launchLoading {
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
AngConfigManager.updateConfigViaSubAll()
|
||||
}
|
||||
|
||||
when {
|
||||
result.successCount + result.failureCount + result.skipCount == 0 ->
|
||||
toast(R.string.title_update_subscription_no_subscription)
|
||||
|
||||
result.successCount > 0 && result.failureCount + result.skipCount == 0 ->
|
||||
toast(getString(R.string.title_update_config_count, result.configCount))
|
||||
|
||||
else ->
|
||||
toast(getString(R.string.title_update_subscription_result, result.configCount, result.successCount, result.failureCount, result.skipCount))
|
||||
}
|
||||
reload()
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Subscription update failed", e)
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSubscriptionsMore() {
|
||||
SettingsChangeManager.makeSetupGroupTab()
|
||||
val subIds = MmkvManager.decodeSubscriptions()
|
||||
.filter { it.subscription.enabled && it.subscription.url.isNotEmpty() }
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
|
||||
<string name="subscription_updater_job_tips">任务将在后台运行,在通知中会显示进度信息</string>
|
||||
<string name="title_pref_auto_test_after_update_subscription">更新订阅后自动测试</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">自动测试需要比较长的时间,请耐心等待</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">自动测试需要比较长的时间,开启后任务将在后台运行;不开启则在当前页面运行</string>
|
||||
<string name="title_pref_auto_remove_invalid_after_test">测试后自动删除无效配置</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">测试结果可能不准确且已删除的配置无法恢复</string>
|
||||
<string name="title_pref_auto_sort_after_test">测试后自动排序</string>
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
|
||||
<string name="subscription_updater_job_tips">任務將在背景運行,在通知中會顯示進度資訊</string>
|
||||
<string name="title_pref_auto_test_after_update_subscription">更新訂閱後自動測試</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">自動測試需要比較長的時間,請耐心等待</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">自動測試需要較長的時間,開啟後任務將在後台運行;不開啟則在目前頁面運行</string>
|
||||
<string name="title_pref_auto_remove_invalid_after_test">測試後自動刪除無效配置</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">測試結果可能不準確且已刪除的配置無法復原</string>
|
||||
<string name="title_pref_auto_sort_after_test">測試後自動排序</string>
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
<string name="subscription_updater_job_tips">The task will run in the background, and progress information will be displayed in the notification.</string>
|
||||
<string name="title_pref_auto_test_after_update_subscription">Auto test after updating subscription</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">Automated testing takes a relatively long time, please be patient.</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">Automated testing takes a long time. If enabled, the task will run in the background; otherwise, it will run on the current page.</string>
|
||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||
|
||||
Reference in new issue
Block a user