Compare commits

...

23 Commits

Author SHA1 Message Date
2dust cf33391785 up 2.1.4 2026-04-30 11:30:31 +08:00
dependabot[bot] a35ba5dfbd Bump timheuer/base64-to-file from 1.2.4 to 2.0.0 (#5575)
Bumps [timheuer/base64-to-file](https://github.com/timheuer/base64-to-file) from 1.2.4 to 2.0.0.
- [Release notes](https://github.com/timheuer/base64-to-file/releases)
- [Commits](https://github.com/timheuer/base64-to-file/compare/v1.2.4...v2.0.0)

---
updated-dependencies:
- dependency-name: timheuer/base64-to-file
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 10:52:30 +08:00
Copilot 3b4d895c1d Refactor: centralize subscription auto-update scheduling in SubscriptionUpdater (#5567)
* Initial plan

* Refactor subscription auto-update system into SubscriptionUpdater facade

Agent-Logs-Url: https://github.com/2dust/v2rayNG/sessions/63125c89-4db9-4572-8adb-10c29ea531f5

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>

* Add TODO comment to legacy fallback in SubscriptionUpdater

Agent-Logs-Url: https://github.com/2dust/v2rayNG/sessions/63125c89-4db9-4572-8adb-10c29ea531f5

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>

* Use per-subscription auto-update intervals

Migrate subscription auto-update from a global toggle/interval to per-subscription scheduling.

- Add per-subscription updateInterval (Long, default 1440 minutes) and persist in SubEdit UI; validate minimum interval (15 minutes).
- Update SubscriptionUpdater to schedule/cancel tasks per subscription, compute initial delay from subscription.lastUpdated, and skip when autoUpdate is disabled or subId is missing.
- Remove legacy global auto-update prefs and fallback logic: deleted global prefs/UI handling, MmkvManager last-attempt encode/decode, and related default setting.
- Ensure cancelling of scheduled task when removing a subscription and adjust SettingsManager initialization.
- Add localized toast strings for invalid update interval and update related layouts/resources.

This change enables individual update intervals per subscription and simplifies the updater by removing legacy global state and last-attempt persistence.

* Allow optional force-reschedule for subscriptions

Add a forceReschedule flag to SubscriptionUpdater.sync to choose the WorkManager policy (KEEP by default, REPLACE when forced). Refactor scheduleOne to accept an ExistingPeriodicWorkPolicy and use it when enqueuing periodic work; syncOne now forces REPLACE so a manual refresh recalculates next run. Remove the cancelAll helper and improve logging to include the reschedule flag/policy. This lets startup keep existing periodic work while allowing explicit reschedules when needed (e.g., after manual updates).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-04-30 10:52:14 +08:00
2dust e8585a82c6 Remove echForceQuery
Remove echForceQuery support across the app: delete the echForceQuery fields from ProfileItem and V2rayConfig, stop mapping it in V2rayConfigManager, and remove the related UI spinner and containers in ServerActivity and layout_tls.xml. Also delete the ech_force_query_value array and all localized string entries for the ECH force query label, and adjust focus navigation to skip the removed control. This cleans up a deprecated/unused ECH force query setting.
2026-04-29 20:53:28 +08:00
Copilot 087c08f95a fix: immediately reconfigure WorkManager when auto-update interval changes (#5564)
* Initial plan

* fix: reconfigure WorkManager task when auto-update interval changes

Agent-Logs-Url: https://github.com/2dust/v2rayNG/sessions/2fd439e4-ecb0-4594-a90f-fbfe1e81b021

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-04-27 20:59:02 +08:00
Hossein Abaspanah 6612ca8256 Update Luri Bakhtiari translation (#5554) 2026-04-27 18:55:03 +08:00
solokot 487692deb7 Update Russian translation (#5553) 2026-04-27 18:54:47 +08:00
2dust e9332168bf up 2.1.3 2026-04-26 11:23:14 +08:00
2dust 15fcfec4c8 Update AndroidLibXrayLite 2026-04-26 11:19:29 +08:00
hippo2025 a4baeeb0fa Add options to turn off local proxy completely or turn off UDP in SOCKS5 (#5548)
* Add options to turn off local proxy completely or turn off UDP in SOCKS5.

* Improve consistency of code style

* Auto-disable additional http proxy when local proxy is disabled

* Add local proxy summary and wire to prefs

* Update V2rayNG/app/src/main/res/values/strings.xml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: hippo2025 <4xcw60t6e@mozmail.com>
Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-26 11:14:00 +08:00
2dust 093e51186d Upgrade Android Gradle Plugin and Gradle wrapper
Bump AGP to 9.2.0 in gradle/libs.versions.toml and update Gradle wrapper distribution to gradle-9.4.1 in gradle/wrapper/gradle-wrapper.properties. Keeps the build tooling up to date to take advantage of Gradle/AGP fixes and improvements.

Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com>
2026-04-25 20:11:03 +08:00
2dust 14a29884e7 Disable VPN session name setting
Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com>
2026-04-25 20:10:33 +08:00
solokot 30b0bd8d0d Update Russian translation (#5538)
* Update Russian translation

* Update Russian translation
2026-04-25 19:56:04 +08:00
hippo2025 ae84df7367 Add MTU and TTI settings in KCP network (#5539)
* Add MTU and TTI settings in KCP network

* Rename ProfileItem.tti to kcpTti

Rename the ProfileItem field `tti` to `kcpTti` and update all usages accordingly. Changes update parsing/serialization in FmtBase (map query param "tti" to config.kcpTti and emit it), KCP stream settings mapping in V2rayConfigManager, UI bindings in ServerActivity, and equality checks in ProfileItem to use kcpTti. This clarifies that the field applies to KCP-specific TTI values while preserving the external query key.

Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com>

* Add kcpMtu/kcpTti for KCP transport

Introduce kcpMtu and kcpTti properties on ProfileItem and update equality checks. Parse and emit KCP-specific mtu/tti in FmtBase (use kcpMtu/kcpTti instead of the generic mtu). Wire kcpMtu into V2rayConfigManager when building KCP stream settings and update ServerActivity to read/write the UI field to profileItem.kcpMtu/kcpTti. This separates KCP MTU/TTI from the general mtu field and removes a duplicated kcpTti entry.

Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com>

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-04-25 19:55:44 +08:00
hippo2025 b673167731 Return null in IP test if http port is 0 (#5537)
new name

Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com>
2026-04-25 19:13:23 +08:00
2dust 659f2ded09 up 2.1.2 2026-04-24 10:52:42 +08:00
2dust 585d64b0be Bump Android Gradle Plugin to 9.1.1
Update agp from 9.1.0 to 9.1.1 in V2rayNG/gradle/libs.versions.toml to pick up the latest patch fixes and keep the build tooling current.
2026-04-24 10:52:02 +08:00
fanymagnet 2ae2e747e2 Replace package names on UIDs in custom config (#5527)
* Replace package names on UIDs in custom config

* Update V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix copilot review & detect tun optimization

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-24 10:39:19 +08:00
2dust 4199cee4e5 Use OkHttp with proxy auth and support SOCKS creds
Replace HttpURLConnection-based networking with OkHttp and add proxy username/password support. Introduces buildOkHttpClient, updated getUrlContent/getUrlContentWithUserAgent signatures, and downloadToFile; callers in AngConfigManager, SpeedtestManager, UpdateCheckerManager, PerAppProxyActivity, UserAssetActivity and UserAssetViewModel now pass SOCKS proxy credentials. Removes createProxyConnection and related URL-resolve logic, clears HTTP inbound auth/udp in V2rayConfigManager, and simplifies SettingsManager port generation. These changes enable authenticated proxy requests, improved redirect handling and more robust file downloads.
2026-04-23 20:40:17 +08:00
dependabot[bot] 8be8a912c4 Bump robinraju/release-downloader from 1.12 to 1.13 (#5520)
Bumps [robinraju/release-downloader](https://github.com/robinraju/release-downloader) from 1.12 to 1.13.
- [Release notes](https://github.com/robinraju/release-downloader/releases)
- [Commits](https://github.com/robinraju/release-downloader/compare/v1.12...v1.13)

---
updated-dependencies:
- dependency-name: robinraju/release-downloader
  dependency-version: '1.13'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 19:44:50 +08:00
Kapkap5454 851f22680a Add negative UID handling to process routing (#5509)
* Handle unidentified package UID resolution

Add special handling for unidentified package names.

* Add method to create special unidentified app item

Added a method to create a special unidentified app item for the app picker.

* Support unidentified app UID and label

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-04-19 19:48:59 +08:00
2dust 12abc99bfc Update hev-socks5-tunnel 2026-04-19 19:31:11 +08:00
2dust 07e50637a2 Update AndroidLibXrayLite 2026-04-19 19:30:40 +08:00
45 changed files with 707 additions and 405 deletions
+2 -2
View File
@@ -70,7 +70,7 @@ jobs:
popd
- name: Download libv2ray
uses: robinraju/release-downloader@v1.12
uses: robinraju/release-downloader@v1.13
with:
repository: '2dust/AndroidLibXrayLite'
tag: ${{ env.CURRENT_TAG }}
@@ -84,7 +84,7 @@ jobs:
java-version: '21'
- name: Decode Keystore
uses: timheuer/base64-to-file@v1.2.4
uses: timheuer/base64-to-file@v2.0.0
id: android_keystore
with:
fileName: "android_keystore.jks"
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 24
targetSdk = 36
versionCode = 721
versionName = "2.1.1"
versionCode = 724
versionName = "2.1.4"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
+5
View File
@@ -273,6 +273,11 @@
</provider>
<service
android:name="androidx.work.multiprocess.RemoteWorkManagerService"
android:exported="false"
android:process=":bg" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.cache"
@@ -43,10 +43,8 @@ object AppConfig {
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
const val SUBSCRIPTION_MIN_INTERVAL_MINUTES = 15L
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
@@ -58,10 +56,12 @@ object AppConfig {
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
const val PREF_ENABLE_LOCAL_PROXY = "pref_enable_local_proxy"
const val PREF_SOCKS_PORT = "pref_socks_port"
const val PREF_DYNAMIC_SOCKS_PORT = "pref_dynamic_socks_port"
const val PREF_SOCKS_USERNAME = "pref_socks_username"
const val PREF_SOCKS_PASSWORD = "pref_socks_password"
const val PREF_SOCKS_ENABLE_UDP = "pref_socks_enable_udp"
const val PREF_REMOTE_DNS = "pref_remote_dns"
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
const val PREF_DNS_HOSTS = "pref_dns_hosts"
@@ -219,6 +219,8 @@ object AppConfig {
const val REALITY = "reality"
const val HEADER_TYPE_HTTP = "http"
const val UNIDENTIFIED_PACKAGE = "__unknown_app__"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5")
@@ -29,6 +29,9 @@ data class ProfileItem(
var host: String? = null,
var path: String? = null,
var seed: String? = null,
var kcpMtu: Int? = null,
var kcpTti: Int? = null,
var quicSecurity: String? = null,
var quicKey: String? = null,
var mode: String? = null,
@@ -43,7 +46,6 @@ data class ProfileItem(
var fingerPrint: String? = null,
var insecure: Boolean? = null,
var echConfigList: String? = null,
var echForceQuery: String? = null,
var pinnedCA256: String? = null,
var publicKey: String? = null,
@@ -102,6 +104,8 @@ data class ProfileItem(
&& this.host == obj.host
&& this.path == obj.path
&& this.seed == obj.seed
&& this.kcpMtu == obj.kcpMtu
&& this.kcpTti == obj.kcpTti
&& this.quicSecurity == obj.quicSecurity
&& this.quicKey == obj.quicKey
&& this.mode == obj.mode
@@ -7,7 +7,7 @@ data class SubscriptionItem(
val addedTime: Long = System.currentTimeMillis(),
var lastUpdated: Long = -1,
var autoUpdate: Boolean = false,
val updateInterval: Int? = null,
var updateInterval: Long = 1440, // in minutes, default to 24 hours
var prevProfile: String? = null,
var nextProfile: String? = null,
var filter: String? = null,
@@ -256,7 +256,6 @@ data class V2rayConfig(
val disableSystemRoot: Boolean? = null,
val enableSessionResumption: Boolean? = null,
var echConfigList: String? = null,
var echForceQuery: String? = null,
var pinnedPeerCertSha256: String? = null,
// REALITY settings
val show: Boolean = false,
@@ -60,6 +60,8 @@ open class FmtBase {
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.kcpMtu = queryParam["mtu"]?.toIntOrNull()
config.kcpTti = queryParam["tti"]?.toIntOrNull()
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
@@ -112,6 +114,8 @@ open class FmtBase {
config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it }
config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it }
config.finalMask?.nullIfBlank()?.let { dicQuery["fm"] = it }
config.kcpMtu?.let { dicQuery["mtu"] = it.toString() }
config.kcpTti?.let { dicQuery["tti"] = it.toString() }
// Add two keys for compatibility: "insecure" and "allowInsecure"
if (config.security == AppConfig.TLS) {
val insecureFlag = if (config.insecure == true) "1" else "0"
@@ -532,10 +532,12 @@ object AngConfigManager {
}
LogUtil.i(AppConfig.TAG, url)
val userAgent = it.subscription.userAgent
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var configText = try {
val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort)
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort, proxyUsername, proxyPassword)
} catch (e: Exception) {
LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
""
@@ -679,6 +679,7 @@ object MmkvManager {
return settingsStorage.decodeStringSet(key)
}
/**
* Encodes the start on boot setting.
*
@@ -252,12 +252,7 @@ object SettingsManager {
* it creates a new default subscription to ensure that ungroup
**/
fun removeSubscriptionWithDefault(subid: String) {
// val subsList = decodeSubsList()
// if (subsList.size == 1 && subsList.first() == DEFAULT_SUBSCRIPTION_ID) {
// LogUtil.i(ANG_PACKAGE,"Attempted to remove the only existing default subscription, operation ignored.")
// return
// }
SubscriptionUpdater.cancelOne(subId = subid)
// Remove the subscription
removeSubscription(subid)
@@ -317,11 +312,7 @@ object SettingsManager {
}
private fun generateRandomSocksPort(): Int {
return if (Utils.isXray()) {
Random.nextInt(10000, 65536)
} else {
Random.nextInt(10000, 65535)
}
return Random.nextInt(10000, 65535)
}
/**
@@ -503,7 +494,6 @@ object SettingsManager {
ensureDefaultValue(AppConfig.PREF_MODE, VPN)
ensureDefaultValue(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
ensureDefaultValue(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
ensureDefaultValue(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
ensureDefaultValue(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
ensureDefaultValue(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
ensureDefaultValue(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
@@ -1,6 +1,5 @@
package com.v2ray.ang.handler
import android.content.Context
import android.os.SystemClock
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
@@ -83,49 +82,15 @@ object SpeedtestManager {
}
}
/**
* Tests the connection to a given URL and port.
*
* @param context The Context in which the test is running.
* @param port The port to connect to.
* @return A pair containing the elapsed time in milliseconds and the result message.
*/
fun testConnection(context: Context, port: Int): Pair<Long, String> {
var result: String
var elapsed = -1L
val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "")
try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
elapsed = SystemClock.elapsedRealtime() - start
result = when (code) {
204 -> context.getString(R.string.connection_test_available, elapsed)
200 if conn.contentLengthLong == 0L -> context.getString(R.string.connection_test_available, elapsed)
else -> throw IOException(
context.getString(R.string.connection_test_error_status_code, code)
)
}
} catch (e: IOException) {
LogUtil.e(AppConfig.TAG, "Connection test IOException", e)
result = context.getString(R.string.connection_test_error, e.message)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Connection test Exception", e)
result = context.getString(R.string.connection_test_error, e.message)
} finally {
conn.disconnect()
}
return Pair(elapsed, result)
}
fun getRemoteIPInfo(): String? {
val url = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL)
.takeIf { !it.isNullOrBlank() } ?: AppConfig.IP_API_URL
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
val content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: return null
if (httpPort == 0) return null
val content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: return null
val ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
val ip = listOf(
@@ -7,14 +7,149 @@ import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import androidx.work.multiprocess.RemoteWorkManager
import androidx.work.workDataOf
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.util.LogUtil
import java.util.concurrent.TimeUnit
object SubscriptionUpdater {
// -------------------------------------------------------------------------
// Public API — the only methods external callers should ever use
// -------------------------------------------------------------------------
/**
* Sync all subscription tasks with current settings.
*
* Startup/boot callers should use the default mode so existing periodic work is kept.
* Use forceReschedule=true only when the next run time needs to be recalculated from
* the latest persisted subscription state (for example after a manual refresh).
* Call from: MainActivity.onCreate(), BootReceiver.onReceive().
*/
fun sync(
context: Context = AngApplication.application,
forceReschedule: Boolean = false
) {
val existingWorkPolicy =
if (forceReschedule) {
ExistingPeriodicWorkPolicy.REPLACE
} else {
ExistingPeriodicWorkPolicy.KEEP
}
MmkvManager.decodeSubscriptions().forEach { sub ->
scheduleOne(
context = context,
subId = sub.guid,
shouldRun = sub.subscription.autoUpdate,
existingWorkPolicy = existingWorkPolicy
)
}
LogUtil.i(
AppConfig.TAG,
"SubscriptionUpdater: sync complete forceReschedule=$forceReschedule"
)
}
/**
* Sync a single subscription's task.
* Call from: SubEditActivity after saving, after a manual update (to reset the timer).
*/
fun syncOne(context: Context = AngApplication.application, subId: String) {
val subItem = MmkvManager.decodeSubscription(subId) ?: return
scheduleOne(
context = context,
subId = subId,
shouldRun = subItem.autoUpdate,
existingWorkPolicy = ExistingPeriodicWorkPolicy.REPLACE
)
}
/**
* Cancel the auto-update task for a single subscription.
* Call from: when a subscription is deleted.
*/
fun cancelOne(context: Context = AngApplication.application, subId: String) {
RemoteWorkManager.getInstance(context)
.cancelUniqueWork(taskName(subId))
}
// -------------------------------------------------------------------------
// Internal scheduling logic
// -------------------------------------------------------------------------
private fun taskName(subId: String) = "${AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME}_$subId"
private fun scheduleOne(
context: Context,
subId: String,
shouldRun: Boolean,
existingWorkPolicy: ExistingPeriodicWorkPolicy
) {
val rw = RemoteWorkManager.getInstance(context)
if (!shouldRun) {
rw.cancelUniqueWork(taskName(subId))
LogUtil.d(AppConfig.TAG, "SubscriptionUpdater: cancelled task for $subId")
return
}
val subItem = MmkvManager.decodeSubscription(subId) ?: return
val intervalMinutes = maxOf(
AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES,
subItem.updateInterval
)
// Base initial delay on the last successful update time persisted in subscription.
val lastUpdated = subItem.lastUpdated
val intervalMillis = intervalMinutes * 60 * 1000L
val now = System.currentTimeMillis()
val initialDelayMillis = if (lastUpdated <= 0L) {
0L
} else {
maxOf(0L, lastUpdated + intervalMillis - now)
}
val request = PeriodicWorkRequestBuilder<UpdateTask>(intervalMinutes, TimeUnit.MINUTES)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setInputData(workDataOf(KEY_SUB_ID to subId))
.setInitialDelay(initialDelayMillis, TimeUnit.MILLISECONDS)
.addTag(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
.build()
rw.enqueueUniquePeriodicWork(
taskName(subId),
existingWorkPolicy,
request
)
LogUtil.i(
AppConfig.TAG,
"SubscriptionUpdater: scheduled [$subId] interval=${intervalMinutes}min " +
"initialDelay=${initialDelayMillis / 1000}s policy=$existingWorkPolicy"
)
}
// -------------------------------------------------------------------------
// Worker
// -------------------------------------------------------------------------
private const val KEY_SUB_ID = "subId"
class UpdateTask(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
@@ -23,39 +158,50 @@ object SubscriptionUpdater {
NotificationCompat.Builder(applicationContext, AppConfig.SUBSCRIPTION_UPDATE_CHANNEL)
.setWhen(0)
.setTicker("Update")
.setContentTitle(context.getString(R.string.title_pref_auto_update_subscription))
.setContentTitle(applicationContext.getString(R.string.title_pref_auto_update_subscription))
.setSmallIcon(R.drawable.ic_stat_name)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
/**
* Performs the subscription update work.
* @return The result of the work.
*/
@SuppressLint("MissingPermission")
override suspend fun doWork(): Result {
LogUtil.i(AppConfig.TAG, "subscription automatic update starting")
val subId = inputData.getString(KEY_SUB_ID)
LogUtil.i(AppConfig.TAG, "SubscriptionUpdater automatic update starting: $subId")
val subs = MmkvManager.decodeSubscriptions().filter { it.subscription.autoUpdate }
for (sub in subs) {
val subItem = sub.subscription
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setChannelId(AppConfig.SUBSCRIPTION_UPDATE_CHANNEL)
val channel =
NotificationChannel(
AppConfig.SUBSCRIPTION_UPDATE_CHANNEL,
AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME,
NotificationManager.IMPORTANCE_MIN
)
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(3, notification.build())
LogUtil.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
AngConfigManager.updateConfigViaSub(sub)
notification.setContentText("Updating ${subItem.remarks}")
if (subId.isNullOrEmpty()) {
LogUtil.w(AppConfig.TAG, "SubscriptionUpdater: missing subId in worker input")
return Result.success()
}
val subItem = MmkvManager.decodeSubscription(subId)
if (subItem == null) {
LogUtil.w(AppConfig.TAG, "SubscriptionUpdater: no subscription found for $subId")
return Result.success()
}
if (!subItem.autoUpdate) {
LogUtil.i(AppConfig.TAG, "SubscriptionUpdater: auto-update disabled for $subId, skip")
return Result.success()
}
val sub = SubscriptionCache(subId, subItem)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
AppConfig.SUBSCRIPTION_UPDATE_CHANNEL,
AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME,
NotificationManager.IMPORTANCE_MIN
)
)
}
notificationManager.notify(3, notification.build())
LogUtil.i(AppConfig.TAG, "SubscriptionUpdater automatic update: ---${sub.subscription.remarks}")
AngConfigManager.updateConfigViaSub(sub)
notification.setContentText("Updating ${sub.subscription.remarks}")
notificationManager.cancel(3)
return Result.success()
}
@@ -23,10 +23,13 @@ object UpdateCheckerManager {
AppConfig.APP_API_URL.concatUrl("latest")
}
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var response = HttpUtil.getUrlContent(url, 5000)
if (response.isNullOrEmpty()) {
val httpPort = SettingsManager.getHttpPort()
response = HttpUtil.getUrlContent(url, 5000, httpPort)
response = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword)
?: throw IllegalStateException("Failed to get response")
}
@@ -61,39 +64,6 @@ object UpdateCheckerManager {
}
}
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
try {
val httpPort = SettingsManager.getHttpPort()
val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
?: throw IllegalStateException("Failed to create connection")
try {
val apkFile = File(context.cacheDir, "update.apk")
LogUtil.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
FileOutputStream(apkFile).use { outputStream ->
connection.inputStream.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
LogUtil.i(AppConfig.TAG, "APK download completed")
return@withContext apkFile
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
return@withContext null
} finally {
try {
connection.disconnect()
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Error closing connection: ${e.message}")
}
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
return@withContext null
}
}
private fun compareVersions(version1: String, version2: String): Int {
val v1 = version1.split(".")
val v2 = version2.split(".")
@@ -98,36 +98,46 @@ object V2rayConfigManager {
return result
}
// check if tun inbound exists
val json = JsonUtil.parseString(raw) ?: return result
val inboundsJson = if (json.has("inbounds") && json.get("inbounds")?.isJsonNull == false) {
json.getAsJsonArray("inbounds")
} else {
JsonArray()
}
val json = JsonUtil.parseString(raw)?.takeIf { it.isJsonObject }?.asJsonObject ?: return result
for (i in 0 until inboundsJson.size()) {
val elem = inboundsJson.get(i)
if (elem.isJsonObject) {
val inb = elem.asJsonObject
val tag = if (inb.has("tag") && inb.get("tag")?.isJsonNull == false) inb.get("tag").asString else ""
if (tag == "tun") return result
// Check whether package names need to be replaced with UIDs
if (SettingsManager.canUseProcessRouting()) {
val rulesJson = json.get("routing")?.takeIf { it.isJsonObject }?.asJsonObject
?.get("rules")?.takeIf { it.isJsonArray }?.asJsonArray
?: JsonArray()
for (elem in rulesJson) {
val rule = elem.takeIf { it.isJsonObject }?.asJsonObject ?: continue
val process = rule.get("process")?.takeIf { it.isJsonArray }?.asJsonArray ?: continue
val packages = process.mapNotNull {
it.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }?.asString
}.takeIf { it.isNotEmpty() } ?: continue
val uids = PackageUidResolver.packageNamesToUids(context, packages).takeIf { it.isNotEmpty() } ?: continue
rule.add("process", JsonArray().apply { uids.forEach { add(it) } })
}
}
// add tun inbound from template
val templateConfig = initV2rayConfig(context) ?: return result
val inboundTun = templateConfig.inbounds.firstOrNull { it.tag == "tun" } ?: return result
inboundTun.settings?.mtu = SettingsManager.getVpnMtu()
// add to json
inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun)))
if (inboundsJson.size() == 1) {
json.add("inbounds", inboundsJson)
// check if tun inbound exists
val inboundsJson = json.get("inbounds")?.takeIf { it.isJsonArray }?.asJsonArray
?: JsonArray().also { json.add("inbounds", it) }
val tunNotExists = inboundsJson.none { elem ->
elem.isJsonObject && elem.asJsonObject.get("protocol")
?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }
?.asString == "tun"
}
val updatedRaw = JsonUtil.toJsonPretty(json) ?: return result
return ConfigResult(true, guid, updatedRaw)
if (tunNotExists) {
// add tun inbound from template
initV2rayConfig(context)?.let { templateConfig ->
templateConfig.inbounds.firstOrNull { it.tag == "tun" }?.let { inboundTun ->
inboundTun.settings?.mtu = SettingsManager.getVpnMtu()
inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun)))
}
}
}
return JsonUtil.toJsonPretty(json)?.let { ConfigResult(true, guid, it) } ?: result
}
/**
@@ -374,6 +384,12 @@ object V2rayConfigManager {
*/
private fun getInbounds(v2rayConfig: V2rayConfig): Boolean {
try {
val vpn = SettingsManager.isVpnMode()
val useHev = SettingsManager.isUsingHevTun()
val forcedByHev = vpn && useHev
val enableLocalProxy = forcedByHev || MmkvManager.decodeSettingsBool(AppConfig.PREF_ENABLE_LOCAL_PROXY, true)
val socksPort = SettingsManager.getSocksPort()
val socksUsername = SettingsManager.getSocksUsername()
val socksPassword = SettingsManager.getSocksPassword()
@@ -386,6 +402,7 @@ object V2rayConfigManager {
inbound1.listen = AppConfig.LOOPBACK
}
inbound1.port = socksPort
inbound1.settings?.udp = MmkvManager.decodeSettingsBool(AppConfig.PREF_SOCKS_ENABLE_UDP, true)
if (socksUsername != null && socksPassword != null) {
inbound1.settings?.auth = "password"
inbound1.settings?.accounts = listOf(
@@ -416,9 +433,15 @@ object V2rayConfigManager {
inbound2.tag = EConfigType.HTTP.name.lowercase()
inbound2.port = SettingsManager.getHttpPort()
inbound2.protocol = EConfigType.HTTP.name.lowercase()
inbound2.settings?.auth = null
inbound2.settings?.udp = null
v2rayConfig.inbounds.add(inbound2)
}
if (!enableLocalProxy) {
v2rayConfig.inbounds.removeIf { it.protocol == "socks" || it.protocol == "http" }
}
if (needTun()) {
val inboundTun = v2rayConfig.inbounds.firstOrNull { e -> e.tag == "tun" }
inboundTun?.settings?.mtu = SettingsManager.getVpnMtu()
@@ -1293,7 +1316,10 @@ object V2rayConfigManager {
}
NetworkType.KCP.type -> {
streamSettings.kcpSettings = StreamSettingsBean.KcpSettingsBean()
val kcpSetting = StreamSettingsBean.KcpSettingsBean()
profileItem.kcpMtu?.let { kcpSetting.mtu = it }
profileItem.kcpTti?.let { kcpSetting.tti = it }
streamSettings.kcpSettings = kcpSetting
val udpMaskList = mutableListOf<StreamSettingsBean.FinalMaskBean.MaskBean>()
if (!headerType.isNullOrEmpty() && headerType != "none") {
val kcpHeaderType = when {
@@ -1490,7 +1516,6 @@ object V2rayConfigManager {
fingerprint = profileItem.fingerPrint.nullIfBlank(),
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
echConfigList = profileItem.echConfigList.nullIfBlank(),
echForceQuery = profileItem.echForceQuery.nullIfBlank(),
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
publicKey = profileItem.publicKey.nullIfBlank(),
shortId = profileItem.shortId.nullIfBlank(),
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent
import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SubscriptionUpdater
import com.v2ray.ang.handler.V2RayServiceManager
import com.v2ray.ang.util.LogUtil
@@ -37,5 +38,6 @@ class BootReceiver : BroadcastReceiver() {
LogUtil.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service")
V2RayServiceManager.startVService(context)
SubscriptionUpdater.sync(context)
}
}
@@ -253,7 +253,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
}
builder.setSession(V2RayServiceManager.getRunningServerName())
//builder.setSession(V2RayServiceManager.getRunningServerName())
}
/**
@@ -1,5 +1,6 @@
package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
@@ -7,11 +8,13 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAppPickerBinding
import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.PackageUidResolver
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -101,6 +104,21 @@ class AppPickerActivity : BaseActivity() {
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
}
@SuppressLint("UseCompatLoadingForDrawables")
private fun createSpecialItemUnidentified(): AppInfo {
val icon = requireNotNull(
getDrawable(android.R.drawable.ic_menu_help)
?: getDrawable(android.R.drawable.sym_def_app_icon)
) { "No fallback drawable available" }
return AppInfo(
appName = getString(R.string.app_picker_unknown_app),
packageName = AppConfig.UNIDENTIFIED_PACKAGE,
appIcon = icon,
isSystemApp = false,
isSelected = 0
)
}
private fun loadApps() {
showLoading()
@@ -108,7 +126,8 @@ class AppPickerActivity : BaseActivity() {
try {
val apps = withContext(Dispatchers.IO) {
val appsList = AppManagerUtil.loadNetworkAppList(this@AppPickerActivity)
sortApps(appsList)
val sortedApps = sortApps(appsList)
listOf(createSpecialItemUnidentified()) + sortedApps
}
appsAll = apps
@@ -31,6 +31,7 @@ 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.handler.SubscriptionUpdater
import com.v2ray.ang.handler.V2RayServiceManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
@@ -98,6 +99,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
setupGroupTab()
setupViewModel()
SubscriptionUpdater.sync()
mainViewModel.reloadServerList()
checkAndRequestPermission(PermissionType.POST_NOTIFICATIONS) {
@@ -191,8 +191,10 @@ class PerAppProxyActivity : BaseActivity() {
lifecycleScope.launch(Dispatchers.IO) {
var content = HttpUtil.getUrlContent(url, 5000)
if (content.isNullOrEmpty()) {
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: ""
}
launch(Dispatchers.Main) {
//LogUtil.i(AppConfig.TAG, content)
@@ -84,9 +84,6 @@ class ServerActivity : BaseActivity() {
private val xhttpMode: Array<out String> by lazy {
resources.getStringArray(R.array.xhttp_mode)
}
private val echForceQuerys: Array<out String> by lazy {
resources.getStringArray(R.array.ech_force_query_value)
}
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
@@ -132,13 +129,14 @@ class ServerActivity : BaseActivity() {
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
private val et_kcp_mtu: EditText? by lazy { findViewById(R.id.et_kcp_mtu) }
private val et_kcp_tti: EditText? by lazy { findViewById(R.id.et_kcp_tti) }
private val layout_kcp: LinearLayout? by lazy { findViewById(R.id.layout_kcp) }
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
private val et_fm: EditText? by lazy { findViewById(R.id.et_fm) }
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
private val et_ech_config_list: EditText? by lazy { findViewById(R.id.et_ech_config_list) }
private val container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) }
private val sp_ech_force_query: Spinner? by lazy { findViewById(R.id.sp_ech_force_query) }
private val container_ech_force_query: LinearLayout? by lazy { findViewById(R.id.lay_ech_force_query) }
private val et_pinned_ca256: EditText? by lazy { findViewById(R.id.et_pinned_ca256) }
private val container_pinned_ca256: LinearLayout? by lazy { findViewById(R.id.lay_pinned_ca256) }
@@ -245,6 +243,14 @@ class ServerActivity : BaseActivity() {
)
et_fm?.text = Utils.getEditable(config?.finalMask)
layout_kcp?.visibility =
when (networks[position]) {
NetworkType.KCP.type -> View.VISIBLE
else -> View.GONE
}
et_kcp_mtu?.text = Utils.getEditable(config?.kcpMtu?.toString().orEmpty())
et_kcp_tti?.text = Utils.getEditable(config?.kcpTti?.toString().orEmpty())
layout_extra?.visibility =
when (networks[position]) {
NetworkType.XHTTP.type -> View.VISIBLE
@@ -279,7 +285,6 @@ class ServerActivity : BaseActivity() {
container_spider_x,
container_mldsa65_verify,
container_ech_config_list,
container_ech_force_query,
container_pinned_ca256
).forEach { it?.visibility = View.GONE }
}
@@ -292,7 +297,6 @@ class ServerActivity : BaseActivity() {
container_alpn,
container_allow_insecure,
container_ech_config_list,
container_ech_force_query,
container_pinned_ca256
).forEach { it?.visibility = View.VISIBLE }
listOf(
@@ -313,7 +317,6 @@ class ServerActivity : BaseActivity() {
container_alpn,
container_allow_insecure,
container_ech_config_list,
container_ech_force_query,
container_pinned_ca256
).forEach { it?.visibility = View.GONE }
listOf(
@@ -397,10 +400,6 @@ class ServerActivity : BaseActivity() {
sp_allow_insecure?.setSelection(allowinsecure)
}
et_ech_config_list?.text = Utils.getEditable(config.echConfigList)
config.echForceQuery?.let { it ->
val index = Utils.arrayFind(echForceQuerys, it)
index.let { sp_ech_force_query?.setSelection(if (it >= 0) it else 0) }
}
et_pinned_ca256?.text = Utils.getEditable(config.pinnedCA256)
} else if (config.security == REALITY) {
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
@@ -569,6 +568,8 @@ class ServerActivity : BaseActivity() {
profileItem.xhttpMode = transportTypes(networks[network])[type]
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim().nullIfBlank()
profileItem.finalMask = et_fm?.text?.toString()?.trim()?.nullIfBlank()
profileItem.kcpMtu = et_kcp_mtu?.text?.toString()?.toIntOrNull()
profileItem.kcpTti = et_kcp_tti?.text?.toString()?.toIntOrNull()
}
private fun saveTls(config: ProfileItem) {
@@ -582,7 +583,6 @@ class ServerActivity : BaseActivity() {
val spiderX = et_spider_x?.text?.toString()
val mldsa65Verify = et_mldsa65_verify?.text?.toString()
val echConfigList = et_ech_config_list?.text?.toString()
val echForceQueryIndex = sp_ech_force_query?.selectedItemPosition ?: 0
val pinnedCA256 = et_pinned_ca256?.text?.toString()
val allowInsecure =
@@ -602,7 +602,6 @@ class ServerActivity : BaseActivity() {
config.spiderX = spiderX
config.mldsa65Verify = mldsa65Verify
config.echConfigList = echConfigList
config.echForceQuery = echForceQuerys[echForceQueryIndex]
config.pinnedCA256 = pinnedCA256
}
@@ -6,19 +6,12 @@ import androidx.preference.CheckBoxPreference
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.multiprocess.RemoteWorkManager
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.R
import com.v2ray.ang.extension.toLongEx
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SubscriptionUpdater
import com.v2ray.ang.helper.MmkvPreferenceDataStore
import com.v2ray.ang.util.Utils
import java.util.concurrent.TimeUnit
class SettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -48,16 +41,20 @@ class SettingsActivity : BaseActivity() {
private val fragmentLength by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_LENGTH) }
private val fragmentInterval by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_INTERVAL) }
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
private val dynamicSocksPort by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_DYNAMIC_SOCKS_PORT) }
private val hevTunLogLevel by lazy { findPreference<ListPreference>(AppConfig.PREF_HEV_TUNNEL_LOGLEVEL) }
private val hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) }
private val useHevTun by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_USE_HEV_TUNNEL) }
private val enableLocalProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_ENABLE_LOCAL_PROXY) }
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
private val dynamicSocksPort by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_DYNAMIC_SOCKS_PORT) }
private val socksUsername by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_USERNAME) }
private val socksPassword by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PASSWORD) }
private val socksEnableUdp by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_SOCKS_ENABLE_UDP) }
private val proxySharing by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PROXY_SHARING) }
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
// Use MMKV as the storage backend for all Preferences
// This prevents inconsistencies between SharedPreferences and MMKV
@@ -90,15 +87,6 @@ class SettingsActivity : BaseActivity() {
true
}
autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean
autoUpdateCheck?.isChecked = value
autoUpdateInterval?.isEnabled = value
autoUpdateInterval?.text?.toLongEx()?.let {
if (newValue) configureUpdateTask(it) else cancelUpdateTask()
}
true
}
mode?.setOnPreferenceChangeListener { pref, newValue ->
val valueStr = newValue.toString()
(pref as? ListPreference)?.let { lp ->
@@ -115,6 +103,11 @@ class SettingsActivity : BaseActivity() {
true
}
enableLocalProxy?.setOnPreferenceChangeListener { _, newValue ->
updateEnableLocalProxy(newValue as Boolean)
true
}
dynamicSocksPort?.setOnPreferenceChangeListener { _, newValue ->
updateDynamicSocksPort(newValue as Boolean)
true
@@ -174,15 +167,15 @@ class SettingsActivity : BaseActivity() {
// Initialize mode-dependent UI states
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
// Initialize local proxy state
updateEnableLocalProxy(MmkvManager.decodeSettingsBool(AppConfig.PREF_ENABLE_LOCAL_PROXY, true))
// Initialize mux-dependent UI states
updateMux(MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false))
// Initialize fragment-dependent UI states
updateFragment(MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false))
// Initialize auto-update interval state
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
updateDynamicSocksPort(MmkvManager.decodeSettingsBool(AppConfig.PREF_DYNAMIC_SOCKS_PORT, false))
}
@@ -220,29 +213,6 @@ class SettingsActivity : BaseActivity() {
vpnDns?.isEnabled = !enabled
}
private fun configureUpdateTask(interval: Long) {
val rw = RemoteWorkManager.getInstance(AngApplication.application)
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
rw.enqueueUniquePeriodicWork(
AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
PeriodicWorkRequest.Builder(
SubscriptionUpdater.UpdateTask::class.java,
interval,
TimeUnit.MINUTES
)
.apply {
setInitialDelay(interval, TimeUnit.MINUTES)
}
.build()
)
}
private fun cancelUpdateTask() {
val rw = RemoteWorkManager.getInstance(AngApplication.application)
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
}
private fun updateMux(enabled: Boolean) {
muxConcurrency?.isEnabled = enabled
muxXudpConcurrency?.isEnabled = enabled
@@ -276,12 +246,44 @@ class SettingsActivity : BaseActivity() {
}
private fun updateDynamicSocksPort(enabled: Boolean) {
socksPort?.isEnabled = !enabled
socksPort?.isEnabled = (enableLocalProxy?.isChecked == true) && !enabled
}
private fun updateEnableLocalProxy(enabled: Boolean) {
val dynamic = MmkvManager.decodeSettingsBool(AppConfig.PREF_DYNAMIC_SOCKS_PORT, false)
socksPort?.isEnabled = enabled && !dynamic
dynamicSocksPort?.isEnabled = enabled
socksUsername?.isEnabled = enabled
socksPassword?.isEnabled = enabled
socksEnableUdp?.isEnabled = enabled
proxySharing?.isEnabled = enabled
if (!enabled) {
if (appendHttpProxy?.isChecked == true) {
appendHttpProxy?.isChecked = false
MmkvManager.encodeSettings(AppConfig.PREF_APPEND_HTTP_PROXY, false)
}
appendHttpProxy?.isEnabled = false
} else {
val vpn = MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) == VPN
appendHttpProxy?.isEnabled = vpn
}
}
private fun updateHevTunSettings(enabled: Boolean) {
hevTunLogLevel?.isEnabled = enabled
hevTunRwTimeout?.isEnabled = enabled
if (enabled) {
if (enableLocalProxy?.isChecked == false) {
enableLocalProxy?.isChecked = true
MmkvManager.encodeSettings(AppConfig.PREF_ENABLE_LOCAL_PROXY, true)
}
enableLocalProxy?.isEnabled = false
} else {
enableLocalProxy?.isEnabled = true
}
updateEnableLocalProxy(enableLocalProxy?.isChecked == true)
}
}
@@ -15,6 +15,7 @@ import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.SubscriptionUpdater
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -51,6 +52,7 @@ class SubEditActivity : BaseActivity() {
binding.etFilter.text = Utils.getEditable(subItem.filter)
binding.chkEnable.isChecked = subItem.enabled
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
binding.etUpdateInterval.text = Utils.getEditable(subItem.updateInterval.toString())
binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
@@ -65,6 +67,7 @@ class SubEditActivity : BaseActivity() {
binding.etUrl.text = null
binding.etFilter.text = null
binding.chkEnable.isChecked = true
binding.etUpdateInterval.text = null
binding.etPreProfile.text = null
binding.etNextProfile.text = null
return true
@@ -82,6 +85,27 @@ class SubEditActivity : BaseActivity() {
subItem.filter = binding.etFilter.text.toString()
subItem.enabled = binding.chkEnable.isChecked
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
val intervalInput = binding.etUpdateInterval.text.toString().trim()
val intervalMinutes = intervalInput.toLongOrNull()
if (subItem.autoUpdate) {
// autoUpdate is enabled: interval must be valid
if (intervalMinutes == null) {
// field is empty, reset to default
subItem.updateInterval = SubscriptionItem().updateInterval
} else if (intervalMinutes < AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES) {
toast(R.string.toast_invalid_update_interval)
return false
} else {
subItem.updateInterval = intervalMinutes
}
} else {
// autoUpdate is disabled: save only if the value is valid, otherwise keep the existing value
if (intervalMinutes != null && intervalMinutes >= AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES) {
subItem.updateInterval = intervalMinutes
}
}
subItem.prevProfile = binding.etPreProfile.text.toString()
subItem.nextProfile = binding.etNextProfile.text.toString()
subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked
@@ -105,6 +129,7 @@ class SubEditActivity : BaseActivity() {
}
MmkvManager.encodeSubscription(editSubId, subItem)
SubscriptionUpdater.syncOne(subId = editSubId)
toastSuccess(R.string.toast_success)
finish()
return true
@@ -173,9 +173,11 @@ class UserAssetActivity : HelperBaseActivity() {
showLoading()
toast(R.string.msg_downloading_content)
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
lifecycleScope.launch(Dispatchers.IO) {
val result = viewModel.downloadGeoFiles(extDir, httpPort)
val result = viewModel.downloadGeoFiles(extDir, httpPort, proxyUsername, proxyPassword)
withContext(Dispatchers.Main) {
if (result.successCount > 0) {
toast(getString(R.string.title_update_config_count, result.successCount))
@@ -3,10 +3,8 @@ package com.v2ray.ang.util
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.util.Utils.encode
import com.v2ray.ang.util.Utils.urlDecode
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.net.IDN
import java.net.Inet6Address
import java.net.InetAddress
@@ -15,6 +13,10 @@ import java.net.MalformedURLException
import java.net.Proxy
import java.net.URI
import java.net.URL
import java.util.concurrent.TimeUnit
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
object HttpUtil {
@@ -106,13 +108,31 @@ object HttpUtil {
* @param httpPort The HTTP port to use.
* @return The content of the URL as a string.
*/
fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? {
val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null
fun getUrlContent(
url: String,
timeout: Int,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String? {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder()
.url(url)
.get()
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
}
try {
return conn.inputStream.bufferedReader().readText()
} catch (_: Exception) {
} finally {
conn.disconnect()
client.newCall(requestBuilder.build()).execute().use { response ->
if (!response.isSuccessful) {
LogUtil.w(AppConfig.TAG, "Failed to get URL content, code=${response.code}")
return null
}
return response.body?.string()
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to get URL content", e)
}
return null
}
@@ -127,127 +147,147 @@ object HttpUtil {
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class)
fun getUrlContentWithUserAgent(url: String?, userAgent: String?, timeout: Int = 15000, httpPort: Int = 0): String {
fun getUrlContentWithUserAgent(
url: String?,
userAgent: String?,
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String {
var currentUrl = url
var redirects = 0
val maxRedirects = 3
while (redirects++ < maxRedirects) {
if (currentUrl == null) continue
val conn = createProxyConnection(currentUrl, httpPort, timeout, timeout) ?: continue
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = false)
val finalUserAgent = if (userAgent.isNullOrBlank()) {
"v2rayNG/${BuildConfig.VERSION_NAME}"
} else {
userAgent
}
conn.setRequestProperty("User-agent", finalUserAgent)
conn.connect()
val requestBuilder = Request.Builder()
.url(currentUrl)
.get()
.header("User-agent", finalUserAgent)
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
}
val responseCode = conn.responseCode
when (responseCode) {
in 300..399 -> {
val location = resolveLocation(conn)
conn.disconnect()
if (location.isNullOrEmpty()) {
throw IOException("Redirect location not found")
client.newCall(requestBuilder.build()).execute().use { response ->
when {
response.isRedirect -> {
val location = response.header("Location")
if (location.isNullOrEmpty()) {
throw IOException("Redirect location not found")
}
currentUrl = resolveLocation(currentUrl, location)
if (currentUrl.isNullOrEmpty()) {
throw IOException("Failed to resolve redirect location")
}
continue
}
currentUrl = location
continue
}
else -> try {
return conn.inputStream.use { it.bufferedReader().readText() }
} finally {
conn.disconnect()
response.isSuccessful -> {
return response.body?.string() ?: ""
}
else -> {
throw IOException("Request failed with status code ${response.code}")
}
}
}
}
throw IOException("Too many redirects")
}
/**
* Creates an HttpURLConnection object connected through a proxy.
*
* @param urlStr The target URL address.
* @param port The port of the proxy server.
* @param connectTimeout The connection timeout in milliseconds (default is 15000 ms).
* @param readTimeout The read timeout in milliseconds (default is 15000 ms).
* @param needStream Whether the connection needs to support streaming.
* @return Returns a configured HttpURLConnection object, or null if it fails.
*/
fun createProxyConnection(
urlStr: String,
port: Int,
connectTimeout: Int = 15000,
readTimeout: Int = 15000,
needStream: Boolean = false
): HttpURLConnection? {
private fun buildOkHttpClient(
timeout: Int,
httpPort: Int,
proxyUsername: String?,
proxyPassword: String?,
followRedirects: Boolean
): OkHttpClient {
val builder = OkHttpClient.Builder()
.connectTimeout(timeout.toLong(), TimeUnit.MILLISECONDS)
.readTimeout(timeout.toLong(), TimeUnit.MILLISECONDS)
.followRedirects(followRedirects)
.followSslRedirects(followRedirects)
var conn: HttpURLConnection? = null
try {
val url = URL(urlStr)
// Create a connection
conn = if (port == 0) {
url.openConnection()
} else {
url.openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress(LOOPBACK, port)
)
)
} as HttpURLConnection
// Set connection and read timeouts
conn.connectTimeout = connectTimeout
conn.readTimeout = readTimeout
if (!needStream) {
// Set request headers
conn.setRequestProperty("Connection", "close")
// Disable automatic redirects
conn.instanceFollowRedirects = false
// Disable caching
conn.useCaches = false
if (httpPort != 0) {
builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(LOOPBACK, httpPort)))
if (!proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
builder.proxyAuthenticator { _, response ->
if (response.request.header("Proxy-Authorization") != null) {
null
} else {
response.request.newBuilder()
.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
.build()
}
}
}
//Add Basic Authorization
url.userInfo?.let {
conn.setRequestProperty(
"Authorization",
"Basic ${encode(urlDecode(it))}"
)
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to create proxy connection", e)
// If an exception occurs, close the connection and return null
conn?.disconnect()
return null
}
return conn
return builder.build()
}
// Returns absolute URL string location header sets
fun resolveLocation(conn: HttpURLConnection): String? {
val raw = conn.getHeaderField("Location")?.trim()?.takeIf { it.isNotEmpty() } ?: return null
// Try check url is relative or absolute
private fun resolveLocation(baseUrl: String, raw: String): String? {
return try {
val locUri = URI(raw)
val baseUri = conn.url.toURI()
val baseUri = URI(baseUrl)
val resolved = if (locUri.isAbsolute) locUri else baseUri.resolve(locUri)
resolved.toURL().toString()
} catch (_: Exception) {
// Fallback: url resolver, also should handles //host/...
try {
URL(raw).toString() // absolute with protocol
URL(raw).toString()
} catch (_: MalformedURLException) {
try {
URL(conn.url, raw).toString()
URL(URL(baseUrl), raw).toString()
} catch (_: MalformedURLException) {
null
}
}
}
}
fun downloadToFile(
url: String,
targetFile: File,
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): Boolean {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder()
.url(url)
.get()
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
}
return try {
client.newCall(requestBuilder.build()).execute().use { response ->
if (!response.isSuccessful) {
LogUtil.w(AppConfig.TAG, "Failed to download file, code=${response.code}, url=$url")
return false
}
val body = response.body ?: return false
body.byteStream().use { input ->
targetFile.outputStream().use { output ->
input.copyTo(output)
}
}
true
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download file: $url", e)
false
}
}
}
@@ -2,12 +2,11 @@ package com.v2ray.ang.util
import android.content.Context
import android.content.pm.PackageManager
import com.v2ray.ang.AppConfig
import java.util.concurrent.ConcurrentHashMap
object PackageUidResolver {
private const val TAG = "PackageUidResolver"
// In-process cache to avoid resolving the same package UID repeatedly.
private val packageUidCache = ConcurrentHashMap<String, String>()
@@ -32,13 +31,20 @@ object PackageUidResolver {
}
private fun resolveUid(context: Context, packageName: String): String? {
// Special token for connections whose UID cannot be resolved (mapped to -1)
if (packageName == AppConfig.UNIDENTIFIED_PACKAGE) {
val uid = "-1"
LogUtil.d(AppConfig.TAG, "Special package: $packageName -> UID: $uid")
return uid
}
return try {
val uid = context.packageManager.getPackageUid(packageName, 0).toString()
LogUtil.d(TAG, "Package: $packageName -> UID: $uid")
LogUtil.d(AppConfig.TAG, "Package: $packageName -> UID: $uid")
uid
} catch (_: PackageManager.NameNotFoundException) {
LogUtil.w(TAG, "Package not found: $packageName")
LogUtil.w(AppConfig.TAG, "Package not found: $packageName")
null
}
}
}
}
@@ -10,8 +10,6 @@ import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
class UserAssetViewModel : ViewModel() {
private val assets = mutableListOf<AssetUrlCache>()
@@ -61,7 +59,12 @@ class UserAssetViewModel : ViewModel() {
}
}
fun downloadGeoFiles(extDir: File, httpPort: Int): GeoDownloadResult {
fun downloadGeoFiles(
extDir: File,
httpPort: Int,
proxyUsername: String? = null,
proxyPassword: String? = null
): GeoDownloadResult {
val snapshot = getAssets()
var successCount = 0
val failures = mutableListOf<String>()
@@ -69,7 +72,7 @@ class UserAssetViewModel : ViewModel() {
snapshot.forEach { cache ->
val item = cache.assetUrl
val portsToTry = if (httpPort == 0) listOf(0) else listOf(httpPort, 0)
if (portsToTry.any { tryDownload(item, extDir, it) }) {
if (portsToTry.any { tryDownload(item, extDir, it, proxyUsername, proxyPassword) }) {
successCount++
} else {
failures.add(item.remarks)
@@ -79,25 +82,22 @@ class UserAssetViewModel : ViewModel() {
return GeoDownloadResult(successCount, failures.size, failures)
}
private fun tryDownload(item: AssetUrlItem, extDir: File, httpPort: Int): Boolean {
private fun tryDownload(
item: AssetUrlItem,
extDir: File,
httpPort: Int,
proxyUsername: String? = null,
proxyPassword: String? = null
): Boolean {
val targetTemp = File(extDir, item.remarks + "_temp")
val target = File(extDir, item.remarks)
val conn = HttpUtil.createProxyConnection(item.url, httpPort, 15000, 15000, needStream = true) ?: return false
try {
val responseCode = conn.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
conn.inputStream.use { inputStream ->
FileOutputStream(targetTemp).use { output ->
inputStream.copyTo(output)
}
}
if (HttpUtil.downloadToFile(item.url, targetTemp, 15000, httpPort, proxyUsername, proxyPassword)) {
targetTemp.renameTo(target)
return true
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
} finally {
conn.disconnect()
}
return false
}
@@ -146,7 +146,6 @@
android:layout_weight="1.0"
android:text="@string/sub_auto_update" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/auto_update_check"
android:layout_width="wrap_content"
@@ -157,6 +156,26 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_pref_auto_update_interval" />
<EditText
android:id="@+id/et_update_interval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
tools:ignore="TextFields" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
+2 -22
View File
@@ -134,30 +134,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:nextFocusDown="@+id/sp_ech_force_query" />
android:nextFocusDown="@+id/et_pinned_ca256" />
</LinearLayout>
<LinearLayout
android:id="@+id/lay_ech_force_query"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_ech_force_query" />
<Spinner
android:id="@+id/sp_ech_force_query"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/ech_force_query_value" />
</LinearLayout>
<LinearLayout
android:id="@+id/lay_pinned_ca256"
android:layout_width="match_parent"
@@ -175,7 +155,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:nextFocusDown="@+id/sp_ech_force_query" />
android:nextFocusDown="@+id/et_public_key" />
</LinearLayout>
@@ -96,6 +96,49 @@
android:inputType="text" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_kcp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_kcp_mtu" />
<EditText
android:id="@+id/et_kcp_mtu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_kcp_tti" />
<EditText
android:id="@+id/et_kcp_tti"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layout_extra"
android:layout_width="match_parent"
@@ -89,6 +89,8 @@
<string name="server_lab_reserved">محجوز (اختياري)</string>
<string name="server_lab_local_address">العنوان المحلي (اختياري IPv4/IPv6، مفصولة بفواصل)</string>
<string name="server_lab_local_mtu">Mtu (اختياري، الافتراضي 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (اختياري، الافتراضي في النواة 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (اختياري، الافتراضي في النواة 50)</string>
<string name="toast_success">نجاح</string>
<string name="toast_failure">فشل</string>
<string name="toast_none_data">لا يوجد شيء</string>
@@ -115,7 +117,6 @@
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
<!-- UserAssetActivity -->
@@ -221,6 +222,10 @@
<string name="summary_pref_allow_insecure">عند TLS، الافتراضي هو السماح غير الآمن</string>
<string name="title_pref_socks_port">منفذ بروكسي Local</string>
<string name="title_pref_enable_local_proxy">تفعيل الوكيل المحلي</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">مصادقة socks5 ليست آمنة تمامًا عند استخدام UDP</string>
<string name="summary_pref_socks_port">منفذ بروكسي Local</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
@@ -295,6 +300,7 @@
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
<string name="toast_invalid_update_interval">فاصل التحديث غير صالح. الحد الأدنى هو 15 دقيقة.</string>
<string name="title_sub_update">تحديث الاشتراك (أول خطوة)</string>
<string name="title_ping_all_server">Tcping لجميع الإعدادات</string>
<string name="title_real_ping_all_server"> اختبر جميع الإعدادات (3)</string>
@@ -88,6 +88,8 @@
<string name="server_lab_reserved">সংরক্ষিত (ঐচ্ছিক)</string>
<string name="server_lab_local_address">স্থানীয় ঠিকানা (ঐচ্ছিক IPv4/IPv6, কমা দ্বারা পৃথক করা)</string>
<string name="server_lab_local_mtu">MTU (ঐচ্ছিক, ডিফল্ট 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (ঐচ্ছিক, কোর ডিফল্ট 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (ঐচ্ছিক, কোর ডিফল্ট 50)</string>
<string name="toast_success">সফল</string>
<string name="toast_failure">ব্যর্থ</string>
<string name="toast_none_data">কোনও তথ্য নেই</string>
@@ -114,7 +116,6 @@
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
<!-- UserAssetActivity -->
@@ -221,6 +222,10 @@
<string name="summary_pref_allow_insecure">যখন TLS, ডিফল্টভাবে allowInsecure</string>
<string name="title_pref_socks_port">Local প্রক্সি পোর্ট</string>
<string name="title_pref_enable_local_proxy">লোকাল প্রক্সি সক্ষম করুন</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">UDP ব্যবহার করার সময় socks5 প্রমাণীকরণ সম্পূর্ণরূপে নিরাপদ নয়</string>
<string name="summary_pref_socks_port">Local প্রক্সি পোর্ট</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
@@ -295,6 +300,7 @@
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
<string name="toast_invalid_update_interval">আপডেটের বিরতি সঠিক নয়। সর্বনিম্ন ১৫ মিনিট।</string>
<string name="title_sub_update">সাবস্ক্রিপশন আপডেট</string>
<string name="title_ping_all_server">সব কনফিগারেশন TCPing</string>
<string name="title_real_ping_all_server">সব কনফিগারেশন প্রকৃত বিলম্ব</string>
@@ -88,6 +88,8 @@
<string name="server_lab_reserved">Reserved(اختیاری، وا کاما ز یک جوڌا ابۊن)</string>
<string name="server_lab_local_address">نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن)</string>
<string name="server_lab_local_mtu">Mtu(اختیاری، پؽش فرز 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پؽش فرز هسته 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پؽش فرز هسته 50)</string>
<string name="toast_success">وا مووفقیت ٱنجوم وابی</string>
<string name="toast_failure">شکست خرد</string>
<string name="toast_none_data">هیچ داده ای وۊجۊڌ نڌاره</string>
@@ -112,9 +114,8 @@
<string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</string>
<string name="server_lab_xhttp_mode">هالت XHTTP</string>
<string name="server_lab_xhttp_extra">XHTTP Extra خام JSON، قالوو: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_final_mask">finalMask خام JSON، قالوو: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">نومگه کانفیگ Ech</string>
<string name="server_lab_ech_force_query">پورس وو جۊ اجباری Ech</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
<!-- UserAssetActivity -->
@@ -135,8 +136,8 @@
<string name="menu_item_search">پیتینیڌن</string>
<string name="menu_item_select_all">پسند پوی</string>
<string name="menu_item_invert_selection">پسند چواسه</string>
<string name="msg_enter_keywords">کلمه کلیدی را وارد کنید</string>
<string name="switch_bypass_apps_mode">هالت Bypass</string>
<string name="msg_enter_keywords">کلمه کلیدی ن بزنین</string>
<string name="switch_bypass_apps_mode">هالت دور زیڌن</string>
<string name="menu_item_select_proxy_app">پسند خوتکار پروکسی برنومه</string>
<string name="msg_downloading_content">موئتوا هونی دانلود ابۊن</string>
<string name="menu_item_export_proxy_app">و در کشیڌن من کلیپ بورد</string>
@@ -151,7 +152,7 @@
<string name="title_core_settings">سامووا هسته</string>
<string name="title_vpn_settings">سامووا VPN</string>
<string name="title_pref_per_app_proxy">پروکسی و ری برنومه</string>
<string name="summary_pref_per_app_proxy">پوی وولاتی: برنومه واجۊری بیڌه پروکسی هڌ، منپیز موستقیم بؽ نشووه هڌ. هالت دور زیڌن: برنومه نشووک ناڌه موستقیمن منپیز هڌ، پروکسی نشووک زیڌه نؽڌ. گۊزینه پسند خوتکار برنومه پروکسی من نومگه</string>
<string name="summary_pref_per_app_proxy">پوی وولاتی: برنومه پسند وابیڌه وا ی پروکسی منپیز ابۊ، برنومه پسند نوابیڌه موستقیمن منپیز ابۊ. \nحالت دور زیڌن: برنومه پسند وابیڌه موستقیمن منپیز ابۊ، برنومه پسند نوابیڌه وا ی پروکسی منپیز ابۊ. \nپسند خوتکار برنومه یل پروکسی من نومگه امکووݩ پزیر هڌ.</string>
<string name="title_pref_is_booted">منپیز خوتکار مجال ره ونی</string>
<string name="summary_pref_is_booted">مجال ره وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ.</string>
@@ -187,10 +188,10 @@
<string name="summary_pref_fake_dns_enabled">DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومه یل کار نکونه)</string>
<string name="title_pref_ipv6_enabled">ره وندن IPv6</string>
<string name="summary_pref_ipv6_enabled">نشۊویا IPv6 وو تورا IPv6 ن ز رابط VPN ره وند کۊنین</string>
<string name="summary_pref_ipv6_enabled">نشۊویا IPv6 وو تورا IPv6 ن ز رابت VPN ره ونین</string>
<string name="title_pref_prefer_ipv6">ترجی داڌن IPv6</string>
<string name="summary_pref_prefer_ipv6">مون حل کردن نوم دامنه، نشۊویا IPv6 ن ترجی بڌین</string>
<string name="summary_pref_prefer_ipv6">مجال هل وو فسل نوم دامنه، نشۊویا IPv6 ن ترجی بڌین</string>
<string name="title_pref_remote_dns">DNS ز ره دیر (اختیاری) (udp/tcp/https/quic) (اختیاری)</string>
<string name="summary_pref_remote_dns">DNS</string>
@@ -221,13 +222,17 @@
<string name="summary_pref_allow_insecure">مجال و کار بوردن TLS ب تۉر پؽش فرز، موجوز نا ٱمن فعال هڌ.</string>
<string name="title_pref_socks_port">پورت پروکسی مهلی</string>
<string name="title_pref_enable_local_proxy">ره وندن پروکسی مهلی</string>
<string name="summary_pref_enable_local_proxy">هرسا قیرفعال بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو ورۊ رسۊوی یل اشتراک وو کارا جۊر یو نترن ز پروکسی استفاڌه کونن.</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">ائراز هۊویت socks5 مجال استفاڌه ز UDP کاملن ایمن نؽ</string>
<string name="summary_pref_socks_port">پورت پروکسی مهلی</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
<string name="title_pref_socks_username">Local proxy username (optional)</string>
<string name="summary_pref_socks_username">Username</string>
<string name="title_pref_socks_password">Local proxy password (optional)</string>
<string name="summary_pref_socks_password">Password</string>
<string name="title_pref_dynamic_socks_port">ره وندن پورت پروکسی مهلی پۊیا</string>
<string name="summary_pref_dynamic_socks_port">هر کرت ک و من ٱووڌنی وورکل ابۊ، ی پورت پروکسی مهلی تساڌۊفی وورکل کۊنین</string>
<string name="title_pref_socks_username">نوم منتوری پروکسی مهلی (اختیاری)</string>
<string name="summary_pref_socks_username">نوم منتوری</string>
<string name="title_pref_socks_password">رزم پروکسی مهلی (اختیاری)</string>
<string name="summary_pref_socks_password">رزم</string>
<string name="title_pref_local_dns_port">پورت DNS مهلی</string>
<string name="summary_pref_local_dns_port">پورت DNS مهلی</string>
@@ -245,7 +250,7 @@
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ره وستن، وا برنومه ن ز نۊ ره ونین.</string>
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن فعال کۊنین</string>
<string name="summary_pref_group_all_display">ی بلگه «پوی بلگه یل» ازاف کۊنین</string>
<string name="summary_pref_group_all_display">ی بلگه «پوی بونکۊ یل کانفیگ» ازاف کۊنین</string>
<!-- AboutActivity -->
<string name="title_pref_feedback">فشناڌن منشڌ</string>
<string name="summary_pref_feedback">فشناڌن منشڌ یا گوزارش موشکلا من Github</string>
@@ -295,6 +300,7 @@
<string name="sub_setting_pre_profile">نوم موستعار پروکسی دیندایی</string>
<string name="sub_setting_next_profile">نوم موستعار پروکسی نیایی</string>
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</string>
<string name="toast_invalid_update_interval">فاسله ورۊ کردن نا موئتبر هڌ. اقل مقدار 15 دؽقه هڌ.</string>
<string name="title_sub_update">ورۊ کردن اشتراک بونکۊ سکویی</string>
<string name="title_ping_all_server">Tcping کانفیگا بونکۊ سکویی</string>
<string name="title_real_ping_all_server">تئخیر واقعی کانفیگا بونکۊ سکویی</string>
@@ -310,9 +316,9 @@
<string name="title_update_config_count">ورۊ کردن %d کانفیگ</string>
<string name="title_update_subscription_result">%1$d کانفیگ ورۊ وابی (مووفق: %2$d، شکست خرده: %3$d، رڌ وابیڌه: %4$d)</string>
<string name="title_update_subscription_no_subscription">بؽ اشتراک</string>
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
<string name="toast_fragment_not_available">Unable to locate current view</string>
<string name="title_locate_selected_config">Locate the selected config</string>
<string name="toast_server_not_found_in_group">سرور پسند وابیڌه من بونکۊ سکویی نجۊرست</string>
<string name="toast_fragment_not_available">نتره نما سکویی ن بجۊره</string>
<string name="title_locate_selected_config">کانفیگ پسند وابیڌه ن بجۊرین</string>
<string name="tasker_start_service">ره وندن خدمات</string>
<string name="tasker_setting_confirm">قوۊل</string>
@@ -320,7 +326,7 @@
<!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">نشقه دامنه</string>
<string name="routing_settings_title">سامووا تور جوستن</string>
<string name="routing_settings_tips">وا کاما ز یک جوڌا ابۊن (،)؛ یکی را وارد کنید: domain، ip یا process</string>
<string name="routing_settings_tips">وا کاما ز یک جوڌا ابۊن (,)؛ یکی ن بزنین: domain، ip یا process</string>
<string name="routing_settings_save">زفت کردن</string>
<string name="routing_settings_delete">روفتن</string>
<string name="routing_settings_rule_title">سامووا قانۉݩ تور جوستن</string>
@@ -88,6 +88,8 @@
<string name="server_lab_reserved">Reserved (اختیاری، جدا شده با کاما)</string>
<string name="server_lab_local_address">آدرس محلی (IPv4/IPv6 اختیاری، جدا شده با کاما)</string>
<string name="server_lab_local_mtu">MTU (اختیاری، پیش‌فرض 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پیش‌فرض هسته 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پیش‌فرض هسته 50)</string>
<string name="toast_success">با موفقیت انجام شد</string>
<string name="toast_failure">شکست</string>
<string name="toast_none_data">هیچ داده ای وجود ندارد</string>
@@ -114,7 +116,6 @@
<string name="server_lab_xhttp_extra">خام JSON XHTTP Extra، قالب: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">لیست کانفیگ Ech</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
<!-- UserAssetActivity -->
@@ -219,6 +220,10 @@
<string name="summary_pref_allow_insecure">هنگام استفاده از TLS، به طور پیش‌ فرض مجوز ناامن فعال است.</string>
<string name="title_pref_socks_port">پورت پروکسی محلی</string>
<string name="title_pref_enable_local_proxy">فعال‌سازی پروکسی محلی</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">احراز هویت socks5 هنگام استفاده از UDP کاملاً ایمن نیست</string>
<string name="summary_pref_socks_port">پورت پروکسی محلی</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
@@ -292,6 +297,7 @@
<string name="sub_setting_pre_profile">نام مستعار پروکسی قبلی</string>
<string name="sub_setting_next_profile">نام مستعار پروکسی بعدی</string>
<string name="sub_setting_pre_profile_tip">لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است</string>
<string name="toast_invalid_update_interval">بازه بروزرسانی نامعتبر است. حداقل مقدار 15 دقیقه است.</string>
<string name="title_sub_update">به‌روزرسانی گروه فعلی اشتراک</string>
<string name="title_ping_all_server">TCPING کانفیگ های گروه فعلی</string>
<string name="title_real_ping_all_server">تاخیر واقعی کانفیگ های گروه فعلی</string>
+11 -4
View File
@@ -2,7 +2,7 @@
<resources>
<string name="app_widget_name">v2rayNG</string>
<string name="app_tile_name">v2rayNG</string>
<string name="app_tile_first_use">Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить сервер</string>
<string name="app_tile_first_use">Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить профиль</string>
<string name="navigation_drawer_open">Открыть панель навигации</string>
<string name="navigation_drawer_close">Закрыть панель навигации</string>
<string name="migration_success">Успешный перенос данных!</string>
@@ -88,6 +88,8 @@
<string name="server_lab_reserved">Reserved (необязательно, через запятую)</string>
<string name="server_lab_local_address">Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
<string name="server_lab_local_mtu">MTU (необязательно, по умолчанию 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (необязательно, по умолчанию 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (необязательно, по умолчанию 50)</string>
<string name="toast_success">Успешно</string>
<string name="toast_failure">Ошибка</string>
<string name="toast_none_data">Ничего нет</string>
@@ -114,7 +116,6 @@
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
<string name="server_lab_final_mask">Необработанный JSON FinalMask, формат: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
<!-- UserAssetActivity -->
@@ -143,6 +144,7 @@
<string name="menu_item_import_proxy_app">Импорт из буфера обмена</string>
<string name="per_app_proxy_settings">Выбор приложений</string>
<string name="per_app_proxy_settings_enable">Использовать выбор приложений</string>
<string name="app_picker_unknown_app">Неизвестное приложение (неопознанный UID)</string>
<!-- Preferences -->
<string name="title_settings">Настройки</string>
@@ -222,6 +224,10 @@
<string name="summary_pref_allow_insecure">Для TLS по умолчанию разрешены небезопасные соединения</string>
<string name="title_pref_socks_port">Порт локального прокси</string>
<string name="title_pref_enable_local_proxy">Использовать локальный прокси</string>
<string name="summary_pref_enable_local_proxy">Если отключено, SOCKS-прокси недоступен, поэтому обновления подписки и подобные действия не смогут использовать прокси</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">Аутентификация SOCKS5 не полностью безопасна при использовании UDP</string>
<string name="summary_pref_socks_port">Порт локального прокси</string>
<string name="title_pref_dynamic_socks_port">Динамически менять порт локального прокси</string>
<string name="summary_pref_dynamic_socks_port">Использовать случайный порт локального прокси при каждом создании соединения</string>
@@ -297,6 +303,7 @@
<string name="sub_setting_pre_profile">Предыдущий профиль прокси</string>
<string name="sub_setting_next_profile">Следующий профиль прокси</string>
<string name="sub_setting_pre_profile_tip">Профиль должен быть уникальным</string>
<string name="toast_invalid_update_interval">Неверный интервал обновления. Минимум 15 минут.</string>
<string name="title_sub_update">Обновить подписку</string>
<string name="title_ping_all_server">Проверить профили</string>
<string name="title_real_ping_all_server">Проверить задержку профилей</string>
@@ -321,7 +328,7 @@
<!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">Доменная стратегия</string>
<string name="routing_settings_title">Маршрутизация</string>
<string name="routing_settings_tips">Через запятую (,)\nТолько что-то одно: домен, IP или процесс</string>
<string name="routing_settings_tips">Через запятую (,)\nЧто-то одно: домен, IP или процесс</string>
<string name="routing_settings_save">Сохранить</string>
<string name="routing_settings_delete">Очистить</string>
<string name="routing_settings_rule_title">Настройка правил маршрутизации</string>
@@ -334,7 +341,7 @@
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string>
<string name="routing_settings_domain">Домен</string>
<string name="routing_settings_ip">IP</string>
<string name="routing_settings_process">Процесс (название пакета поддерживается только при использовании Xray TUN, включённой функции routeOnly и ОС Android 10+)</string>
<string name="routing_settings_process">Процесс (название пакета; поддерживается только при использовании Xray TUN, включённой функции routeOnly и ОС Android 10+)</string>
<string name="routing_settings_port">Порт</string>
<string name="routing_settings_protocol">Протокол</string>
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
@@ -88,6 +88,8 @@
<string name="server_lab_reserved">Reserved (Không bắt buộc)</string>
<string name="server_lab_local_address">Địa chỉ cục bộ (IPv4 / IPv6, phân cách bằng dấu phẩy)</string>
<string name="server_lab_local_mtu">MTU (Không bắt buộc, mặc định là 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (Không bắt buộc, mặc định core là 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (Không bắt buộc, mặc định core là 50)</string>
<string name="toast_success">Thành công!</string>
<string name="toast_failure">Đã xảy ra lỗi, vui lòng thử lại!</string>
<string name="toast_none_data">Không có gì ở đây!</string>
@@ -114,7 +116,6 @@
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
<!-- UserAssetActivity -->
@@ -221,6 +222,10 @@
<string name="summary_pref_allow_insecure">Khi nhập những cấu hình có bảo mật TLS, mặc định sẽ không xác minh chứng chỉ.</string>
<string name="title_pref_socks_port">Cổng Proxy Local</string>
<string name="title_pref_enable_local_proxy">Bật proxy cục bộ</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">Xác thực socks5 không hoàn toàn an toàn khi sử dụng UDP</string>
<string name="summary_pref_socks_port">Cổng Proxy Local</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
@@ -295,6 +300,7 @@
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
<string name="toast_invalid_update_interval">Khoảng thời gian cập nhật không hợp lệ. Giá trị tối thiểu là 15 phút.</string>
<string name="title_sub_update">Cập nhật các gói đăng ký</string>
<string name="title_ping_all_server">Ping tất cả máy chủ</string>
<string name="title_real_ping_all_server">Kiểm tra HTTP tất cả máy chủ</string>
@@ -88,6 +88,8 @@
<string name="server_lab_reserved">Reserved (可选,逗号隔开)</string>
<string name="server_lab_local_address">本地地址 (可选 IPv4/IPv6,逗号隔开)</string>
<string name="server_lab_local_mtu">Mtu (可选, 默认 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (可选, 默认 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (可选, 默认 50)</string>
<string name="toast_success">成功</string>
<string name="toast_failure">失败</string>
<string name="toast_none_data">没有数据</string>
@@ -114,7 +116,6 @@
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
<!-- UserAssetActivity -->
@@ -218,6 +219,10 @@
<string name="summary_pref_allow_insecure">传输层安全选 tls 时,默认跳过证书验证 (allowInsecure)</string>
<string name="title_pref_socks_port">本地代理端口</string>
<string name="title_pref_enable_local_proxy">启用本地代理</string>
<string name="summary_pref_enable_local_proxy">不开启时没有 socks 代理,无法通过代理更新订阅等</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">使用 UDP 时,socks5 认证并不完全安全</string>
<string name="summary_pref_socks_port">本地代理端口</string>
<string name="title_pref_dynamic_socks_port">启用动态本地代理端口</string>
<string name="summary_pref_dynamic_socks_port">每次生成 inbound 时随机生成一个本地代理端口</string>
@@ -293,6 +298,7 @@
<string name="sub_setting_pre_profile">前置代理配置别名</string>
<string name="sub_setting_next_profile">落地代理配置別名</string>
<string name="sub_setting_pre_profile_tip">请确保配置别名存在并唯一</string>
<string name="toast_invalid_update_interval">更新间隔无效,最小值为 15 分钟。</string>
<string name="title_sub_update">更新订阅</string>
<string name="title_ping_all_server">测试配置 Tcping</string>
<string name="title_real_ping_all_server">测试配置真连接</string>
@@ -88,6 +88,8 @@
<string name="server_lab_reserved">Reserved (可選,逗號隔開)</string>
<string name="server_lab_local_address">本機位址 (可選 IPv4/IPv6,逗號隔開)</string>
<string name="server_lab_local_mtu">MTU (可選, 預設 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (可選, 預設 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (可選, 預設 50)</string>
<string name="toast_success">成功</string>
<string name="toast_failure">失敗</string>
<string name="toast_none_data">無資料</string>
@@ -114,7 +116,6 @@
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string>
<!-- UserAssetActivity -->
@@ -219,6 +220,10 @@
<string name="summary_pref_allow_insecure">傳輸層安全選 tls 時,預設跳過憑證驗證 (allowInsecure)</string>
<string name="title_pref_socks_port">本地代理埠</string>
<string name="title_pref_enable_local_proxy">啟用本地代理</string>
<string name="summary_pref_enable_local_proxy">未開啟時沒有 socks 代理,無法透過代理更新訂閱等</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">使用 UDP 時,socks5 認證並不完全安全</string>
<string name="summary_pref_socks_port">本地代理埠</string>
<string name="title_pref_dynamic_socks_port">啟用動態本地代理埠</string>
<string name="summary_pref_dynamic_socks_port">每次產生 inbound 時隨機產生一個本地代理埠</string>
@@ -294,6 +299,7 @@
<string name="sub_setting_pre_profile">前置代理設定檔别名</string>
<string name="sub_setting_next_profile">落地代理設定檔別名</string>
<string name="sub_setting_pre_profile_tip">请确保設定檔别名存在并唯一</string>
<string name="toast_invalid_update_interval">更新間隔無效,最小值為 15 分鐘。</string>
<string name="title_sub_update">更新訂閱</string>
<string name="title_ping_all_server">偵測設定 Tcping</string>
<string name="title_real_ping_all_server">偵測設定真延遲</string>
@@ -215,11 +215,5 @@
<item>2</item>
</string-array>
<string-array name="ech_force_query_value" translatable="false">
<item />
<item>none</item>
<item>half</item>
<item>full</item>
</string-array>
</resources>
+8 -1
View File
@@ -89,6 +89,8 @@
<string name="server_lab_reserved">Reserved(Optional, separated by commas)</string>
<string name="server_lab_local_address">Local address (optional IPv4/IPv6, separated by commas)</string>
<string name="server_lab_local_mtu">Mtu(optional, default 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (Optional, core default 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (Optional, core default 50)</string>
<string name="toast_success">Success</string>
<string name="toast_failure">Failure</string>
<string name="toast_none_data">There is nothing</string>
@@ -115,7 +117,6 @@
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
<!-- UserAssetActivity -->
@@ -144,6 +145,7 @@
<string name="menu_item_import_proxy_app">Import from Clipboard</string>
<string name="per_app_proxy_settings">Per-app settings</string>
<string name="per_app_proxy_settings_enable">Enable per-app</string>
<string name="app_picker_unknown_app">Unknown app (unidentified UID)</string>
<!-- Preferences -->
<string name="title_settings">Settings</string>
@@ -224,6 +226,10 @@
<string name="summary_pref_allow_insecure">When TLS is selected, allow insecure connections by default</string>
<string name="title_pref_socks_port">Local proxy port</string>
<string name="title_pref_enable_local_proxy">Enable local proxy</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">SOCKS5 authentication is not fully secure when using UDP.</string>
<string name="summary_pref_socks_port">Local proxy port</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
@@ -299,6 +305,7 @@
<string name="sub_setting_pre_profile">Previous proxy config remarks</string>
<string name="sub_setting_next_profile">Next proxy config remarks</string>
<string name="sub_setting_pre_profile_tip">The config remarks exist and are unique</string>
<string name="toast_invalid_update_interval">Invalid update interval. Minimum is 15 minutes.</string>
<string name="title_sub_update">Update subscription</string>
<string name="title_ping_all_server">Tcping config</string>
<string name="title_real_ping_all_server">Real delay config</string>
+17 -19
View File
@@ -146,15 +146,20 @@
<CheckBoxPreference
android:defaultValue="false"
android:key="pref_proxy_sharing_enabled"
android:summary="@string/summary_pref_proxy_sharing_enabled"
android:title="@string/title_pref_proxy_sharing_enabled" />
android:key="pref_allow_insecure"
android:title="@string/title_pref_allow_insecure" />
<CheckBoxPreference
android:defaultValue="true"
android:key="pref_enable_local_proxy"
android:summary="@string/summary_pref_enable_local_proxy"
android:title="@string/title_pref_enable_local_proxy" />
<CheckBoxPreference
android:defaultValue="false"
android:key="pref_allow_insecure"
android:summary="@string/summary_pref_allow_insecure"
android:title="@string/title_pref_allow_insecure" />
android:key="pref_proxy_sharing_enabled"
android:summary="@string/summary_pref_proxy_sharing_enabled"
android:title="@string/title_pref_proxy_sharing_enabled" />
<EditTextPreference
android:inputType="number"
@@ -179,6 +184,12 @@
android:summary="@string/summary_pref_socks_password"
android:title="@string/title_pref_socks_password" />
<CheckBoxPreference
android:defaultValue="true"
android:key="pref_socks_enable_udp"
android:summary="@string/summary_pref_socks_enable_udp"
android:title="@string/title_pref_socks_enable_udp" />
<EditTextPreference
android:key="pref_remote_dns"
android:summary="@string/summary_pref_remote_dns"
@@ -267,19 +278,6 @@
android:title="@string/title_pref_fragment_packets" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/title_sub_setting"
app:initialExpandedChildrenCount="0">
<CheckBoxPreference
android:key="pref_auto_update_subscription"
android:summary="@string/summary_pref_auto_update_subscription"
android:title="@string/title_pref_auto_update_subscription" />
<EditTextPreference
android:inputType="number"
android:key="pref_auto_update_interval"
android:summary="1440"
android:title="@string/title_pref_auto_update_interval" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/title_advanced">
+1 -1
View File
@@ -1,5 +1,5 @@
[versions]
agp = "9.1.0"
agp = "9.2.0"
desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
kotlin = "2.3.10"
+1 -1
View File
@@ -1,6 +1,6 @@
#Thu Nov 14 12:42:51 BDT 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists