Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30792299e8 | |||
| e6db0f6f9e | |||
| 04478ab490 | |||
| f371da490a | |||
| 0a0ef6e6b5 | |||
| fd57019e73 | |||
| 04ac6cb2d7 | |||
| 29a0be9629 | |||
| 7aa27fd3de | |||
| fcc478ff63 | |||
| 54d5ad2342 | |||
| ba25e05536 | |||
| 5a5a81fa0b | |||
| 322c4b305e | |||
| 590f11e671 | |||
| 872ec80292 | |||
| f19f153f9c | |||
| d7abb8f49e | |||
| e54e713db7 | |||
| de91fb27db | |||
| db8f46eae5 | |||
| 42e7f95e4f | |||
| 0a52a79fa0 | |||
| 9dc1b1f2cf | |||
| b61cad5a5f | |||
| 66cd746ad2 | |||
| ae4a92b66b | |||
| 55c0ccf1b0 | |||
| deedd26fac | |||
| e6c8307b96 | |||
| d6d5753283 | |||
| 014d510774 | |||
| 42f89e69a7 | |||
| d5a0997775 | |||
| af7db6f86b | |||
| 7b7f43e12e | |||
| 8dd1df1451 | |||
| d4a73493b5 | |||
| 9fce03d0ed | |||
| a12ca3ea04 | |||
| 4e9df84d3e | |||
| d3118b53f2 | |||
| cd45b51487 | |||
| 6aa26d2621 | |||
| 0b452707c3 | |||
| 303b642ade |
@@ -22,7 +22,7 @@ jobs:
|
|||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3.2.0
|
uses: android-actions/setup-android@v4.0.1
|
||||||
with:
|
with:
|
||||||
log-accepted-android-sdk-licenses: false
|
log-accepted-android-sdk-licenses: false
|
||||||
cmdline-tools-version: '13114758'
|
cmdline-tools-version: '13114758'
|
||||||
|
|||||||
+1
-1
Submodule AndroidLibXrayLite updated: c0a0231614...203f317afd
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 714
|
versionCode = 720
|
||||||
versionName = "2.0.14"
|
versionName = "2.1.0"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -30,4 +30,34 @@
|
|||||||
android:targetPackage="com.v2ray.ang.fdroid" />
|
android:targetPackage="com.v2ray.ang.fdroid" />
|
||||||
<categories android:name="android.shortcut.conversation" />
|
<categories android:name="android.shortcut.conversation" />
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_qu_start_24dp"
|
||||||
|
android:shortcutDisabledMessage="@string/toast_services_start"
|
||||||
|
android:shortcutId="shortcuts_start"
|
||||||
|
android:shortcutLongLabel="@string/toast_services_start"
|
||||||
|
android:shortcutShortLabel="@string/toast_services_start">
|
||||||
|
|
||||||
|
<!--suppress AndroidDomInspection -->
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetClass="com.v2ray.ang.ui.ScStartActivity"
|
||||||
|
android:targetPackage="com.v2ray.ang.fdroid" />
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_qu_stop_24dp"
|
||||||
|
android:shortcutDisabledMessage="@string/toast_services_stop"
|
||||||
|
android:shortcutId="shortcuts_stop"
|
||||||
|
android:shortcutLongLabel="@string/toast_services_stop"
|
||||||
|
android:shortcutShortLabel="@string/toast_services_stop">
|
||||||
|
|
||||||
|
<!--suppress AndroidDomInspection -->
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetClass="com.v2ray.ang.ui.ScStopActivity"
|
||||||
|
android:targetPackage="com.v2ray.ang.fdroid" />
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
</shortcuts>
|
</shortcuts>
|
||||||
@@ -122,6 +122,18 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
|
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ScStartActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ScStopActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.UrlSchemeActivity"
|
android:name=".ui.UrlSchemeActivity"
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tag": "tun",
|
"tag": "tun",
|
||||||
"port": 0,
|
|
||||||
"protocol": "tun",
|
"protocol": "tun",
|
||||||
"settings": {
|
"settings": {
|
||||||
"name": "xray0",
|
"name": "xray0",
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ object AppConfig {
|
|||||||
/** Directory names used in the app's file system. */
|
/** Directory names used in the app's file system. */
|
||||||
const val DIR_ASSETS = "assets"
|
const val DIR_ASSETS = "assets"
|
||||||
|
|
||||||
const val WEBDAV_BACKUP_DIR = "backups"
|
const val WEBDAV_BACKUP_DIR = "backups"
|
||||||
const val WEBDAV_BACKUP_FILE_NAME = "backup_ng.zip"
|
const val WEBDAV_BACKUP_FILE_NAME = "backup_ng.zip"
|
||||||
|
|
||||||
/** Legacy configuration keys. */
|
/** Legacy configuration keys. */
|
||||||
const val ANG_CONFIG = "ang_config"
|
const val ANG_CONFIG = "ang_config"
|
||||||
@@ -54,10 +54,14 @@ object AppConfig {
|
|||||||
const val PREF_GROUP_ALL_DISPLAY = "pref_group_all_display"
|
const val PREF_GROUP_ALL_DISPLAY = "pref_group_all_display"
|
||||||
const val PREF_LANGUAGE = "pref_language"
|
const val PREF_LANGUAGE = "pref_language"
|
||||||
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
||||||
|
const val PREF_IPV6_ENABLED = "pref_ipv6_enabled"
|
||||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
||||||
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
||||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
||||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
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_REMOTE_DNS = "pref_remote_dns"
|
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
||||||
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
||||||
const val PREF_DNS_HOSTS = "pref_dns_hosts"
|
const val PREF_DNS_HOSTS = "pref_dns_hosts"
|
||||||
@@ -120,7 +124,8 @@ object AppConfig {
|
|||||||
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
||||||
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||||
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
||||||
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
|
||||||
|
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
||||||
const val IP_API_URL = "https://api.ip.sb/geoip"
|
const val IP_API_URL = "https://api.ip.sb/geoip"
|
||||||
|
|
||||||
/** DNS server addresses. */
|
/** DNS server addresses. */
|
||||||
@@ -274,4 +279,9 @@ object AppConfig {
|
|||||||
"Chocolate4U/Iran-v2ray-rules"
|
"Chocolate4U/Iran-v2ray-rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val BUILTIN_OUTBOUND_TAGS = setOf(
|
||||||
|
TAG_PROXY,
|
||||||
|
TAG_DIRECT,
|
||||||
|
TAG_BLOCKED,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.v2ray.ang.contracts
|
|||||||
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
|
||||||
interface MainAdapterListener :BaseAdapterListener {
|
interface MainAdapterListener : BaseAdapterListener {
|
||||||
|
|
||||||
fun onEdit(guid: String, position: Int, profile: ProfileItem)
|
fun onEdit(guid: String, position: Int, profile: ProfileItem)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface Tun2SocksControl {
|
|||||||
* This initializes the VPN tunnel and connects it to the SOCKS proxy.
|
* This initializes the VPN tunnel and connects it to the SOCKS proxy.
|
||||||
*/
|
*/
|
||||||
fun startTun2Socks()
|
fun startTun2Socks()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the tun2socks process and cleans up resources.
|
* Stops the tun2socks process and cleans up resources.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ data class ProfileItem(
|
|||||||
var authority: String? = null,
|
var authority: String? = null,
|
||||||
var xhttpMode: String? = null,
|
var xhttpMode: String? = null,
|
||||||
var xhttpExtra: String? = null,
|
var xhttpExtra: String? = null,
|
||||||
|
var finalMask: String? = null,
|
||||||
var security: String? = null,
|
var security: String? = null,
|
||||||
var sni: String? = null,
|
var sni: String? = null,
|
||||||
var alpn: String? = null,
|
var alpn: String? = null,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ data class RulesetItem(
|
|||||||
var remarks: String? = "",
|
var remarks: String? = "",
|
||||||
var ip: List<String>? = null,
|
var ip: List<String>? = null,
|
||||||
var domain: List<String>? = null,
|
var domain: List<String>? = null,
|
||||||
|
var process: List<String>? = null,
|
||||||
var outboundTag: String = "",
|
var outboundTag: String = "",
|
||||||
var port: String? = null,
|
var port: String? = null,
|
||||||
var network: String? = null,
|
var network: String? = null,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class InboundBean(
|
data class InboundBean(
|
||||||
var tag: String,
|
var tag: String,
|
||||||
var port: Int,
|
var port: Int?,
|
||||||
var protocol: String,
|
var protocol: String,
|
||||||
var listen: String? = null,
|
var listen: String? = null,
|
||||||
var settings: InSettingsBean? = null,
|
var settings: InSettingsBean? = null,
|
||||||
@@ -44,10 +44,16 @@ data class V2rayConfig(
|
|||||||
var auth: String? = null,
|
var auth: String? = null,
|
||||||
var udp: Boolean? = null,
|
var udp: Boolean? = null,
|
||||||
var userLevel: Int? = null,
|
var userLevel: Int? = null,
|
||||||
|
var accounts: List<SocksAccountBean>? = null,
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
@SerializedName("MTU")
|
|
||||||
var mtu: Int? = null
|
var mtu: Int? = null
|
||||||
)
|
) {
|
||||||
|
data class SocksAccountBean(
|
||||||
|
var user: String = "",
|
||||||
|
var pass: String = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
data class SniffingBean(
|
data class SniffingBean(
|
||||||
var enabled: Boolean,
|
var enabled: Boolean,
|
||||||
@@ -68,8 +74,6 @@ data class V2rayConfig(
|
|||||||
) {
|
) {
|
||||||
data class OutSettingsBean(
|
data class OutSettingsBean(
|
||||||
var vnext: List<VnextBean>? = null,
|
var vnext: List<VnextBean>? = null,
|
||||||
var fragment: FragmentBean? = null,
|
|
||||||
var noises: List<NoiseBean>? = null,
|
|
||||||
var servers: List<ServersBean>? = null,
|
var servers: List<ServersBean>? = null,
|
||||||
/*Blackhole*/
|
/*Blackhole*/
|
||||||
var response: Response? = null,
|
var response: Response? = null,
|
||||||
@@ -108,18 +112,6 @@ data class V2rayConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FragmentBean(
|
|
||||||
var packets: String? = null,
|
|
||||||
var length: String? = null,
|
|
||||||
var interval: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class NoiseBean(
|
|
||||||
var type: String? = null,
|
|
||||||
var packet: String? = null,
|
|
||||||
var delay: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ServersBean(
|
data class ServersBean(
|
||||||
var address: String = "",
|
var address: String = "",
|
||||||
var method: String? = null,
|
var method: String? = null,
|
||||||
@@ -162,7 +154,7 @@ data class V2rayConfig(
|
|||||||
var realitySettings: TlsSettingsBean? = null,
|
var realitySettings: TlsSettingsBean? = null,
|
||||||
var grpcSettings: GrpcSettingsBean? = null,
|
var grpcSettings: GrpcSettingsBean? = null,
|
||||||
var hysteriaSettings: HysteriaSettingsBean? = null,
|
var hysteriaSettings: HysteriaSettingsBean? = null,
|
||||||
var finalmask: FinalMaskBean? = null,
|
var finalmask: Any? = null,
|
||||||
val dsSettings: Any? = null,
|
val dsSettings: Any? = null,
|
||||||
var sockopt: SockoptBean? = null
|
var sockopt: SockoptBean? = null
|
||||||
) {
|
) {
|
||||||
@@ -292,28 +284,51 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class HysteriaSettingsBean(
|
data class HysteriaSettingsBean(
|
||||||
var version: Int,
|
var version: Int,
|
||||||
var auth: String? = null,
|
var auth: String? = null
|
||||||
var up: String? = null,
|
)
|
||||||
var down: String? = null,
|
|
||||||
var udphop: HysteriaUdpHopBean? = null
|
|
||||||
) {
|
|
||||||
data class HysteriaUdpHopBean(
|
|
||||||
var port: String? = null,
|
|
||||||
var interval: Int? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
//https://xtls.github.io/config/transport.html#finalmaskobject
|
||||||
data class FinalMaskBean(
|
data class FinalMaskBean(
|
||||||
var tcp: List<MaskBean>? = null,
|
var tcp: List<MaskBean>? = null,
|
||||||
var udp: List<MaskBean>? = null
|
var udp: List<MaskBean>? = null,
|
||||||
|
var quicParams: QuicParamsBean? = null
|
||||||
) {
|
) {
|
||||||
data class MaskBean(
|
data class MaskBean(
|
||||||
var type: String,
|
var type: String,
|
||||||
var settings: MaskSettingsBean? = null
|
var settings: MaskSettingsBean? = null
|
||||||
) {
|
) {
|
||||||
data class MaskSettingsBean(
|
data class MaskSettingsBean(
|
||||||
var password: String? = null,
|
val password: String? = null,
|
||||||
var domain: String? = null
|
val domain: String? = null,
|
||||||
|
// fragment
|
||||||
|
val packets: String? = null,
|
||||||
|
val length: String? = null,
|
||||||
|
val delay: String? = null,
|
||||||
|
// val maxSplit: String? = null,
|
||||||
|
// noise
|
||||||
|
val reset: Int? = null,
|
||||||
|
val noise: List<NoiseMaskBean>? = null
|
||||||
|
) {
|
||||||
|
data class NoiseMaskBean(
|
||||||
|
val rand: String? = null,
|
||||||
|
// val randRange: String? = null,
|
||||||
|
// val type: String? = null,
|
||||||
|
// val packet: String? = null,
|
||||||
|
val delay: String? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class QuicParamsBean(
|
||||||
|
var congestion: String? = null,
|
||||||
|
var brutalUp: String? = null,
|
||||||
|
var brutalDown: String? = null,
|
||||||
|
var udpHop: UdpHopBean? = null,
|
||||||
|
) {
|
||||||
|
// Nested data class for the udpHop JSON object
|
||||||
|
data class UdpHopBean(
|
||||||
|
var ports: String? = null,
|
||||||
|
var interval: String? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,8 +424,9 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class RulesBean(
|
data class RulesBean(
|
||||||
var type: String = "field",
|
var type: String = "field",
|
||||||
var ip: ArrayList<String>? = null,
|
var ip: List<String>? = null,
|
||||||
var domain: ArrayList<String>? = null,
|
var domain: List<String>? = null,
|
||||||
|
var process: List<String>? = null,
|
||||||
var outboundTag: String? = null,
|
var outboundTag: String? = null,
|
||||||
var balancerTag: String? = null,
|
var balancerTag: String? = null,
|
||||||
var port: String? = null,
|
var port: String? = null,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
|
|||||||
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
||||||
HYSTERIA(900, AppConfig.HYSTERIA),
|
HYSTERIA(900, AppConfig.HYSTERIA),
|
||||||
HTTP(10, AppConfig.HTTP),
|
HTTP(10, AppConfig.HTTP),
|
||||||
POLICYGROUP (101, AppConfig.CUSTOM);
|
POLICYGROUP(101, AppConfig.CUSTOM);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
|
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
|
||||||
|
|||||||
@@ -189,4 +189,15 @@ fun String.concatUrl(vararg paths: String): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to match text either by Regex or literal string.
|
||||||
|
*/
|
||||||
|
fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean = true): Boolean {
|
||||||
|
if (keyword.isNullOrEmpty()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return regex?.containsMatchIn(this)
|
||||||
|
?: this.contains(keyword, ignoreCase = ignoreCase)
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ object CustomFmt : FmtBase() {
|
|||||||
* @param str the JSON string to parse
|
* @param str the JSON string to parse
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
*/
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem {
|
||||||
val config = ProfileItem.create(EConfigType.CUSTOM)
|
val config = ProfileItem.create(EConfigType.CUSTOM)
|
||||||
|
|
||||||
val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java)
|
val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java)
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ open class FmtBase {
|
|||||||
config.authority = queryParam["authority"]
|
config.authority = queryParam["authority"]
|
||||||
config.xhttpMode = queryParam["mode"]
|
config.xhttpMode = queryParam["mode"]
|
||||||
config.xhttpExtra = queryParam["extra"]
|
config.xhttpExtra = queryParam["extra"]
|
||||||
|
config.finalMask = queryParam["fm"]
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
config.security = queryParam["security"]
|
||||||
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
|
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
|
||||||
@@ -110,6 +111,7 @@ open class FmtBase {
|
|||||||
config.spiderX?.nullIfBlank()?.let { dicQuery["spx"] = it }
|
config.spiderX?.nullIfBlank()?.let { dicQuery["spx"] = it }
|
||||||
config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it }
|
config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it }
|
||||||
config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it }
|
config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it }
|
||||||
|
config.finalMask?.nullIfBlank()?.let { dicQuery["fm"] = it }
|
||||||
// Add two keys for compatibility: "insecure" and "allowInsecure"
|
// Add two keys for compatibility: "insecure" and "allowInsecure"
|
||||||
if (config.security == AppConfig.TLS) {
|
if (config.security == AppConfig.TLS) {
|
||||||
val insecureFlag = if (config.insecure == true) "1" else "0"
|
val insecureFlag = if (config.insecure == true) "1" else "0"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.fmt
|
|||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean.FinalMaskBean
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import com.v2ray.ang.enums.NetworkType
|
import com.v2ray.ang.enums.NetworkType
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
@@ -21,7 +20,7 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
* @param str the Hysteria2 URI string to parse
|
* @param str the Hysteria2 URI string to parse
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
*/
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem {
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
@@ -73,7 +72,37 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
dicQuery["mport"] = config.portHopping.orEmpty()
|
dicQuery["mport"] = config.portHopping.orEmpty()
|
||||||
}
|
}
|
||||||
if (config.portHoppingInterval.isNotNullEmpty()) {
|
if (config.portHoppingInterval.isNotNullEmpty()) {
|
||||||
dicQuery["mportHopInt"] = config.portHoppingInterval.orEmpty()
|
val rawInterval = config.portHoppingInterval?.trim().nullIfBlank()
|
||||||
|
val interval = if (rawInterval == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val singleValue = rawInterval.toIntOrNull()
|
||||||
|
if (singleValue != null) {
|
||||||
|
if (singleValue < 5) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
rawInterval
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val parts = rawInterval.split('-')
|
||||||
|
if (parts.size == 2) {
|
||||||
|
val start = parts[0].trim().toIntOrNull()
|
||||||
|
val end = parts[1].trim().toIntOrNull()
|
||||||
|
if (start != null && end != null) {
|
||||||
|
val minStart = maxOf(5, start)
|
||||||
|
val minEnd = maxOf(minStart, end)
|
||||||
|
(minStart + minEnd) / 2
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (interval != null) {
|
||||||
|
dicQuery["mportHopInt"] = interval.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (config.pinnedCA256.isNotNullEmpty()) {
|
if (config.pinnedCA256.isNotNullEmpty()) {
|
||||||
dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty()
|
dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty()
|
||||||
@@ -107,18 +136,6 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileItem.obfsPassword.isNotNullEmpty()) {
|
|
||||||
outboundBean.streamSettings?.finalmask = FinalMaskBean(
|
|
||||||
udp = listOf(
|
|
||||||
FinalMaskBean.MaskBean(
|
|
||||||
type = "salamander",
|
|
||||||
settings = FinalMaskBean.MaskBean.MaskSettingsBean(
|
|
||||||
password = profileItem.obfsPassword
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.v2ray.ang.fmt
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
@@ -8,6 +7,7 @@ import com.v2ray.ang.enums.EConfigType
|
|||||||
import com.v2ray.ang.enums.NetworkType
|
import com.v2ray.ang.enums.NetworkType
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
config.remarks =
|
config.remarks =
|
||||||
Utils.decodeURIComponent(result.substring(indexSplit + 1, result.length))
|
Utils.decodeURIComponent(result.substring(indexSplit + 1, result.length))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
|
LogUtil.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = result.substring(0, indexSplit)
|
result = result.substring(0, indexSplit)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ object TrojanFmt : FmtBase() {
|
|||||||
* @param str the Trojan URI string to parse
|
* @param str the Trojan URI string to parse
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
*/
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem {
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.v2ray.ang.fmt
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
@@ -13,6 +12,7 @@ import com.v2ray.ang.extension.nullIfBlank
|
|||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ object VmessFmt : FmtBase() {
|
|||||||
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
||||||
result = Utils.decode(result)
|
result = Utils.decode(result)
|
||||||
if (TextUtils.isEmpty(result)) {
|
if (TextUtils.isEmpty(result)) {
|
||||||
Log.w(AppConfig.TAG, "Toast decoding failed")
|
LogUtil.w(AppConfig.TAG, "Toast decoding failed")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java) ?: return null
|
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java) ?: return null
|
||||||
@@ -44,7 +44,7 @@ object VmessFmt : FmtBase() {
|
|||||||
|| TextUtils.isEmpty(vmessQRCode.id)
|
|| TextUtils.isEmpty(vmessQRCode.id)
|
||||||
|| TextUtils.isEmpty(vmessQRCode.net)
|
|| TextUtils.isEmpty(vmessQRCode.net)
|
||||||
) {
|
) {
|
||||||
Log.w(AppConfig.TAG, "Toast incorrect protocol")
|
LogUtil.w(AppConfig.TAG, "Toast incorrect protocol")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ object WireguardFmt : FmtBase() {
|
|||||||
* @param str the Wireguard configuration file string to parse
|
* @param str the Wireguard configuration file string to parse
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
*/
|
*/
|
||||||
fun parseWireguardConfFile(str: String): ProfileItem? {
|
fun parseWireguardConfFile(str: String): ProfileItem {
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
val interfaceParams: MutableMap<String, String> = mutableMapOf()
|
val interfaceParams: MutableMap<String, String> = mutableMapOf()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.handler
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.HY2
|
import com.v2ray.ang.AppConfig.HY2
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -23,6 +22,7 @@ import com.v2ray.ang.fmt.VmessFmt
|
|||||||
import com.v2ray.ang.fmt.WireguardFmt
|
import com.v2ray.ang.fmt.WireguardFmt
|
||||||
import com.v2ray.ang.util.HttpUtil
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -47,7 +47,7 @@ object AngConfigManager {
|
|||||||
Utils.setClipboard(context, conf)
|
Utils.setClipboard(context, conf)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to share config to clipboard", e)
|
LogUtil.e(AppConfig.TAG, "Failed to share config to clipboard", e)
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
@@ -76,7 +76,7 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
return sb.lines().count() - 1
|
return sb.lines().count() - 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
|
LogUtil.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ object AngConfigManager {
|
|||||||
return QRCodeDecoder.createQRCode(conf)
|
return QRCodeDecoder.createQRCode(conf)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to share config as QR code", e)
|
LogUtil.e(AppConfig.TAG, "Failed to share config as QR code", e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ object AngConfigManager {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
|
LogUtil.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
@@ -148,7 +148,7 @@ object AngConfigManager {
|
|||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
|
LogUtil.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ object AngConfigManager {
|
|||||||
count = parseBatchConfig(server, subid, append)
|
count = parseBatchConfig(server, subid, append)
|
||||||
}
|
}
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
count = parseCustomConfigServer(server, subid)
|
count = parseCustomConfigServer(server, subid, append)
|
||||||
}
|
}
|
||||||
|
|
||||||
var countSub = parseBatchSubscription(server)
|
var countSub = parseBatchSubscription(server)
|
||||||
@@ -203,7 +203,7 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse batch subscription", e)
|
LogUtil.e(AppConfig.TAG, "Failed to parse batch subscription", e)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -221,21 +221,14 @@ object AngConfigManager {
|
|||||||
if (servers == null) {
|
if (servers == null) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
val removedSelectedServer =
|
// Find the currently selected server that matches the subscription ID
|
||||||
if (!TextUtils.isEmpty(subid) && !append) {
|
val removedSelected = if (subid.isNotBlank() && !append) {
|
||||||
MmkvManager.decodeServerConfig(
|
MmkvManager.getSelectServer()
|
||||||
MmkvManager.getSelectServer().orEmpty()
|
.takeIf { it?.isNotBlank() == true }
|
||||||
)?.let {
|
?.let { MmkvManager.decodeServerConfig(it) }
|
||||||
if (it.subscriptionId == subid) {
|
?.takeIf { it.subscriptionId == subid }
|
||||||
return@let it
|
} else {
|
||||||
}
|
null
|
||||||
return@let null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
@@ -254,23 +247,17 @@ object AngConfigManager {
|
|||||||
|
|
||||||
// Batch save all parsed configs (only one serverList read/write)
|
// Batch save all parsed configs (only one serverList read/write)
|
||||||
if (configs.isNotEmpty()) {
|
if (configs.isNotEmpty()) {
|
||||||
val keys = batchSaveConfigs(configs, subid)
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
// Handle removed selected server
|
|
||||||
removedSelectedServer?.let { removed ->
|
|
||||||
val matchKey = keys.find { key ->
|
|
||||||
val savedConfig = MmkvManager.decodeServerConfig(key)
|
|
||||||
savedConfig != null &&
|
|
||||||
savedConfig.server == removed.server &&
|
|
||||||
savedConfig.serverPort == removed.serverPort
|
|
||||||
}
|
|
||||||
matchKey?.let { MmkvManager.setSelectServer(it) }
|
|
||||||
}
|
}
|
||||||
|
val keyToProfile = batchSaveConfigs(configs, subid)
|
||||||
|
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||||
|
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs.size
|
return configs.size
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
LogUtil.e(AppConfig.TAG, "Failed to parse batch config", e)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -281,10 +268,10 @@ object AngConfigManager {
|
|||||||
*
|
*
|
||||||
* @param configs The list of ProfileItem to save.
|
* @param configs The list of ProfileItem to save.
|
||||||
* @param subid The subscription ID.
|
* @param subid The subscription ID.
|
||||||
* @return The list of generated keys.
|
* @return Map of generated keys to their corresponding ProfileItem.
|
||||||
*/
|
*/
|
||||||
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): List<String> {
|
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): Map<String, ProfileItem> {
|
||||||
val keys = mutableListOf<String>()
|
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
||||||
|
|
||||||
// Read serverList once
|
// Read serverList once
|
||||||
val serverList = MmkvManager.decodeServerList(subid)
|
val serverList = MmkvManager.decodeServerList(subid)
|
||||||
@@ -302,12 +289,67 @@ object AngConfigManager {
|
|||||||
needSetSelected = false
|
needSetSelected = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keys.add(key)
|
keyToProfile[key] = config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write serverList once
|
// Write serverList once
|
||||||
MmkvManager.encodeServerList(serverList, subid)
|
MmkvManager.encodeServerList(serverList, subid)
|
||||||
return keys
|
return keyToProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a matched profile key from the given key-profile map using multi-level matching.
|
||||||
|
* Matching priority (from highest to lowest):
|
||||||
|
* 1. Exact match: server + port + password
|
||||||
|
* 2. Match by remarks (exact match)
|
||||||
|
* 3. Match by server + port
|
||||||
|
* 4. Match by server only
|
||||||
|
*
|
||||||
|
* @param keyToProfile Map of server keys to their ProfileItem
|
||||||
|
* @param target Target profile to match
|
||||||
|
* @return Matched key or null
|
||||||
|
*/
|
||||||
|
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
|
||||||
|
if (keyToProfile.isEmpty() || target == null) return null
|
||||||
|
|
||||||
|
// Level 1: Match by remarks
|
||||||
|
if (target.remarks.isNotBlank()) {
|
||||||
|
keyToProfile.entries.firstOrNull { (_, saved) ->
|
||||||
|
isSameText(saved.remarks, target.remarks)
|
||||||
|
}?.key?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 2: Exact match (server + port + password)
|
||||||
|
keyToProfile.entries.firstOrNull { (_, saved) ->
|
||||||
|
isSameText(saved.server, target.server) &&
|
||||||
|
isSameText(saved.serverPort, target.serverPort) &&
|
||||||
|
isSameText(saved.password, target.password)
|
||||||
|
}?.key?.let { return it }
|
||||||
|
|
||||||
|
// Level 3: Match by server + port
|
||||||
|
keyToProfile.entries.firstOrNull { (_, saved) ->
|
||||||
|
isSameText(saved.server, target.server) &&
|
||||||
|
isSameText(saved.serverPort, target.serverPort)
|
||||||
|
}?.key?.let { return it }
|
||||||
|
|
||||||
|
// Level 4: Match by server only
|
||||||
|
keyToProfile.entries.firstOrNull { (_, saved) ->
|
||||||
|
isSameText(saved.server, target.server)
|
||||||
|
}?.key?.let { return it }
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case-insensitive trimmed string comparison.
|
||||||
|
*
|
||||||
|
* @param left First string
|
||||||
|
* @param right Second string
|
||||||
|
* @return True if both are non-empty and equal (case-insensitive, trimmed)
|
||||||
|
*/
|
||||||
|
private fun isSameText(left: String?, right: String?): Boolean {
|
||||||
|
if (left.isNullOrBlank() || right.isNullOrBlank()) return false
|
||||||
|
return left.trim().equals(right.trim(), ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -315,9 +357,10 @@ object AngConfigManager {
|
|||||||
*
|
*
|
||||||
* @param server The server string.
|
* @param server The server string.
|
||||||
* @param subid The subscription ID.
|
* @param subid The subscription ID.
|
||||||
|
* @param append Whether to append the configurations.
|
||||||
* @return The number of configurations parsed.
|
* @return The number of configurations parsed.
|
||||||
*/
|
*/
|
||||||
private fun parseCustomConfigServer(server: String?, subid: String): Int {
|
private fun parseCustomConfigServer(server: String?, subid: String, append: Boolean): Int {
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -330,6 +373,9 @@ object AngConfigManager {
|
|||||||
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
if (serverList.isNotEmpty()) {
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
var count = 0
|
var count = 0
|
||||||
for (srv in serverList.reversed()) {
|
for (srv in serverList.reversed()) {
|
||||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
||||||
@@ -342,7 +388,7 @@ object AngConfigManager {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
LogUtil.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -350,22 +396,28 @@ object AngConfigManager {
|
|||||||
val config = CustomFmt.parse(server) ?: return 0
|
val config = CustomFmt.parse(server) ?: return 0
|
||||||
config.subscriptionId = subid
|
config.subscriptionId = subid
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
return 1
|
return 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
LogUtil.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
||||||
try {
|
try {
|
||||||
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
return 1
|
return 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
LogUtil.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
} else {
|
} else {
|
||||||
@@ -426,7 +478,7 @@ object AngConfigManager {
|
|||||||
|
|
||||||
return config
|
return config
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse config", e)
|
LogUtil.e(AppConfig.TAG, "Failed to parse config", e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -443,7 +495,7 @@ object AngConfigManager {
|
|||||||
acc + updateConfigViaSub(subscription)
|
acc + updateConfigViaSub(subscription)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
|
LogUtil.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
|
||||||
SubscriptionUpdateResult()
|
SubscriptionUpdateResult()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,21 +530,21 @@ object AngConfigManager {
|
|||||||
return SubscriptionUpdateResult(failureCount = 1)
|
return SubscriptionUpdateResult(failureCount = 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(AppConfig.TAG, url)
|
LogUtil.i(AppConfig.TAG, url)
|
||||||
val userAgent = it.subscription.userAgent
|
val userAgent = it.subscription.userAgent
|
||||||
|
|
||||||
var configText = try {
|
var configText = try {
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort)
|
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
if (configText.isEmpty()) {
|
if (configText.isEmpty()) {
|
||||||
configText = try {
|
configText = try {
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent)
|
HttpUtil.getUrlContentWithUserAgent(url, userAgent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
LogUtil.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,7 +556,7 @@ object AngConfigManager {
|
|||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
it.subscription.lastUpdated = System.currentTimeMillis()
|
it.subscription.lastUpdated = System.currentTimeMillis()
|
||||||
MmkvManager.encodeSubscription(it.guid, it.subscription)
|
MmkvManager.encodeSubscription(it.guid, it.subscription)
|
||||||
Log.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs")
|
LogUtil.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs")
|
||||||
return SubscriptionUpdateResult(
|
return SubscriptionUpdateResult(
|
||||||
configCount = count,
|
configCount = count,
|
||||||
successCount = 1
|
successCount = 1
|
||||||
@@ -514,7 +566,7 @@ object AngConfigManager {
|
|||||||
return SubscriptionUpdateResult(failureCount = 1)
|
return SubscriptionUpdateResult(failureCount = 1)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
|
LogUtil.e(AppConfig.TAG, "Failed to update config via subscription", e)
|
||||||
return SubscriptionUpdateResult(failureCount = 1)
|
return SubscriptionUpdateResult(failureCount = 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,7 +585,7 @@ object AngConfigManager {
|
|||||||
count = parseBatchConfig(server, subid, append)
|
count = parseBatchConfig(server, subid, append)
|
||||||
}
|
}
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
count = parseCustomConfigServer(server, subid)
|
count = parseCustomConfigServer(server, subid, append)
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ object MmkvManager {
|
|||||||
|
|
||||||
//region Subscriptions
|
//region Subscriptions
|
||||||
|
|
||||||
private fun getSubscriptionId(subscriptionId: String?):String {
|
private fun getSubscriptionId(subscriptionId: String?): String {
|
||||||
return subscriptionId?.ifEmpty { DEFAULT_SUBSCRIPTION_ID } ?: DEFAULT_SUBSCRIPTION_ID
|
return subscriptionId?.ifEmpty { DEFAULT_SUBSCRIPTION_ID } ?: DEFAULT_SUBSCRIPTION_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +374,7 @@ object MmkvManager {
|
|||||||
decodeSubsList().forEach { key ->
|
decodeSubsList().forEach { key ->
|
||||||
val json = subStorage.decodeString(key)
|
val json = subStorage.decodeString(key)
|
||||||
if (!json.isNullOrBlank()) {
|
if (!json.isNullOrBlank()) {
|
||||||
val item = JsonUtil.fromJson(json, SubscriptionItem::class.java)?: SubscriptionItem()
|
val item = JsonUtil.fromJson(json, SubscriptionItem::class.java) ?: SubscriptionItem()
|
||||||
subscriptions.add(SubscriptionCache(key, item))
|
subscriptions.add(SubscriptionCache(key, item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,7 +442,7 @@ object MmkvManager {
|
|||||||
return if (json.isNullOrBlank()) {
|
return if (json.isNullOrBlank()) {
|
||||||
mutableListOf()
|
mutableListOf()
|
||||||
} else {
|
} else {
|
||||||
JsonUtil.fromJson(json, Array<String>::class.java)?.toMutableList()?: mutableListOf()
|
JsonUtil.fromJson(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,7 +460,7 @@ object MmkvManager {
|
|||||||
assetStorage.allKeys()?.forEach { key ->
|
assetStorage.allKeys()?.forEach { key ->
|
||||||
val json = assetStorage.decodeString(key)
|
val json = assetStorage.decodeString(key)
|
||||||
if (!json.isNullOrBlank()) {
|
if (!json.isNullOrBlank()) {
|
||||||
val item = JsonUtil.fromJson(json, AssetUrlItem::class.java)?: AssetUrlItem()
|
val item = JsonUtil.fromJson(json, AssetUrlItem::class.java) ?: AssetUrlItem()
|
||||||
assetUrlItems.add(AssetUrlCache(key, item))
|
assetUrlItems.add(AssetUrlCache(key, item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,7 @@ object MmkvManager {
|
|||||||
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
||||||
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
||||||
if (ruleset.isNullOrEmpty()) return null
|
if (ruleset.isNullOrEmpty()) return null
|
||||||
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java)?.toMutableList()?: mutableListOf()
|
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java)?.toMutableList() ?: mutableListOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
@@ -17,6 +16,7 @@ import com.v2ray.ang.R
|
|||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.toSpeedString
|
import com.v2ray.ang.extension.toSpeedString
|
||||||
import com.v2ray.ang.ui.MainActivity
|
import com.v2ray.ang.ui.MainActivity
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -57,7 +57,7 @@ object NotificationManager {
|
|||||||
|
|
||||||
// If the query interval is too short, skip this round to avoid excessive CPU usage
|
// If the query interval is too short, skip this round to avoid excessive CPU usage
|
||||||
if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
|
if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
|
||||||
Log.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
|
LogUtil.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
|
||||||
lastQueryTime = queryTime
|
lastQueryTime = queryTime
|
||||||
delay(QUERY_INTERVAL_MS)
|
delay(QUERY_INTERVAL_MS)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package com.v2ray.ang.handler
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
|
import android.os.Build
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
@@ -27,14 +27,19 @@ import com.v2ray.ang.handler.MmkvManager.decodeSubscription
|
|||||||
import com.v2ray.ang.handler.MmkvManager.encodeSubscription
|
import com.v2ray.ang.handler.MmkvManager.encodeSubscription
|
||||||
import com.v2ray.ang.handler.MmkvManager.removeSubscription
|
import com.v2ray.ang.handler.MmkvManager.removeSubscription
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
object SettingsManager {
|
object SettingsManager {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var runtimeSocksPort: Int? = null
|
||||||
|
|
||||||
fun initApp(context: Context) {
|
fun initApp(context: Context) {
|
||||||
ensureDefaultSettings()
|
ensureDefaultSettings()
|
||||||
//ensureDefaultSubscription()
|
//ensureDefaultSubscription()
|
||||||
@@ -100,7 +105,7 @@ object SettingsManager {
|
|||||||
resetRoutingRulesetsCommon(rulesetList)
|
resetRoutingRulesetsCommon(rulesetList)
|
||||||
return true
|
return true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
|
LogUtil.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +224,7 @@ object SettingsManager {
|
|||||||
* @param toPosition The position to swap to.
|
* @param toPosition The position to swap to.
|
||||||
*/
|
*/
|
||||||
fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
|
fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
|
||||||
val subsList = MmkvManager.decodeSubsList()
|
val subsList = decodeSubsList()
|
||||||
if (subsList.isEmpty()) return
|
if (subsList.isEmpty()) return
|
||||||
|
|
||||||
Collections.swap(subsList, fromPosition, toPosition)
|
Collections.swap(subsList, fromPosition, toPosition)
|
||||||
@@ -249,7 +254,7 @@ object SettingsManager {
|
|||||||
fun removeSubscriptionWithDefault(subid: String) {
|
fun removeSubscriptionWithDefault(subid: String) {
|
||||||
// val subsList = decodeSubsList()
|
// val subsList = decodeSubsList()
|
||||||
// if (subsList.size == 1 && subsList.first() == DEFAULT_SUBSCRIPTION_ID) {
|
// if (subsList.size == 1 && subsList.first() == DEFAULT_SUBSCRIPTION_ID) {
|
||||||
// Log.i(ANG_PACKAGE,"Attempted to remove the only existing default subscription, operation ignored.")
|
// LogUtil.i(ANG_PACKAGE,"Attempted to remove the only existing default subscription, operation ignored.")
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@@ -273,7 +278,30 @@ object SettingsManager {
|
|||||||
* @return The SOCKS port.
|
* @return The SOCKS port.
|
||||||
*/
|
*/
|
||||||
fun getSocksPort(): Int {
|
fun getSocksPort(): Int {
|
||||||
return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
val port =
|
||||||
|
if (IsDynamicSocksPort()) {
|
||||||
|
runtimeSocksPort ?: refreshRuntimeSocksPort()
|
||||||
|
} else {
|
||||||
|
Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
||||||
|
}
|
||||||
|
return port ?: AppConfig.PORT_SOCKS.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun refreshRuntimeSocksPort(): Int? {
|
||||||
|
if (IsDynamicSocksPort()) {
|
||||||
|
runtimeSocksPort = generateRandomSocksPort()
|
||||||
|
return runtimeSocksPort
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSocksUsername(): String? {
|
||||||
|
return MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_USERNAME)?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSocksPassword(): String? {
|
||||||
|
return MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PASSWORD)?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,6 +312,18 @@ object SettingsManager {
|
|||||||
return getSocksPort() + if (Utils.isXray()) 0 else 1
|
return getSocksPort() + if (Utils.isXray()) 0 else 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun IsDynamicSocksPort(): Boolean {
|
||||||
|
return MmkvManager.decodeSettingsBool(AppConfig.PREF_DYNAMIC_SOCKS_PORT, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateRandomSocksPort(): Int {
|
||||||
|
return if (Utils.isXray()) {
|
||||||
|
Random.nextInt(10000, 65536)
|
||||||
|
} else {
|
||||||
|
Random.nextInt(10000, 65535)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize assets.
|
* Initialize assets.
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
@@ -304,10 +344,10 @@ object SettingsManager {
|
|||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
|
LogUtil.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
LogUtil.e(ANG_PACKAGE, "asset copy failed", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,12 +473,34 @@ object SettingsManager {
|
|||||||
return mode == null || mode == VPN
|
return mode == null || mode == VPN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if process routing can be used.
|
||||||
|
*/
|
||||||
|
fun canUseProcessRouting(): Boolean {
|
||||||
|
// Android 10+
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must xray tun
|
||||||
|
if (isUsingHevTun()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have route only enabled
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false) == false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure default settings are present in MMKV.
|
* Ensure default settings are present in MMKV.
|
||||||
*/
|
*/
|
||||||
private fun ensureDefaultSettings() {
|
private fun ensureDefaultSettings() {
|
||||||
// Write defaults in the exact order requested by the user
|
// Write defaults in the exact order requested by the user
|
||||||
ensureDefaultValue(AppConfig.PREF_MODE, AppConfig.VPN)
|
ensureDefaultValue(AppConfig.PREF_MODE, VPN)
|
||||||
ensureDefaultValue(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
ensureDefaultValue(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
||||||
ensureDefaultValue(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
|
ensureDefaultValue(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
|
||||||
ensureDefaultValue(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
ensureDefaultValue(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.v2ray.ang.handler
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.IPAPIInfo
|
import com.v2ray.ang.dto.IPAPIInfo
|
||||||
import com.v2ray.ang.util.HttpUtil
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -62,11 +62,11 @@ object SpeedtestManager {
|
|||||||
socket.close()
|
socket.close()
|
||||||
return time
|
return time
|
||||||
} catch (e: UnknownHostException) {
|
} catch (e: UnknownHostException) {
|
||||||
Log.e(AppConfig.TAG, "Unknown host: $url", e)
|
LogUtil.e(AppConfig.TAG, "Unknown host: $url", e)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(AppConfig.TAG, "socketConnectTime IOException: $e")
|
LogUtil.e(AppConfig.TAG, "socketConnectTime IOException: $e")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
|
LogUtil.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
@@ -108,10 +108,10 @@ object SpeedtestManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(AppConfig.TAG, "Connection test IOException", e)
|
LogUtil.e(AppConfig.TAG, "Connection test IOException", e)
|
||||||
result = context.getString(R.string.connection_test_error, e.message)
|
result = context.getString(R.string.connection_test_error, e.message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Connection test Exception", e)
|
LogUtil.e(AppConfig.TAG, "Connection test Exception", e)
|
||||||
result = context.getString(R.string.connection_test_error, e.message)
|
result = context.getString(R.string.connection_test_error, e.message)
|
||||||
} finally {
|
} finally {
|
||||||
conn.disconnect()
|
conn.disconnect()
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
|
||||||
object SubscriptionUpdater {
|
object SubscriptionUpdater {
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ object SubscriptionUpdater {
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Log.i(AppConfig.TAG, "subscription automatic update starting")
|
LogUtil.i(AppConfig.TAG, "subscription automatic update starting")
|
||||||
|
|
||||||
val subs = MmkvManager.decodeSubscriptions().filter { it.subscription.autoUpdate }
|
val subs = MmkvManager.decodeSubscriptions().filter { it.subscription.autoUpdate }
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ object SubscriptionUpdater {
|
|||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
notificationManager.notify(3, notification.build())
|
notificationManager.notify(3, notification.build())
|
||||||
Log.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
|
LogUtil.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
|
||||||
AngConfigManager.updateConfigViaSub(sub)
|
AngConfigManager.updateConfigViaSub(sub)
|
||||||
notification.setContentText("Updating ${subItem.remarks}")
|
notification.setContentText("Updating ${subItem.remarks}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.v2ray.ang.handler
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.dto.CheckUpdateResult
|
import com.v2ray.ang.dto.CheckUpdateResult
|
||||||
@@ -10,6 +9,7 @@ import com.v2ray.ang.dto.GitHubRelease
|
|||||||
import com.v2ray.ang.extension.concatUrl
|
import com.v2ray.ang.extension.concatUrl
|
||||||
import com.v2ray.ang.util.HttpUtil
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -42,7 +42,7 @@ object UpdateCheckerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val latestVersion = latestRelease.tagName.removePrefix("v")
|
val latestVersion = latestRelease.tagName.removePrefix("v")
|
||||||
Log.i(
|
LogUtil.i(
|
||||||
AppConfig.TAG,
|
AppConfig.TAG,
|
||||||
"Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})"
|
"Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})"
|
||||||
)
|
)
|
||||||
@@ -69,27 +69,27 @@ object UpdateCheckerManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val apkFile = File(context.cacheDir, "update.apk")
|
val apkFile = File(context.cacheDir, "update.apk")
|
||||||
Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
|
LogUtil.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
|
||||||
|
|
||||||
FileOutputStream(apkFile).use { outputStream ->
|
FileOutputStream(apkFile).use { outputStream ->
|
||||||
connection.inputStream.use { inputStream ->
|
connection.inputStream.use { inputStream ->
|
||||||
inputStream.copyTo(outputStream)
|
inputStream.copyTo(outputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(AppConfig.TAG, "APK download completed")
|
LogUtil.i(AppConfig.TAG, "APK download completed")
|
||||||
return@withContext apkFile
|
return@withContext apkFile
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
|
LogUtil.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error closing connection: ${e.message}")
|
LogUtil.e(AppConfig.TAG, "Error closing connection: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
|
LogUtil.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.v2ray.ang.handler
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import libv2ray.CoreCallbackHandler
|
import libv2ray.CoreCallbackHandler
|
||||||
@@ -32,14 +32,14 @@ object V2RayNativeManager {
|
|||||||
val assetPath = Utils.userAssetPath(context)
|
val assetPath = Utils.userAssetPath(context)
|
||||||
val deviceId = Utils.getDeviceIdForXUDPBaseKey()
|
val deviceId = Utils.getDeviceIdForXUDPBaseKey()
|
||||||
Libv2ray.initCoreEnv(assetPath, deviceId)
|
Libv2ray.initCoreEnv(assetPath, deviceId)
|
||||||
Log.i(AppConfig.TAG, "V2Ray core environment initialized successfully")
|
LogUtil.i(AppConfig.TAG, "V2Ray core environment initialized successfully")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to initialize V2Ray core environment", e)
|
LogUtil.e(AppConfig.TAG, "Failed to initialize V2Ray core environment", e)
|
||||||
initialized.set(false)
|
initialized.set(false)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(AppConfig.TAG, "V2Ray core environment already initialized, skipping")
|
LogUtil.d(AppConfig.TAG, "V2Ray core environment already initialized, skipping")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ object V2RayNativeManager {
|
|||||||
return try {
|
return try {
|
||||||
Libv2ray.checkVersionX()
|
Libv2ray.checkVersionX()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to check V2Ray version", e)
|
LogUtil.e(AppConfig.TAG, "Failed to check V2Ray version", e)
|
||||||
"Unknown"
|
"Unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ object V2RayNativeManager {
|
|||||||
return try {
|
return try {
|
||||||
Libv2ray.measureOutboundDelay(config, testUrl)
|
Libv2ray.measureOutboundDelay(config, testUrl)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to measure outbound delay", e)
|
LogUtil.e(AppConfig.TAG, "Failed to measure outbound delay", e)
|
||||||
-1L
|
-1L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ object V2RayNativeManager {
|
|||||||
return try {
|
return try {
|
||||||
Libv2ray.newCoreController(handler)
|
Libv2ray.newCoreController(handler)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to create core controller", e)
|
LogUtil.e(AppConfig.TAG, "Failed to create core controller", e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.system.OsConstants
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -16,6 +18,7 @@ import com.v2ray.ang.enums.EConfigType
|
|||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.service.V2RayProxyOnlyService
|
import com.v2ray.ang.service.V2RayProxyOnlyService
|
||||||
import com.v2ray.ang.service.V2RayVpnService
|
import com.v2ray.ang.service.V2RayVpnService
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -23,18 +26,26 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import libv2ray.CoreCallbackHandler
|
import libv2ray.CoreCallbackHandler
|
||||||
import libv2ray.CoreController
|
import libv2ray.CoreController
|
||||||
|
import libv2ray.ProcessFinder
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
|
||||||
object V2RayServiceManager {
|
object V2RayServiceManager {
|
||||||
|
|
||||||
private val coreController: CoreController = V2RayNativeManager.newCoreController(CoreCallback())
|
private val coreController: CoreController = V2RayNativeManager.newCoreController(CoreCallback())
|
||||||
private val mMsgReceive = ReceiveMessageHandler()
|
private val mMsgReceive = ReceiveMessageHandler()
|
||||||
private var currentConfig: ProfileItem? = null
|
private var currentConfig: ProfileItem? = null
|
||||||
|
private var processFinder: XrayProcessFinder? = null
|
||||||
|
|
||||||
var serviceControl: SoftReference<ServiceControl>? = null
|
var serviceControl: SoftReference<ServiceControl>? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
V2RayNativeManager.initCoreEnv(value?.get()?.getService())
|
val service = value?.get()?.getService()
|
||||||
|
V2RayNativeManager.initCoreEnv(service)
|
||||||
|
if (service != null && processFinder == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
processFinder = XrayProcessFinder(service)
|
||||||
|
coreController.registerProcessFinder(processFinder)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +68,7 @@ object V2RayServiceManager {
|
|||||||
* @param guid The GUID of the server configuration to use (optional).
|
* @param guid The GUID of the server configuration to use (optional).
|
||||||
*/
|
*/
|
||||||
fun startVService(context: Context, guid: String? = null) {
|
fun startVService(context: Context, guid: String? = null) {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
|
||||||
|
|
||||||
if (guid != null) {
|
if (guid != null) {
|
||||||
MmkvManager.setSelectServer(guid)
|
MmkvManager.setSelectServer(guid)
|
||||||
@@ -94,19 +105,19 @@ object V2RayServiceManager {
|
|||||||
*/
|
*/
|
||||||
private fun startContextService(context: Context) {
|
private fun startContextService(context: Context) {
|
||||||
if (coreController.isRunning) {
|
if (coreController.isRunning) {
|
||||||
Log.w(AppConfig.TAG, "StartCore-Manager: Core already running")
|
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val guid = MmkvManager.getSelectServer()
|
val guid = MmkvManager.getSelectServer()
|
||||||
if (guid == null) {
|
if (guid == null) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: No server selected")
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid)
|
val config = MmkvManager.decodeServerConfig(guid)
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,9 +126,12 @@ object V2RayServiceManager {
|
|||||||
&& !Utils.isValidUrl(config.server)
|
&& !Utils.isValidUrl(config.server)
|
||||||
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
||||||
) {
|
) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration")
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// refresh socks port when enabled dynamic socks port
|
||||||
|
SettingsManager.refreshRuntimeSocksPort()
|
||||||
|
|
||||||
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
||||||
// if (!result.status) return
|
// if (!result.status) return
|
||||||
|
|
||||||
@@ -129,17 +143,17 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
val isVpnMode = SettingsManager.isVpnMode()
|
val isVpnMode = SettingsManager.isVpnMode()
|
||||||
val intent = if (isVpnMode) {
|
val intent = if (isVpnMode) {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service")
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service")
|
||||||
Intent(context.applicationContext, V2RayVpnService::class.java)
|
Intent(context.applicationContext, V2RayVpnService::class.java)
|
||||||
} else {
|
} else {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Starting Proxy service")
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting Proxy service")
|
||||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to start service", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to start service", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,32 +164,33 @@ object V2RayServiceManager {
|
|||||||
*/
|
*/
|
||||||
fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean {
|
fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean {
|
||||||
if (coreController.isRunning) {
|
if (coreController.isRunning) {
|
||||||
Log.w(AppConfig.TAG, "StartCore-Manager: Core already running")
|
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val service = getService()
|
val service = getService()
|
||||||
if (service == null) {
|
if (service == null) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Service is null")
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Service is null")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val guid = MmkvManager.getSelectServer()
|
val guid = MmkvManager.getSelectServer()
|
||||||
if (guid == null) {
|
if (guid == null) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: No server selected")
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid)
|
val config = MmkvManager.decodeServerConfig(guid)
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}")
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}")
|
||||||
val result = V2rayConfigManager.getV2rayConfig(service, guid)
|
val result = V2rayConfigManager.getV2rayConfig(service, guid)
|
||||||
|
LogUtil.d(AppConfig.TAG, result.content)
|
||||||
if (!result.status) {
|
if (!result.status) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to get V2Ray config")
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to get V2Ray config")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +201,7 @@ object V2RayServiceManager {
|
|||||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||||
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to register receiver", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to register receiver", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,12 +215,12 @@ object V2RayServiceManager {
|
|||||||
NotificationManager.showNotification(currentConfig)
|
NotificationManager.showNotification(currentConfig)
|
||||||
coreController.startLoop(result.content, tunFd)
|
coreController.startLoop(result.content, tunFd)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to start core loop", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to start core loop", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coreController.isRunning == false) {
|
if (coreController.isRunning == false) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Core failed to start")
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Core failed to start")
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
||||||
NotificationManager.cancelNotification()
|
NotificationManager.cancelNotification()
|
||||||
return false
|
return false
|
||||||
@@ -214,9 +229,9 @@ object V2RayServiceManager {
|
|||||||
try {
|
try {
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||||
NotificationManager.startSpeedNotification(currentConfig)
|
NotificationManager.startSpeedNotification(currentConfig)
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to complete startup", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to complete startup", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -235,7 +250,7 @@ object V2RayServiceManager {
|
|||||||
try {
|
try {
|
||||||
coreController.stopLoop()
|
coreController.stopLoop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,7 +261,7 @@ object V2RayServiceManager {
|
|||||||
try {
|
try {
|
||||||
service.unregisterReceiver(mMsgReceive)
|
service.unregisterReceiver(mMsgReceive)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -280,14 +295,14 @@ object V2RayServiceManager {
|
|||||||
try {
|
try {
|
||||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
|
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
||||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
}
|
}
|
||||||
if (time == -1L) {
|
if (time == -1L) {
|
||||||
try {
|
try {
|
||||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
||||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +354,7 @@ object V2RayServiceManager {
|
|||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
0
|
0
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
|
||||||
-1
|
-1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,6 +370,43 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process finder implementation for Xray core.
|
||||||
|
* Uses ConnectivityManager to find the owning UID of a connection based on network parameters.
|
||||||
|
*/
|
||||||
|
private class XrayProcessFinder(context: Context) : ProcessFinder {
|
||||||
|
private val cm: ConnectivityManager? = context.getSystemService(ConnectivityManager::class.java)
|
||||||
|
|
||||||
|
override fun findProcessByConnection(network: String, srcIP: String, srcPort: Long, destIP: String, destPort: Long): Long {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return -1L
|
||||||
|
if (cm == null) return -1L
|
||||||
|
val proto = when (network) {
|
||||||
|
"tcp" -> OsConstants.IPPROTO_TCP
|
||||||
|
"udp" -> OsConstants.IPPROTO_UDP
|
||||||
|
else -> return -1L
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destIP.isBlank() || destPort == 0L) {
|
||||||
|
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to :$destPort, (no dest)")
|
||||||
|
return -1L
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val uid = cm.getConnectionOwnerUid(
|
||||||
|
proto,
|
||||||
|
InetSocketAddress(srcIP, srcPort.toInt()),
|
||||||
|
InetSocketAddress(destIP, destPort.toInt())
|
||||||
|
).toLong()
|
||||||
|
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid")
|
||||||
|
//LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid,${PackageUidResolver.uidToPackageName(uid.toString())}")
|
||||||
|
|
||||||
|
uid
|
||||||
|
} catch (e: Exception) {
|
||||||
|
-1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast receiver for handling messages sent to the service.
|
* Broadcast receiver for handling messages sent to the service.
|
||||||
* Handles registration, service control, and screen events.
|
* Handles registration, service control, and screen events.
|
||||||
@@ -386,12 +438,12 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
AppConfig.MSG_STATE_STOP -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
AppConfig.MSG_STATE_RESTART -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
Thread.sleep(500L)
|
Thread.sleep(500L)
|
||||||
startVService(serviceControl.getService())
|
startVService(serviceControl.getService())
|
||||||
@@ -404,12 +456,12 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
Intent.ACTION_SCREEN_OFF -> {
|
Intent.ACTION_SCREEN_OFF -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Screen off")
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen off")
|
||||||
NotificationManager.stopSpeedNotification(currentConfig)
|
NotificationManager.stopSpeedNotification(currentConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent.ACTION_SCREEN_ON -> {
|
Intent.ACTION_SCREEN_ON -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Screen on")
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen on")
|
||||||
NotificationManager.startSpeedNotification(currentConfig)
|
NotificationManager.startSpeedNotification(currentConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package com.v2ray.ang.handler
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||||
import com.v2ray.ang.dto.ConfigResult
|
import com.v2ray.ang.dto.ConfigResult
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
@@ -27,6 +27,8 @@ import com.v2ray.ang.fmt.VmessFmt
|
|||||||
import com.v2ray.ang.fmt.WireguardFmt
|
import com.v2ray.ang.fmt.WireguardFmt
|
||||||
import com.v2ray.ang.util.HttpUtil
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
import com.v2ray.ang.util.PackageUidResolver
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
object V2rayConfigManager {
|
object V2rayConfigManager {
|
||||||
@@ -53,7 +55,7 @@ object V2rayConfigManager {
|
|||||||
getV2rayNormalConfig(context, guid, config)
|
getV2rayNormalConfig(context, guid, config)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to get V2ray config", e)
|
LogUtil.e(AppConfig.TAG, "Failed to get V2ray config", e)
|
||||||
return ConfigResult(false)
|
return ConfigResult(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +79,7 @@ object V2rayConfigManager {
|
|||||||
getV2rayNormalConfig4Speedtest(context, guid, config)
|
getV2rayNormalConfig4Speedtest(context, guid, config)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e)
|
LogUtil.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e)
|
||||||
return ConfigResult(false)
|
return ConfigResult(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +188,7 @@ object V2rayConfigManager {
|
|||||||
val address = config.server ?: return result
|
val address = config.server ?: return result
|
||||||
if (!Utils.isPureIpAddress(address)) {
|
if (!Utils.isPureIpAddress(address)) {
|
||||||
if (!Utils.isValidUrl(address)) {
|
if (!Utils.isValidUrl(address)) {
|
||||||
Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
|
LogUtil.w(AppConfig.TAG, "$address is an invalid ip or domain")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +202,7 @@ object V2rayConfigManager {
|
|||||||
getOutbounds(v2rayConfig, config) ?: return result
|
getOutbounds(v2rayConfig, config) ?: return result
|
||||||
getMoreOutbounds(v2rayConfig, config.subscriptionId)
|
getMoreOutbounds(v2rayConfig, config.subscriptionId)
|
||||||
|
|
||||||
getRouting(v2rayConfig)
|
getRouting(context, v2rayConfig)
|
||||||
|
|
||||||
getFakeDns(v2rayConfig)
|
getFakeDns(v2rayConfig)
|
||||||
|
|
||||||
@@ -233,7 +235,7 @@ object V2rayConfigManager {
|
|||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
if (validConfigs.isEmpty()) {
|
if (validConfigs.isEmpty()) {
|
||||||
Log.w(AppConfig.TAG, "All configs are invalid")
|
LogUtil.w(AppConfig.TAG, "All configs are invalid")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +259,7 @@ object V2rayConfigManager {
|
|||||||
outboundsList.addAll(v2rayConfig.outbounds)
|
outboundsList.addAll(v2rayConfig.outbounds)
|
||||||
v2rayConfig.outbounds = ArrayList(outboundsList)
|
v2rayConfig.outbounds = ArrayList(outboundsList)
|
||||||
|
|
||||||
getRouting(v2rayConfig)
|
getRouting(context, v2rayConfig)
|
||||||
|
|
||||||
getFakeDns(v2rayConfig)
|
getFakeDns(v2rayConfig)
|
||||||
|
|
||||||
@@ -295,7 +297,7 @@ object V2rayConfigManager {
|
|||||||
val address = config.server ?: return result
|
val address = config.server ?: return result
|
||||||
if (!Utils.isPureIpAddress(address)) {
|
if (!Utils.isPureIpAddress(address)) {
|
||||||
if (!Utils.isValidUrl(address)) {
|
if (!Utils.isValidUrl(address)) {
|
||||||
Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
|
LogUtil.w(AppConfig.TAG, "$address is an invalid ip or domain")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,12 +375,29 @@ object V2rayConfigManager {
|
|||||||
private fun getInbounds(v2rayConfig: V2rayConfig): Boolean {
|
private fun getInbounds(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
val socksPort = SettingsManager.getSocksPort()
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
|
val socksUsername = SettingsManager.getSocksUsername()
|
||||||
|
val socksPassword = SettingsManager.getSocksPassword()
|
||||||
val inbound1 = v2rayConfig.inbounds[0]
|
val inbound1 = v2rayConfig.inbounds[0]
|
||||||
|
if (inbound1.settings == null) {
|
||||||
|
inbound1.settings = V2rayConfig.InboundBean.InSettingsBean()
|
||||||
|
}
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
||||||
inbound1.listen = AppConfig.LOOPBACK
|
inbound1.listen = AppConfig.LOOPBACK
|
||||||
}
|
}
|
||||||
inbound1.port = socksPort
|
inbound1.port = socksPort
|
||||||
|
if (socksUsername != null && socksPassword != null) {
|
||||||
|
inbound1.settings?.auth = "password"
|
||||||
|
inbound1.settings?.accounts = listOf(
|
||||||
|
V2rayConfig.InboundBean.InSettingsBean.SocksAccountBean(
|
||||||
|
user = socksUsername,
|
||||||
|
pass = socksPassword
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
inbound1.settings?.auth = "noauth"
|
||||||
|
inbound1.settings?.accounts = null
|
||||||
|
}
|
||||||
val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
||||||
val sniffAllTlsAndHttp =
|
val sniffAllTlsAndHttp =
|
||||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
|
||||||
@@ -406,7 +425,7 @@ object V2rayConfigManager {
|
|||||||
inboundTun?.sniffing = inbound1.sniffing
|
inboundTun?.sniffing = inbound1.sniffing
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to configure inbounds", e)
|
LogUtil.e(AppConfig.TAG, "Failed to configure inbounds", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -427,6 +446,45 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-pass: scans all routing rulesets for non-builtin outbound tags,
|
||||||
|
* looks up the matching profile by remarks, converts it to an OutboundBean,
|
||||||
|
* and appends it to v2rayConfig.outbounds. This must run before getRouting
|
||||||
|
* so that every custom tag is already present when routing rules are applied.
|
||||||
|
*
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
*/
|
||||||
|
private fun injectCustomOutbounds(v2rayConfig: V2rayConfig) {
|
||||||
|
val existingTags = v2rayConfig.outbounds.mapTo(mutableSetOf()) { it.tag }
|
||||||
|
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return
|
||||||
|
|
||||||
|
rulesetItems
|
||||||
|
.filter { it.enabled }
|
||||||
|
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
|
||||||
|
.filter { it !in BUILTIN_OUTBOUND_TAGS }
|
||||||
|
.distinct()
|
||||||
|
.forEach { tag ->
|
||||||
|
if (tag in existingTags) return@forEach
|
||||||
|
try {
|
||||||
|
val profile = SettingsManager.getServerViaRemarks(tag) ?: run {
|
||||||
|
LogUtil.w(AppConfig.TAG, "Custom outbound tag '$tag' not found by remarks, skipping")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
val outbound = convertProfile2Outbound(profile) ?: run {
|
||||||
|
LogUtil.w(AppConfig.TAG, "Could not convert profile '$tag' to outbound, skipping")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
updateOutboundWithGlobalSettings(outbound)
|
||||||
|
outbound.tag = tag
|
||||||
|
v2rayConfig.outbounds.add(outbound)
|
||||||
|
existingTags.add(tag)
|
||||||
|
LogUtil.d(AppConfig.TAG, "Injected custom outbound: tag='$tag'")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to inject custom outbound for tag '$tag', skipping", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures routing settings for V2ray.
|
* Configures routing settings for V2ray.
|
||||||
*
|
*
|
||||||
@@ -435,19 +493,22 @@ object V2rayConfigManager {
|
|||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
* @return true if routing configuration was successful, false otherwise
|
* @return true if routing configuration was successful, false otherwise
|
||||||
*/
|
*/
|
||||||
private fun getRouting(v2rayConfig: V2rayConfig): Boolean {
|
private fun getRouting(context: Context, v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
v2rayConfig.routing.domainStrategy =
|
v2rayConfig.routing.domainStrategy =
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
||||||
?: "AsIs"
|
?: "AsIs"
|
||||||
|
|
||||||
|
// Pre-pass: inject outbounds referenced by remarks in routing rules
|
||||||
|
injectCustomOutbounds(v2rayConfig)
|
||||||
|
|
||||||
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
rulesetItems?.forEach { key ->
|
rulesetItems?.forEach { key ->
|
||||||
getRoutingUserRule(key, v2rayConfig)
|
getRoutingUserRule(context, key, v2rayConfig)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to configure routing", e)
|
LogUtil.e(AppConfig.TAG, "Failed to configure routing", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -459,7 +520,7 @@ object V2rayConfigManager {
|
|||||||
* @param item The ruleset item to add
|
* @param item The ruleset item to add
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
*/
|
*/
|
||||||
private fun getRoutingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
|
private fun getRoutingUserRule(context: Context, item: RulesetItem?, v2rayConfig: V2rayConfig) {
|
||||||
try {
|
try {
|
||||||
if (item == null || !item.enabled) {
|
if (item == null || !item.enabled) {
|
||||||
return
|
return
|
||||||
@@ -480,10 +541,32 @@ object V2rayConfigManager {
|
|||||||
rule.ip = updatedIpList
|
rule.ip = updatedIpList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SettingsManager.canUseProcessRouting()) {
|
||||||
|
// Convert process package names to UIDs
|
||||||
|
rule.process?.let { processList ->
|
||||||
|
if (processList.isNotEmpty()) {
|
||||||
|
val uids = PackageUidResolver.packageNamesToUids(context, processList)
|
||||||
|
rule.process = uids.ifEmpty { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rule.process = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the outbound tag is a custom one that failed to inject, fall back to proxy
|
||||||
|
val outboundTag = rule.outboundTag
|
||||||
|
if (!outboundTag.isNullOrBlank()
|
||||||
|
&& outboundTag !in BUILTIN_OUTBOUND_TAGS
|
||||||
|
&& v2rayConfig.outbounds.none { it.tag == outboundTag }
|
||||||
|
) {
|
||||||
|
LogUtil.w(AppConfig.TAG, "Outbound tag '$outboundTag' not found, falling back to '${AppConfig.TAG_PROXY}'")
|
||||||
|
rule.outboundTag = AppConfig.TAG_PROXY
|
||||||
|
}
|
||||||
|
|
||||||
v2rayConfig.routing.rules.add(rule)
|
v2rayConfig.routing.rules.add(rule)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to apply routing user rule", e)
|
LogUtil.e(AppConfig.TAG, "Failed to apply routing user rule", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,7 +621,7 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(SettingsManager.isVpnMode()) {
|
if (SettingsManager.isVpnMode()) {
|
||||||
if (SettingsManager.isUsingHevTun()) {
|
if (SettingsManager.isUsingHevTun()) {
|
||||||
//hev-socks5-tunnel dns routing
|
//hev-socks5-tunnel dns routing
|
||||||
v2rayConfig.routing.rules.add(
|
v2rayConfig.routing.rules.add(
|
||||||
@@ -572,7 +655,7 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to configure custom local DNS", e)
|
LogUtil.e(AppConfig.TAG, "Failed to configure custom local DNS", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -653,7 +736,7 @@ object V2rayConfigManager {
|
|||||||
if (userHostsMap != null) hosts.putAll(userHostsMap)
|
if (userHostsMap != null) hosts.putAll(userHostsMap)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
|
LogUtil.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS dns
|
// DNS dns
|
||||||
@@ -679,7 +762,7 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to configure DNS", e)
|
LogUtil.e(AppConfig.TAG, "Failed to configure DNS", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -710,8 +793,6 @@ object V2rayConfigManager {
|
|||||||
} else {
|
} else {
|
||||||
v2rayConfig.outbounds.add(outbound)
|
v2rayConfig.outbounds.add(outbound)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOutboundFragment(v2rayConfig)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,7 +845,7 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to configure more outbounds", e)
|
LogUtil.e(AppConfig.TAG, "Failed to configure more outbounds", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,7 +896,7 @@ object V2rayConfigManager {
|
|||||||
} else {
|
} else {
|
||||||
outbound.settings?.address as List<*>
|
outbound.settings?.address as List<*>
|
||||||
}
|
}
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) != true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) != true) {
|
||||||
localTunAddr = listOf(localTunAddr.first())
|
localTunAddr = listOf(localTunAddr.first())
|
||||||
}
|
}
|
||||||
outbound.settings?.address = localTunAddr
|
outbound.settings?.address = localTunAddr
|
||||||
@@ -845,7 +926,7 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
|
LogUtil.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -860,13 +941,13 @@ object V2rayConfigManager {
|
|||||||
private fun getBalance(v2rayConfig: V2rayConfig, config: ProfileItem) {
|
private fun getBalance(v2rayConfig: V2rayConfig, config: ProfileItem) {
|
||||||
try {
|
try {
|
||||||
v2rayConfig.routing.rules.forEach { rule ->
|
v2rayConfig.routing.rules.forEach { rule ->
|
||||||
if (rule.outboundTag == "proxy") {
|
if (rule.outboundTag == AppConfig.TAG_PROXY) {
|
||||||
rule.outboundTag = null
|
rule.outboundTag = null
|
||||||
rule.balancerTag = AppConfig.TAG_BALANCER
|
rule.balancerTag = AppConfig.TAG_BALANCER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val lstSelector = listOf("proxy-")
|
val lstSelector = listOf("proxy-")
|
||||||
when (config.policyGroupType) {
|
when (config.policyGroupType) {
|
||||||
// Least Ping goto else
|
// Least Ping goto else
|
||||||
"1" -> {
|
"1" -> {
|
||||||
@@ -889,6 +970,7 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"2" -> {
|
"2" -> {
|
||||||
// Random
|
// Random
|
||||||
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
||||||
@@ -900,6 +982,7 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
v2rayConfig.routing.balancers = listOf(balancer)
|
v2rayConfig.routing.balancers = listOf(balancer)
|
||||||
}
|
}
|
||||||
|
|
||||||
"3" -> {
|
"3" -> {
|
||||||
// Round Robin
|
// Round Robin
|
||||||
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
||||||
@@ -911,6 +994,7 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
v2rayConfig.routing.balancers = listOf(balancer)
|
v2rayConfig.routing.balancers = listOf(balancer)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// Default: Least Ping
|
// Default: Least Ping
|
||||||
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
||||||
@@ -946,7 +1030,7 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to configure balance", e)
|
LogUtil.e(AppConfig.TAG, "Failed to configure balance", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,70 +1039,82 @@ object V2rayConfigManager {
|
|||||||
*
|
*
|
||||||
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
|
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
|
||||||
*
|
*
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
* @param streamSettings The streamSettings object to be modified
|
||||||
* @return true if fragment configuration was successful, false otherwise
|
* @return true if fragment configuration was successful, false otherwise
|
||||||
*/
|
*/
|
||||||
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
|
private fun updateOutboundFragment(streamSettings: StreamSettingsBean): Boolean {
|
||||||
try {
|
try {
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS
|
if (streamSettings.security != AppConfig.TLS
|
||||||
&& v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY
|
&& streamSettings.security != AppConfig.REALITY
|
||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if (streamSettings.sockopt?.dialerProxy.isNotNullEmpty()) {
|
||||||
val fragmentOutbound =
|
return true
|
||||||
OutboundBean(
|
}
|
||||||
protocol = AppConfig.PROTOCOL_FREEDOM,
|
|
||||||
tag = AppConfig.TAG_FRAGMENT,
|
|
||||||
mux = null
|
|
||||||
)
|
|
||||||
|
|
||||||
var packets =
|
var packets =
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
||||||
if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY
|
if (streamSettings.security == AppConfig.REALITY
|
||||||
&& packets == "tlshello"
|
&& packets == "tlshello"
|
||||||
) {
|
) {
|
||||||
packets = "1-3"
|
packets = "1-3"
|
||||||
} else if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.TLS
|
} else if (streamSettings.security == AppConfig.TLS
|
||||||
&& packets != "tlshello"
|
&& packets != "tlshello"
|
||||||
) {
|
) {
|
||||||
packets = "tlshello"
|
packets = "tlshello"
|
||||||
}
|
}
|
||||||
|
|
||||||
fragmentOutbound.settings = OutSettingsBean(
|
val fragmentMask = StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
fragment = OutSettingsBean.FragmentBean(
|
type = "fragment",
|
||||||
|
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
packets = packets,
|
packets = packets,
|
||||||
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
||||||
?: "50-100",
|
?: "50-100",
|
||||||
interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
delay = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||||
?: "10-20"
|
?: "10-20"
|
||||||
),
|
)
|
||||||
noises = listOf(
|
)
|
||||||
OutSettingsBean.NoiseBean(
|
val noiseMask = StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
type = "rand",
|
type = "noise",
|
||||||
packet = "10-20",
|
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
delay = "10-16",
|
noise = listOf(
|
||||||
|
StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean.NoiseMaskBean(
|
||||||
|
rand = "10-20",
|
||||||
|
delay = "10-16",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
fragmentOutbound.streamSettings = StreamSettingsBean(
|
|
||||||
sockopt = StreamSettingsBean.SockoptBean(
|
|
||||||
TcpNoDelay = true,
|
|
||||||
mark = 255
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
v2rayConfig.outbounds.add(fragmentOutbound)
|
|
||||||
|
|
||||||
//proxy chain
|
val finalMaskObj = streamSettings.finalmask?.let { existingFinalMask ->
|
||||||
v2rayConfig.outbounds[0].streamSettings?.sockopt =
|
JsonUtil.parseString(JsonUtil.toJson(existingFinalMask))
|
||||||
StreamSettingsBean.SockoptBean(
|
} ?: com.google.gson.JsonObject()
|
||||||
dialerProxy = AppConfig.TAG_FRAGMENT
|
|
||||||
)
|
// finalmask.tcp / finalmask.udp are arrays; prepend mask at index 0.
|
||||||
|
fun prependMask(scope: String, mask: StreamSettingsBean.FinalMaskBean.MaskBean) {
|
||||||
|
val current = finalMaskObj.get(scope)
|
||||||
|
if (current != null && current.isJsonArray && current.asJsonArray.size() > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newArray = JsonArray()
|
||||||
|
newArray.add(JsonUtil.parseString(JsonUtil.toJson(mask)))
|
||||||
|
|
||||||
|
if (current != null && current.isJsonArray) {
|
||||||
|
current.asJsonArray.forEach { newArray.add(it) }
|
||||||
|
}
|
||||||
|
finalMaskObj.add(scope, newArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
prependMask("tcp", fragmentMask)
|
||||||
|
prependMask("udp", noiseMask)
|
||||||
|
streamSettings.finalmask = finalMaskObj
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
LogUtil.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -1174,7 +1270,7 @@ object V2rayConfigManager {
|
|||||||
val authority = profileItem.authority
|
val authority = profileItem.authority
|
||||||
val xhttpMode = profileItem.xhttpMode
|
val xhttpMode = profileItem.xhttpMode
|
||||||
val xhttpExtra = profileItem.xhttpExtra
|
val xhttpExtra = profileItem.xhttpExtra
|
||||||
|
val finalMask = profileItem.finalMask
|
||||||
var sni: String? = null
|
var sni: String? = null
|
||||||
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
|
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
|
||||||
when (streamSettings.network) {
|
when (streamSettings.network) {
|
||||||
@@ -1204,28 +1300,34 @@ object V2rayConfigManager {
|
|||||||
headerType == "wechat-video" -> "header-wechat"
|
headerType == "wechat-video" -> "header-wechat"
|
||||||
else -> "header-$headerType"
|
else -> "header-$headerType"
|
||||||
}
|
}
|
||||||
udpMaskList.add(StreamSettingsBean.FinalMaskBean.MaskBean(
|
udpMaskList.add(
|
||||||
type = kcpHeaderType,
|
StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
type = kcpHeaderType,
|
||||||
StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
||||||
domain = host
|
StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
)
|
domain = host
|
||||||
} else {
|
)
|
||||||
null
|
} else {
|
||||||
}
|
null
|
||||||
))
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (seed.isNullOrEmpty()) {
|
if (seed.isNullOrEmpty()) {
|
||||||
udpMaskList.add(StreamSettingsBean.FinalMaskBean.MaskBean(
|
udpMaskList.add(
|
||||||
type = "mkcp-original"
|
StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
))
|
type = "mkcp-original"
|
||||||
} else {
|
|
||||||
udpMaskList.add(StreamSettingsBean.FinalMaskBean.MaskBean(
|
|
||||||
type = "mkcp-aes128gcm",
|
|
||||||
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
|
||||||
password = seed
|
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
|
} else {
|
||||||
|
udpMaskList.add(
|
||||||
|
StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
|
type = "mkcp-aes128gcm",
|
||||||
|
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
|
password = seed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
streamSettings.finalmask = StreamSettingsBean.FinalMaskBean(
|
streamSettings.finalmask = StreamSettingsBean.FinalMaskBean(
|
||||||
udp = udpMaskList.toList()
|
udp = udpMaskList.toList()
|
||||||
@@ -1290,21 +1392,69 @@ object V2rayConfigManager {
|
|||||||
val hysteriaSetting = StreamSettingsBean.HysteriaSettingsBean(
|
val hysteriaSetting = StreamSettingsBean.HysteriaSettingsBean(
|
||||||
version = 2,
|
version = 2,
|
||||||
auth = profileItem.password.orEmpty(),
|
auth = profileItem.password.orEmpty(),
|
||||||
up = profileItem.bandwidthUp?.ifEmpty { "0" }.orEmpty(),
|
|
||||||
down = profileItem.bandwidthDown?.ifEmpty { "0" }.orEmpty(),
|
|
||||||
udphop = null
|
|
||||||
)
|
)
|
||||||
|
val quicParams = StreamSettingsBean.FinalMaskBean.QuicParamsBean(
|
||||||
|
brutalUp = profileItem.bandwidthUp?.nullIfBlank(),
|
||||||
|
brutalDown = profileItem.bandwidthDown?.nullIfBlank(),
|
||||||
|
)
|
||||||
|
quicParams.congestion = if (quicParams.brutalUp != null || quicParams.brutalDown != null) "brutal" else null
|
||||||
if (profileItem.portHopping.isNotNullEmpty()) {
|
if (profileItem.portHopping.isNotNullEmpty()) {
|
||||||
hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean(
|
val rawInterval = profileItem.portHoppingInterval?.trim().nullIfBlank()
|
||||||
port = profileItem.portHopping,
|
val interval = if (rawInterval == null) {
|
||||||
interval = profileItem.portHoppingInterval
|
"30"
|
||||||
?.trim()
|
} else {
|
||||||
?.toIntOrNull()
|
val singleValue = rawInterval.toIntOrNull()
|
||||||
?.takeIf { it >= 5 }
|
if (singleValue != null) {
|
||||||
?: 30
|
if (singleValue < 5) {
|
||||||
|
"30"
|
||||||
|
} else {
|
||||||
|
rawInterval
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val parts = rawInterval.split('-')
|
||||||
|
if (parts.size == 2) {
|
||||||
|
val start = parts[0].trim().toIntOrNull()
|
||||||
|
val end = parts[1].trim().toIntOrNull()
|
||||||
|
if (start != null && end != null) {
|
||||||
|
val minStart = maxOf(5, start)
|
||||||
|
val minEnd = maxOf(minStart, end)
|
||||||
|
"$minStart-$minEnd"
|
||||||
|
} else {
|
||||||
|
"30"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"30"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quicParams.udpHop = StreamSettingsBean.FinalMaskBean.QuicParamsBean.UdpHopBean(
|
||||||
|
ports = profileItem.portHopping,
|
||||||
|
interval = interval
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val finalmask = StreamSettingsBean.FinalMaskBean(
|
||||||
|
quicParams = quicParams
|
||||||
|
)
|
||||||
|
if (profileItem.obfsPassword.isNotNullEmpty()) {
|
||||||
|
finalmask.udp = listOf(
|
||||||
|
StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
|
type = "salamander",
|
||||||
|
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
|
password = profileItem.obfsPassword.orEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
streamSettings.hysteriaSettings = hysteriaSetting
|
streamSettings.hysteriaSettings = hysteriaSetting
|
||||||
|
streamSettings.finalmask = finalmask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalMask?.let {
|
||||||
|
val parsedFinalMask = JsonUtil.parseString(finalMask)
|
||||||
|
if (parsedFinalMask != null) {
|
||||||
|
streamSettings.finalmask = parsedFinalMask
|
||||||
|
} else {
|
||||||
|
LogUtil.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sni
|
return sni
|
||||||
@@ -1338,7 +1488,7 @@ object V2rayConfigManager {
|
|||||||
allowInsecure = allowInsecure,
|
allowInsecure = allowInsecure,
|
||||||
serverName = sni.nullIfBlank(),
|
serverName = sni.nullIfBlank(),
|
||||||
fingerprint = profileItem.fingerPrint.nullIfBlank(),
|
fingerprint = profileItem.fingerPrint.nullIfBlank(),
|
||||||
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
|
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
|
||||||
echConfigList = profileItem.echConfigList.nullIfBlank(),
|
echConfigList = profileItem.echConfigList.nullIfBlank(),
|
||||||
echForceQuery = profileItem.echForceQuery.nullIfBlank(),
|
echForceQuery = profileItem.echForceQuery.nullIfBlank(),
|
||||||
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
|
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
|
||||||
@@ -1354,6 +1504,10 @@ object V2rayConfigManager {
|
|||||||
streamSettings.tlsSettings = null
|
streamSettings.tlsSettings = null
|
||||||
streamSettings.realitySettings = tlsSetting
|
streamSettings.realitySettings = tlsSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profileItem.finalMask.isNullOrEmpty()) {
|
||||||
|
updateOutboundFragment(streamSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.v2ray.ang.handler
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.WebDavConfig
|
import com.v2ray.ang.dto.WebDavConfig
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Credentials
|
import okhttp3.Credentials
|
||||||
@@ -67,14 +67,14 @@ object WebDavManager {
|
|||||||
cl.newCall(req).execute().use { resp ->
|
cl.newCall(req).execute().use { resp ->
|
||||||
val success = resp.isSuccessful
|
val success = resp.isSuccessful
|
||||||
if (success) {
|
if (success) {
|
||||||
Log.i(AppConfig.TAG, "WebDAV upload success: $remote")
|
LogUtil.i(AppConfig.TAG, "WebDAV upload success: $remote")
|
||||||
} else {
|
} else {
|
||||||
Log.e(AppConfig.TAG, "WebDAV upload failed: $remote (HTTP ${resp.code})")
|
LogUtil.e(AppConfig.TAG, "WebDAV upload failed: $remote (HTTP ${resp.code})")
|
||||||
}
|
}
|
||||||
return@withContext success
|
return@withContext success
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV upload exception: $remote", e)
|
LogUtil.e(AppConfig.TAG, "WebDAV upload exception: $remote", e)
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ object WebDavManager {
|
|||||||
val req = applyAuth(Request.Builder().url(remote).get()).build()
|
val req = applyAuth(Request.Builder().url(remote).get()).build()
|
||||||
cl.newCall(req).execute().use { resp ->
|
cl.newCall(req).execute().use { resp ->
|
||||||
if (!resp.isSuccessful) {
|
if (!resp.isSuccessful) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV download failed: $remote (HTTP ${resp.code})")
|
LogUtil.e(AppConfig.TAG, "WebDAV download failed: $remote (HTTP ${resp.code})")
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,11 +104,11 @@ object WebDavManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(AppConfig.TAG, "WebDAV download success: $remote")
|
LogUtil.i(AppConfig.TAG, "WebDAV download success: $remote")
|
||||||
return@withContext true
|
return@withContext true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV download exception: $remote", e)
|
LogUtil.e(AppConfig.TAG, "WebDAV download exception: $remote", e)
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ object WebDavManager {
|
|||||||
cl.newCall(req).execute().use { resp ->
|
cl.newCall(req).execute().use { resp ->
|
||||||
// 201 Created or 405 Method Not Allowed (already exists) are acceptable
|
// 201 Created or 405 Method Not Allowed (already exists) are acceptable
|
||||||
if (resp.code != 201 && resp.code != 405 && resp.code != 409) {
|
if (resp.code != 201 && resp.code != 405 && resp.code != 409) {
|
||||||
Log.w(AppConfig.TAG, "WebDAV MKCOL $mkUrl returned ${resp.code}")
|
LogUtil.w(AppConfig.TAG, "WebDAV MKCOL $mkUrl returned ${resp.code}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
@@ -177,7 +177,7 @@ object WebDavManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV ensureRemoteDirs error", e)
|
LogUtil.e(AppConfig.TAG, "WebDAV ensureRemoteDirs error", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,13 +3,13 @@ package com.v2ray.ang.helper
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for choosing and creating files using Android Storage Access Framework.
|
* Helper for choosing and creating files using Android Storage Access Framework.
|
||||||
@@ -58,7 +58,7 @@ class FileChooserHelper(private val activity: AppCompatActivity) {
|
|||||||
Intent.createChooser(intent, activity.getString(R.string.title_file_chooser))
|
Intent.createChooser(intent, activity.getString(R.string.title_file_chooser))
|
||||||
)
|
)
|
||||||
} catch (ex: ActivityNotFoundException) {
|
} catch (ex: ActivityNotFoundException) {
|
||||||
Log.e(AppConfig.TAG, "File chooser activity not found", ex)
|
LogUtil.e(AppConfig.TAG, "File chooser activity not found", ex)
|
||||||
activity.toast(R.string.toast_require_file_manager)
|
activity.toast(R.string.toast_require_file_manager)
|
||||||
fileChooserCallback?.invoke(null)
|
fileChooserCallback?.invoke(null)
|
||||||
fileChooserCallback = null
|
fileChooserCallback = null
|
||||||
@@ -79,7 +79,7 @@ class FileChooserHelper(private val activity: AppCompatActivity) {
|
|||||||
try {
|
try {
|
||||||
documentCreateLauncher.launch(fileName)
|
documentCreateLauncher.launch(fileName)
|
||||||
} catch (ex: ActivityNotFoundException) {
|
} catch (ex: ActivityNotFoundException) {
|
||||||
Log.e(AppConfig.TAG, "Document creator activity not found", ex)
|
LogUtil.e(AppConfig.TAG, "Document creator activity not found", ex)
|
||||||
activity.toast(R.string.toast_require_file_manager)
|
activity.toast(R.string.toast_require_file_manager)
|
||||||
documentCreateCallback?.invoke(null)
|
documentCreateCallback?.invoke(null)
|
||||||
documentCreateCallback = null
|
documentCreateCallback = null
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.v2ray.ang.AppConfig
|
|||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsChangeManager
|
import com.v2ray.ang.handler.SettingsChangeManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PreferenceDataStore implementation that bridges AndroidX Preference framework to MMKV storage.
|
* PreferenceDataStore implementation that bridges AndroidX Preference framework to MMKV storage.
|
||||||
@@ -73,6 +74,10 @@ class MmkvPreferenceDataStore : PreferenceDataStore() {
|
|||||||
|
|
||||||
// Internal helper: notify other modules about setting changes
|
// Internal helper: notify other modules about setting changes
|
||||||
private fun notifySettingChanged(key: String) {
|
private fun notifySettingChanged(key: String) {
|
||||||
|
if (key == AppConfig.PREF_LOGLEVEL) {
|
||||||
|
LogUtil.refreshLogLevel()
|
||||||
|
}
|
||||||
|
|
||||||
// Call SettingsManager.setNightMode if UI mode changed
|
// Call SettingsManager.setNightMode if UI mode changed
|
||||||
if (key == AppConfig.PREF_UI_MODE_NIGHT) {
|
if (key == AppConfig.PREF_UI_MODE_NIGHT) {
|
||||||
SettingsManager.setNightMode()
|
SettingsManager.setNightMode()
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package com.v2ray.ang.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.V2RayServiceManager
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
/**
|
/**
|
||||||
@@ -18,24 +18,24 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
* @param intent The Intent being received.
|
* @param intent The Intent being received.
|
||||||
*/
|
*/
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
Log.i(AppConfig.TAG, "BootReceiver received: ${intent?.action}")
|
LogUtil.i(AppConfig.TAG, "BootReceiver received: ${intent?.action}")
|
||||||
|
|
||||||
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) {
|
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) {
|
||||||
Log.w(AppConfig.TAG, "BootReceiver: Invalid context or action")
|
LogUtil.w(AppConfig.TAG, "BootReceiver: Invalid context or action")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MmkvManager.decodeStartOnBoot()) {
|
if (!MmkvManager.decodeStartOnBoot()) {
|
||||||
Log.i(AppConfig.TAG, "BootReceiver: Auto-start on boot is disabled")
|
LogUtil.i(AppConfig.TAG, "BootReceiver: Auto-start on boot is disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
Log.w(AppConfig.TAG, "BootReceiver: No server selected")
|
LogUtil.w(AppConfig.TAG, "BootReceiver: No server selected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service")
|
LogUtil.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service")
|
||||||
V2RayServiceManager.startVService(context)
|
V2RayServiceManager.startVService(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.handler.V2RayServiceManager
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
|
||||||
class TaskerReceiver : BroadcastReceiver() {
|
class TaskerReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ class TaskerReceiver : BroadcastReceiver() {
|
|||||||
V2RayServiceManager.stopVService(context)
|
V2RayServiceManager.stopVService(context)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
|
LogUtil.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.v2ray.ang.service
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -16,7 +16,7 @@ class ProcessService {
|
|||||||
* @param cmd The command to run.
|
* @param cmd The command to run.
|
||||||
*/
|
*/
|
||||||
fun runProcess(context: Context, cmd: MutableList<String>) {
|
fun runProcess(context: Context, cmd: MutableList<String>) {
|
||||||
Log.i(AppConfig.TAG, cmd.toString())
|
LogUtil.i(AppConfig.TAG, cmd.toString())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val proBuilder = ProcessBuilder(cmd)
|
val proBuilder = ProcessBuilder(cmd)
|
||||||
@@ -27,14 +27,14 @@ class ProcessService {
|
|||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
Thread.sleep(50L)
|
Thread.sleep(50L)
|
||||||
Log.i(AppConfig.TAG, "runProcess check")
|
LogUtil.i(AppConfig.TAG, "runProcess check")
|
||||||
process?.waitFor()
|
process?.waitFor()
|
||||||
Log.i(AppConfig.TAG, "runProcess exited")
|
LogUtil.i(AppConfig.TAG, "runProcess exited")
|
||||||
}
|
}
|
||||||
Log.i(AppConfig.TAG, process.toString())
|
LogUtil.i(AppConfig.TAG, process.toString())
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, e.toString(), e)
|
LogUtil.e(AppConfig.TAG, e.toString(), e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +43,10 @@ class ProcessService {
|
|||||||
*/
|
*/
|
||||||
fun stopProcess() {
|
fun stopProcess() {
|
||||||
try {
|
try {
|
||||||
Log.i(AppConfig.TAG, "runProcess destroy")
|
LogUtil.i(AppConfig.TAG, "runProcess destroy")
|
||||||
process?.destroy()
|
process?.destroy()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to destroy process", e)
|
LogUtil.e(AppConfig.TAG, "Failed to destroy process", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import android.content.IntentFilter
|
|||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.handler.V2RayServiceManager
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
@@ -63,7 +63,7 @@ class QSTileService : TileService() {
|
|||||||
applicationContext.unregisterReceiver(mMsgReceive)
|
applicationContext.unregisterReceiver(mMsgReceive)
|
||||||
mMsgReceive = null
|
mMsgReceive = null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to unregister receiver", e)
|
LogUtil.e(AppConfig.TAG, "Failed to unregister receiver", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package com.v2ray.ang.service
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.contracts.Tun2SocksControl
|
import com.v2ray.ang.contracts.Tun2SocksControl
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,9 +22,11 @@ class TProxyService(
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Suppress("FunctionName")
|
@Suppress("FunctionName")
|
||||||
private external fun TProxyStartService(configPath: String, fd: Int)
|
private external fun TProxyStartService(configPath: String, fd: Int)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Suppress("FunctionName")
|
@Suppress("FunctionName")
|
||||||
private external fun TProxyStopService()
|
private external fun TProxyStopService()
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Suppress("FunctionName")
|
@Suppress("FunctionName")
|
||||||
private external fun TProxyGetStats(): LongArray?
|
private external fun TProxyGetStats(): LongArray?
|
||||||
@@ -38,32 +40,36 @@ class TProxyService(
|
|||||||
* Starts the tun2socks process with the appropriate parameters.
|
* Starts the tun2socks process with the appropriate parameters.
|
||||||
*/
|
*/
|
||||||
override fun startTun2Socks() {
|
override fun startTun2Socks() {
|
||||||
// Log.i(AppConfig.TAG, "Starting HevSocks5Tunnel via JNI")
|
// LogUtil.i(AppConfig.TAG, "Starting HevSocks5Tunnel via JNI")
|
||||||
|
|
||||||
val configContent = buildConfig()
|
val configContent = buildConfig()
|
||||||
val configFile = File(context.filesDir, "hev-socks5-tunnel.yaml").apply {
|
val configFile = File(context.filesDir, "hev-socks5-tunnel.yaml").apply {
|
||||||
writeText(configContent)
|
writeText(configContent)
|
||||||
}
|
}
|
||||||
// Log.i(AppConfig.TAG, "Config file created: ${configFile.absolutePath}")
|
// LogUtil.i(AppConfig.TAG, "Config file created: ${configFile.absolutePath}")
|
||||||
Log.d(AppConfig.TAG, "HevSocks5Tunnel Config content:\n$configContent")
|
LogUtil.d(AppConfig.TAG, "HevSocks5Tunnel Config content:\n$configContent")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Log.i(AppConfig.TAG, "TProxyStartService...")
|
// LogUtil.i(AppConfig.TAG, "TProxyStartService...")
|
||||||
TProxyStartService(configFile.absolutePath, vpnInterface.fd)
|
TProxyStartService(configFile.absolutePath, vpnInterface.fd)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "HevSocks5Tunnel exception: ${e.message}")
|
LogUtil.e(AppConfig.TAG, "HevSocks5Tunnel exception: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildConfig(): String {
|
private fun buildConfig(): String {
|
||||||
val socksPort = SettingsManager.getSocksPort()
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
|
val socksUsername = SettingsManager.getSocksUsername()
|
||||||
|
val socksPassword = SettingsManager.getSocksPassword()
|
||||||
val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
|
val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
|
||||||
|
val escapedSocksUsername = socksUsername?.replace("'", "''")
|
||||||
|
val escapedSocksPassword = socksPassword?.replace("'", "''")
|
||||||
return buildString {
|
return buildString {
|
||||||
appendLine("tunnel:")
|
appendLine("tunnel:")
|
||||||
appendLine(" mtu: ${SettingsManager.getVpnMtu()}")
|
appendLine(" mtu: ${SettingsManager.getVpnMtu()}")
|
||||||
appendLine(" ipv4: ${vpnConfig.ipv4Client}")
|
appendLine(" ipv4: ${vpnConfig.ipv4Client}")
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED)) {
|
||||||
appendLine(" ipv6: '${vpnConfig.ipv6Client}'")
|
appendLine(" ipv6: '${vpnConfig.ipv6Client}'")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +77,10 @@ class TProxyService(
|
|||||||
appendLine(" port: ${socksPort}")
|
appendLine(" port: ${socksPort}")
|
||||||
appendLine(" address: ${AppConfig.LOOPBACK}")
|
appendLine(" address: ${AppConfig.LOOPBACK}")
|
||||||
appendLine(" udp: 'udp'")
|
appendLine(" udp: 'udp'")
|
||||||
|
if (escapedSocksUsername != null && escapedSocksPassword != null) {
|
||||||
|
appendLine(" username: '${escapedSocksUsername}'")
|
||||||
|
appendLine(" password: '${escapedSocksPassword}'")
|
||||||
|
}
|
||||||
|
|
||||||
// Read-write timeout settings
|
// Read-write timeout settings
|
||||||
val timeoutSetting = MmkvManager.decodeSettingsString(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) ?: AppConfig.HEVTUN_RW_TIMEOUT
|
val timeoutSetting = MmkvManager.decodeSettingsString(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) ?: AppConfig.HEVTUN_RW_TIMEOUT
|
||||||
@@ -92,10 +102,10 @@ class TProxyService(
|
|||||||
*/
|
*/
|
||||||
override fun stopTun2Socks() {
|
override fun stopTun2Socks() {
|
||||||
try {
|
try {
|
||||||
Log.i(AppConfig.TAG, "TProxyStopService...")
|
LogUtil.i(AppConfig.TAG, "TProxyStopService...")
|
||||||
TProxyStopService()
|
TProxyStopService()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to stop hev-socks5-tunnel", e)
|
LogUtil.e(AppConfig.TAG, "Failed to stop hev-socks5-tunnel", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.contracts.ServiceControl
|
import com.v2ray.ang.contracts.ServiceControl
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.handler.V2RayServiceManager
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.MyContextWrapper
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Log.i(AppConfig.TAG, "StartCore-Proxy: Service created")
|
LogUtil.i(AppConfig.TAG, "StartCore-Proxy: Service created")
|
||||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
|||||||
* @return The start mode.
|
* @return The start mode.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Proxy: Service command received")
|
LogUtil.i(AppConfig.TAG, "StartCore-Proxy: Service command received")
|
||||||
V2RayServiceManager.startCoreLoop(null)
|
V2RayServiceManager.startCoreLoop(null)
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import android.net.VpnService
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
@@ -25,6 +24,7 @@ import com.v2ray.ang.handler.MmkvManager
|
|||||||
import com.v2ray.ang.handler.NotificationManager
|
import com.v2ray.ang.handler.NotificationManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.handler.V2RayServiceManager
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.MyContextWrapper
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
@@ -74,14 +74,14 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Log.i(AppConfig.TAG, "StartCore-VPN: Service created")
|
LogUtil.i(AppConfig.TAG, "StartCore-VPN: Service created")
|
||||||
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
||||||
StrictMode.setThreadPolicy(policy)
|
StrictMode.setThreadPolicy(policy)
|
||||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRevoke() {
|
override fun onRevoke() {
|
||||||
Log.w(AppConfig.TAG, "StartCore-VPN: Permission revoked")
|
LogUtil.w(AppConfig.TAG, "StartCore-VPN: Permission revoked")
|
||||||
stopAllService()
|
stopAllService()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,12 +92,27 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
|
LogUtil.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
|
||||||
|
|
||||||
|
// Ensure VPN interface is properly closed when the service is destroyed without
|
||||||
|
// going through stopAllService() (e.g. when killed unexpectedly). isRunning is
|
||||||
|
// set to false at the start of stopAllService(), so this guard prevents a double-close.
|
||||||
|
if (isRunning) {
|
||||||
|
try {
|
||||||
|
if (::mInterface.isInitialized) {
|
||||||
|
mInterface.close()
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-VPN: VPN interface closed in onDestroy")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to close interface in onDestroy", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NotificationManager.cancelNotification()
|
NotificationManager.cancelNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.i(AppConfig.TAG, "StartCore-VPN: Service command received")
|
LogUtil.i(AppConfig.TAG, "StartCore-VPN: Service command received")
|
||||||
setupVpnService()
|
setupVpnService()
|
||||||
startService()
|
startService()
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
@@ -110,11 +125,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
|
|
||||||
override fun startService() {
|
override fun startService() {
|
||||||
if (!::mInterface.isInitialized) {
|
if (!::mInterface.isInitialized) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-VPN: Interface not initialized")
|
LogUtil.e(AppConfig.TAG, "StartCore-VPN: Interface not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!V2RayServiceManager.startCoreLoop(mInterface)) {
|
if (!V2RayServiceManager.startCoreLoop(mInterface)) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to start core loop")
|
LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to start core loop")
|
||||||
stopAllService()
|
stopAllService()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -142,13 +157,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
private fun setupVpnService() {
|
private fun setupVpnService() {
|
||||||
val prepare = prepare(this)
|
val prepare = prepare(this)
|
||||||
if (prepare != null) {
|
if (prepare != null) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-VPN: Permission not granted")
|
LogUtil.e(AppConfig.TAG, "StartCore-VPN: Permission not granted")
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configureVpnService() != true) {
|
if (configureVpnService() != true) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-VPN: Configuration failed")
|
LogUtil.e(AppConfig.TAG, "StartCore-VPN: Configuration failed")
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -175,7 +190,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
mInterface.close()
|
mInterface.close()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(AppConfig.TAG, "Failed to close old interface", e)
|
LogUtil.w(AppConfig.TAG, "Failed to close old interface", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure platform-specific features
|
// Configure platform-specific features
|
||||||
@@ -187,7 +202,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
isRunning = true
|
isRunning = true
|
||||||
return true
|
return true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to establish VPN interface", e)
|
LogUtil.e(AppConfig.TAG, "Failed to establish VPN interface", e)
|
||||||
stopAllService()
|
stopAllService()
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -218,7 +233,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configure IPv6 if enabled
|
// Configure IPv6 if enabled
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) == true) {
|
||||||
builder.addAddress(vpnConfig.ipv6Client, 126)
|
builder.addAddress(vpnConfig.ipv6Client, 126)
|
||||||
if (bypassLan) {
|
if (bypassLan) {
|
||||||
builder.addRoute("2000::", 3) // Currently only 1/8 of total IPv6 is in use
|
builder.addRoute("2000::", 3) // Currently only 1/8 of total IPv6 is in use
|
||||||
@@ -252,7 +267,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
try {
|
try {
|
||||||
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
|
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to request network", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to request network", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +320,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
builder.addAllowedApplication(it)
|
builder.addAllowedApplication(it)
|
||||||
}
|
}
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to configure app", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to configure app", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +354,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
try {
|
try {
|
||||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(AppConfig.TAG, "StartCore-VPN: Failed to unregister callback", e)
|
LogUtil.w(AppConfig.TAG, "StartCore-VPN: Failed to unregister callback", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,12 +371,22 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
//which means the first v2ray core somehow failed to stop and release the port.
|
//which means the first v2ray core somehow failed to stop and release the port.
|
||||||
stopSelf()
|
stopSelf()
|
||||||
|
|
||||||
|
// Add a small delay to allow the async core stop operation to complete
|
||||||
|
// before closing the VPN interface, preventing a race condition that can
|
||||||
|
// leave the VPN icon in the status bar after stopping the service.
|
||||||
|
try {
|
||||||
|
Thread.sleep(100)
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
LogUtil.w(AppConfig.TAG, "StartCore-VPN: Sleep interrupted", e)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (::mInterface.isInitialized) {
|
if (::mInterface.isInitialized) {
|
||||||
mInterface.close()
|
mInterface.close()
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-VPN: VPN interface closed")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to close interface", e)
|
LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to close interface", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
|
|||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
@@ -20,6 +19,7 @@ import com.v2ray.ang.handler.MmkvManager
|
|||||||
import com.v2ray.ang.handler.SettingsChangeManager
|
import com.v2ray.ang.handler.SettingsChangeManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.handler.WebDavManager
|
import com.v2ray.ang.handler.WebDavManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.ZipUtil
|
import com.v2ray.ang.util.ZipUtil
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -38,7 +38,7 @@ class BackupActivity : HelperBaseActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
//setContentView(binding.root)
|
//setContentView(binding.root)
|
||||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_configuration_backup_restore))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_configuration_backup_restore))
|
||||||
|
|
||||||
binding.layoutBackup.setOnClickListener {
|
binding.layoutBackup.setOnClickListener {
|
||||||
@@ -149,7 +149,7 @@ class BackupActivity : HelperBaseActivity() {
|
|||||||
toastError(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error during file restore", e)
|
LogUtil.e(AppConfig.TAG, "Error during file restore", e)
|
||||||
toastError(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ class BackupActivity : HelperBaseActivity() {
|
|||||||
toastError(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to backup configuration", e)
|
LogUtil.e(AppConfig.TAG, "Failed to backup configuration", e)
|
||||||
toastError(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,7 +217,7 @@ class BackupActivity : HelperBaseActivity() {
|
|||||||
val ok = try {
|
val ok = try {
|
||||||
WebDavManager.uploadFile(tempFile, WEBDAV_BACKUP_FILE_NAME)
|
WebDavManager.uploadFile(tempFile, WEBDAV_BACKUP_FILE_NAME)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV upload error", e)
|
LogUtil.e(AppConfig.TAG, "WebDAV upload error", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ class BackupActivity : HelperBaseActivity() {
|
|||||||
if (ok) toastSuccess(R.string.toast_success) else toastError(R.string.toast_failure)
|
if (ok) toastSuccess(R.string.toast_success) else toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV backup error", e)
|
LogUtil.e(AppConfig.TAG, "WebDAV backup error", e)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
toastError(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ class BackupActivity : HelperBaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV download error", e)
|
LogUtil.e(AppConfig.TAG, "WebDAV download error", e)
|
||||||
withContext(Dispatchers.Main) { toastError(R.string.toast_failure) }
|
withContext(Dispatchers.Main) { toastError(R.string.toast_failure) }
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ abstract class BaseFragment<VB : ViewBinding> : Fragment() {
|
|||||||
* @param drawableResId The resource ID of the drawable to be used as the divider.
|
* @param drawableResId The resource ID of the drawable to be used as the divider.
|
||||||
* @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL).
|
* @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL).
|
||||||
*/
|
*/
|
||||||
fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) {
|
fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) {
|
||||||
// Get the drawable from resources
|
// Get the drawable from resources
|
||||||
val drawable = ContextCompat.getDrawable(requireContext(), drawableResId)
|
val drawable = ContextCompat.getDrawable(requireContext(), drawableResId)
|
||||||
requireNotNull(drawable) { "Drawable resource not found" }
|
requireNotNull(drawable) { "Drawable resource not found" }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
@@ -15,6 +14,7 @@ import com.v2ray.ang.extension.toastSuccess
|
|||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.UpdateCheckerManager
|
import com.v2ray.ang.handler.UpdateCheckerManager
|
||||||
import com.v2ray.ang.handler.V2RayNativeManager
|
import com.v2ray.ang.handler.V2RayNativeManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -56,10 +56,9 @@ class CheckUpdateActivity : BaseActivity() {
|
|||||||
toastSuccess(R.string.update_already_latest_version)
|
toastSuccess(R.string.update_already_latest_version)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
|
LogUtil.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
|
||||||
toastError(e.message ?: getString(R.string.toast_failure))
|
toastError(e.message ?: getString(R.string.toast_failure))
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
hideLoading()
|
hideLoading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -11,6 +10,7 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.contracts.MainAdapterListener
|
import com.v2ray.ang.contracts.MainAdapterListener
|
||||||
@@ -24,11 +24,13 @@ import com.v2ray.ang.extension.toastSuccess
|
|||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
import com.v2ray.ang.viewmodel.MainViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||||
|
SwipeRefreshLayout.OnRefreshListener {
|
||||||
private val ownerActivity: MainActivity
|
private val ownerActivity: MainActivity
|
||||||
get() = requireActivity() as MainActivity
|
get() = requireActivity() as MainActivity
|
||||||
private val mainViewModel: MainViewModel by activityViewModels()
|
private val mainViewModel: MainViewModel by activityViewModels()
|
||||||
@@ -68,15 +70,20 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false))
|
itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false))
|
||||||
itemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
itemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
|
|
||||||
|
binding.refreshLayout.isEnabled = false
|
||||||
|
// binding.refreshLayout.setOnRefreshListener(this)
|
||||||
|
// // Set the distance to trigger sync to 160dp
|
||||||
|
// binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt())
|
||||||
|
|
||||||
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index ->
|
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index ->
|
||||||
if (mainViewModel.subscriptionId != subId) {
|
if (mainViewModel.subscriptionId != subId) {
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
// LogUtil.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
||||||
adapter.setData(mainViewModel.serversCache, index)
|
adapter.setData(mainViewModel.serversCache, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
// LogUtil.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -105,7 +112,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
else -> ownerActivity.toast("else")
|
else -> ownerActivity.toast("else")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error when sharing server", e)
|
LogUtil.e(AppConfig.TAG, "Error when sharing server", e)
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
@@ -142,7 +149,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
* @param guid The server unique identifier
|
* @param guid The server unique identifier
|
||||||
*/
|
*/
|
||||||
private fun shareFullContent(guid: String) {
|
private fun shareFullContent(guid: String) {
|
||||||
ownerActivity.lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val result = AngConfigManager.shareFullContent2Clipboard(ownerActivity, guid)
|
val result = AngConfigManager.shareFullContent2Clipboard(ownerActivity, guid)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
@@ -212,7 +219,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
* @param position The position in the list
|
* @param position The position in the list
|
||||||
*/
|
*/
|
||||||
private fun removeServerSub(guid: String, position: Int) {
|
private fun removeServerSub(guid: String, position: Int) {
|
||||||
ownerActivity.mainViewModel.removeServer(guid)
|
mainViewModel.removeServer(guid)
|
||||||
adapter.removeServerSub(guid, position)
|
adapter.removeServerSub(guid, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,4 +278,43 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
shareServer(guid, profile, position, shareOptions, skip)
|
shareServer(guid, profile, position, shareOptions, skip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRefresh() {
|
||||||
|
ownerActivity.importConfigViaSub()
|
||||||
|
//binding.refreshLayout.isRefreshing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the currently selected server in the RecyclerView
|
||||||
|
*/
|
||||||
|
fun scrollToSelectedServer() {
|
||||||
|
val selectedGuid = MmkvManager.getSelectServer()
|
||||||
|
if (selectedGuid.isNullOrEmpty()) {
|
||||||
|
ownerActivity.toast(R.string.title_file_chooser)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the position of the selected server
|
||||||
|
val serversCache = mainViewModel.serversCache
|
||||||
|
val position = serversCache.indexOfFirst { it.guid == selectedGuid }
|
||||||
|
val recyclerView = binding.recyclerView
|
||||||
|
|
||||||
|
if (position >= 0) {
|
||||||
|
// Get the layout manager
|
||||||
|
val layoutManager = recyclerView.layoutManager as? GridLayoutManager
|
||||||
|
|
||||||
|
if (layoutManager != null) {
|
||||||
|
// Scroll to position with offset to center it on screen
|
||||||
|
// First scroll to position, then adjust to center
|
||||||
|
recyclerView.post {
|
||||||
|
layoutManager.scrollToPositionWithOffset(position, recyclerView.height / 3)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to smooth scroll if layout manager is not GridLayoutManager
|
||||||
|
recyclerView.smoothScrollToPosition(position)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ownerActivity.toast(R.string.toast_server_not_found_in_group)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,9 @@ import com.v2ray.ang.helper.QRCodeScannerHelper
|
|||||||
* - Provide QR code scanning helpers with camera permission handling.
|
* - Provide QR code scanning helpers with camera permission handling.
|
||||||
*/
|
*/
|
||||||
abstract class HelperBaseActivity : BaseActivity() {
|
abstract class HelperBaseActivity : BaseActivity() {
|
||||||
private lateinit var fileChooser : FileChooserHelper
|
private lateinit var fileChooser: FileChooserHelper
|
||||||
private lateinit var permissionRequester : PermissionHelper
|
private lateinit var permissionRequester: PermissionHelper
|
||||||
private lateinit var qrCodeScanner : QRCodeScannerHelper
|
private lateinit var qrCodeScanner: QRCodeScannerHelper
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.viewmodel.LogcatViewModel
|
import com.v2ray.ang.viewmodel.LogcatViewModel
|
||||||
|
|
||||||
class LogcatRecyclerAdapter(
|
class LogcatRecyclerAdapter(
|
||||||
@@ -33,7 +33,7 @@ class LogcatRecyclerAdapter(
|
|||||||
onLongClick?.invoke(log) ?: false
|
onLongClick?.invoke(log) ?: false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error binding log view data", e)
|
LogUtil.e(AppConfig.TAG, "Error binding log view data", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.content.res.ColorStateList
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -33,6 +32,7 @@ import com.v2ray.ang.handler.MmkvManager
|
|||||||
import com.v2ray.ang.handler.SettingsChangeManager
|
import com.v2ray.ang.handler.SettingsChangeManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.handler.V2RayServiceManager
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
import com.v2ray.ang.viewmodel.MainViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -179,7 +179,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
binding.tvTestState.text = content
|
binding.tvTestState.text = content
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) {
|
private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
binding.fab.setImageResource(R.drawable.ic_fab_check)
|
binding.fab.setImageResource(R.drawable.ic_fab_check)
|
||||||
return
|
return
|
||||||
@@ -339,6 +339,10 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.locate_selected_config -> {
|
||||||
|
locateSelectedServer()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
@@ -381,7 +385,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
val clipboard = Utils.getClipboard(this)
|
val clipboard = Utils.getClipboard(this)
|
||||||
importBatchConfig(clipboard)
|
importBatchConfig(clipboard)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to import config from clipboard", e)
|
LogUtil.e(AppConfig.TAG, "Failed to import config from clipboard", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -411,7 +415,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
toastError(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
hideLoading()
|
hideLoading()
|
||||||
}
|
}
|
||||||
Log.e(AppConfig.TAG, "Failed to import batch config", e)
|
LogUtil.e(AppConfig.TAG, "Failed to import batch config", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,7 +427,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
try {
|
try {
|
||||||
showFileChooser()
|
showFileChooser()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to import config from local file", e)
|
LogUtil.e(AppConfig.TAG, "Failed to import config from local file", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -433,7 +437,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
/**
|
/**
|
||||||
* import config from sub
|
* import config from sub
|
||||||
*/
|
*/
|
||||||
private fun importConfigViaSub(): Boolean {
|
fun importConfigViaSub(): Boolean {
|
||||||
showLoading()
|
showLoading()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@@ -565,7 +569,48 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
importBatchConfig(input?.bufferedReader()?.readText())
|
importBatchConfig(input?.bufferedReader()?.readText())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to read content from URI", e)
|
LogUtil.e(AppConfig.TAG, "Failed to read content from URI", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locates and scrolls to the currently selected server.
|
||||||
|
* If the selected server is in a different group, automatically switches to that group first.
|
||||||
|
*/
|
||||||
|
private fun locateSelectedServer() {
|
||||||
|
val targetSubscriptionId = mainViewModel.findSubscriptionIdBySelect()
|
||||||
|
if (targetSubscriptionId.isNullOrEmpty()) {
|
||||||
|
toast(R.string.title_file_chooser)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetGroupIndex = groupPagerAdapter.groups.indexOfFirst { it.id == targetSubscriptionId }
|
||||||
|
if (targetGroupIndex < 0) {
|
||||||
|
toast(R.string.toast_server_not_found_in_group)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to target group if needed, then scroll to the server
|
||||||
|
if (binding.viewPager.currentItem != targetGroupIndex) {
|
||||||
|
binding.viewPager.setCurrentItem(targetGroupIndex, true)
|
||||||
|
binding.viewPager.postDelayed({ scrollToSelectedServer(targetGroupIndex) }, 1000)
|
||||||
|
} else {
|
||||||
|
scrollToSelectedServer(targetGroupIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the selected server in the specified fragment.
|
||||||
|
* @param groupIndex The index of the group/fragment to scroll in
|
||||||
|
*/
|
||||||
|
private fun scrollToSelectedServer(groupIndex: Int) {
|
||||||
|
val itemId = groupPagerAdapter.getItemId(groupIndex)
|
||||||
|
val fragment = supportFragmentManager.findFragmentByTag("f$itemId") as? GroupServerFragment
|
||||||
|
|
||||||
|
if (fragment?.isAdded == true && fragment.view != null) {
|
||||||
|
fragment.scrollToSelectedServer()
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_fragment_not_available)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class MainRecyclerAdapter(
|
|||||||
adapterListener?.onSelectServer(guid)
|
adapterListener?.onSelectServer(guid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -23,6 +22,7 @@ import com.v2ray.ang.handler.SettingsChangeManager
|
|||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.AppManagerUtil
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
import com.v2ray.ang.util.HttpUtil
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.PerAppProxyViewModel
|
import com.v2ray.ang.viewmodel.PerAppProxyViewModel
|
||||||
import es.dmoral.toasty.Toasty
|
import es.dmoral.toasty.Toasty
|
||||||
@@ -99,7 +99,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(ANG_PACKAGE, "Error loading apps", e)
|
LogUtil.e(ANG_PACKAGE, "Error loading apps", e)
|
||||||
} finally {
|
} finally {
|
||||||
hideLoading()
|
hideLoading()
|
||||||
}
|
}
|
||||||
@@ -133,6 +133,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
allowPerAppProxy()
|
allowPerAppProxy()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.invert_selection -> {
|
R.id.invert_selection -> {
|
||||||
invertSelection()
|
invertSelection()
|
||||||
allowPerAppProxy()
|
allowPerAppProxy()
|
||||||
@@ -194,7 +195,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
|
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
|
||||||
}
|
}
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
//Log.i(AppConfig.TAG, content)
|
//LogUtil.i(AppConfig.TAG, content)
|
||||||
selectProxyApp(content, true)
|
selectProxyApp(content, true)
|
||||||
toastSuccess(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
hideLoading()
|
hideLoading()
|
||||||
@@ -258,7 +259,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error selecting proxy app", e)
|
LogUtil.e(AppConfig.TAG, "Error selecting proxy app", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import com.v2ray.ang.viewmodel.PerAppProxyViewModel
|
|||||||
class PerAppProxyAdapter(
|
class PerAppProxyAdapter(
|
||||||
val apps: List<AppInfo>,
|
val apps: List<AppInfo>,
|
||||||
val viewModel: PerAppProxyViewModel
|
val viewModel: PerAppProxyViewModel
|
||||||
) :RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
) : RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val VIEW_TYPE_HEADER = 0
|
private const val VIEW_TYPE_HEADER = 0
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ package com.v2ray.ang.ui
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
import com.v2ray.ang.extension.nullIfBlank
|
import com.v2ray.ang.extension.nullIfBlank
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -20,21 +24,49 @@ class RoutingEditActivity : BaseActivity() {
|
|||||||
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
|
||||||
private val position by lazy { intent.getIntExtra("position", -1) }
|
private val position by lazy { intent.getIntExtra("position", -1) }
|
||||||
|
|
||||||
private val outbound_tag: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.outbound_tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
//setContentView(binding.root)
|
|
||||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_rule_title))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_rule_title))
|
||||||
|
|
||||||
|
setupOutboundTagInput()
|
||||||
|
|
||||||
val rulesetItem = SettingsManager.getRoutingRuleset(position)
|
val rulesetItem = SettingsManager.getRoutingRuleset(position)
|
||||||
if (rulesetItem != null) {
|
if (rulesetItem != null) {
|
||||||
bindingServer(rulesetItem)
|
bindingServer(rulesetItem)
|
||||||
} else {
|
} else {
|
||||||
clearServer()
|
clearServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsManager.canUseProcessRouting().let { canUse ->
|
||||||
|
binding.etProcess.isEnabled = canUse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the AutoCompleteTextView for outbound tag:
|
||||||
|
* suggestions = built-in tags (proxy/direct/block) + all existing profile remarks.
|
||||||
|
* The dropdown button triggers showing the full list without typing.
|
||||||
|
*/
|
||||||
|
private fun setupOutboundTagInput() {
|
||||||
|
val profileRemarks = MmkvManager.decodeAllServerList()
|
||||||
|
.mapNotNull { id -> MmkvManager.decodeServerConfig(id)?.remarks }
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
val suggestions = (BUILTIN_OUTBOUND_TAGS.toList() + profileRemarks).distinct()
|
||||||
|
val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions)
|
||||||
|
binding.spOutboundTag.setAdapter(adapter)
|
||||||
|
// threshold=0 means show all suggestions even before typing; still need focus+request
|
||||||
|
binding.spOutboundTag.threshold = 0
|
||||||
|
|
||||||
|
// Dropdown arrow button shows the full suggestion list
|
||||||
|
binding.btnOutboundTagDropdown.setOnClickListener {
|
||||||
|
binding.spOutboundTag.requestFocus()
|
||||||
|
binding.spOutboundTag.showDropDown()
|
||||||
|
}
|
||||||
|
// Also show on field click when it already has focus
|
||||||
|
binding.spOutboundTag.setOnClickListener {
|
||||||
|
binding.spOutboundTag.showDropDown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindingServer(rulesetItem: RulesetItem): Boolean {
|
private fun bindingServer(rulesetItem: RulesetItem): Boolean {
|
||||||
@@ -42,18 +74,18 @@ class RoutingEditActivity : BaseActivity() {
|
|||||||
binding.chkLocked.isChecked = rulesetItem.locked == true
|
binding.chkLocked.isChecked = rulesetItem.locked == true
|
||||||
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
|
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
|
||||||
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
|
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
|
||||||
|
binding.etProcess.text = Utils.getEditable(rulesetItem.process?.joinToString(","))
|
||||||
binding.etPort.text = Utils.getEditable(rulesetItem.port)
|
binding.etPort.text = Utils.getEditable(rulesetItem.port)
|
||||||
binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(","))
|
binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(","))
|
||||||
binding.etNetwork.text = Utils.getEditable(rulesetItem.network)
|
binding.etNetwork.text = Utils.getEditable(rulesetItem.network)
|
||||||
val outbound = Utils.arrayFind(outbound_tag, rulesetItem.outboundTag)
|
// Set text directly; filter won't fire because we're not using setText(filter=true)
|
||||||
binding.spOutboundTag.setSelection(outbound)
|
binding.spOutboundTag.setText(rulesetItem.outboundTag, false)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearServer(): Boolean {
|
private fun clearServer(): Boolean {
|
||||||
binding.etRemarks.text = null
|
binding.etRemarks.text = null
|
||||||
binding.spOutboundTag.setSelection(0)
|
binding.spOutboundTag.setText(BUILTIN_OUTBOUND_TAGS.first(), false)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +97,11 @@ class RoutingEditActivity : BaseActivity() {
|
|||||||
locked = binding.chkLocked.isChecked
|
locked = binding.chkLocked.isChecked
|
||||||
domain = binding.etDomain.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
domain = binding.etDomain.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||||
ip = binding.etIp.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
ip = binding.etIp.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||||
|
process = binding.etProcess.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||||
protocol = binding.etProtocol.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
protocol = binding.etProtocol.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||||
port = binding.etPort.text.toString().nullIfBlank()
|
port = binding.etPort.text.toString().nullIfBlank()
|
||||||
network = binding.etNetwork.text.toString().nullIfBlank()
|
network = binding.etNetwork.text.toString().nullIfBlank()
|
||||||
outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition]
|
outboundTag = binding.spOutboundTag.text.toString().trim().ifEmpty { TAG_PROXY }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rulesetItem.remarks.isNullOrEmpty()) {
|
if (rulesetItem.remarks.isNullOrEmpty()) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
@@ -21,6 +20,7 @@ import com.v2ray.ang.handler.MmkvManager
|
|||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
|
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -92,7 +92,7 @@ class RoutingSettingActivity : HelperBaseActivity() {
|
|||||||
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value)
|
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value)
|
||||||
binding.tvDomainStrategySummary.text = value
|
binding.tvDomainStrategySummary.text = value
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to set domain strategy", e)
|
LogUtil.e(AppConfig.TAG, "Failed to set domain strategy", e)
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ class RoutingSettingActivity : HelperBaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
|
LogUtil.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
@@ -126,7 +126,7 @@ class RoutingSettingActivity : HelperBaseActivity() {
|
|||||||
val clipboard = try {
|
val clipboard = try {
|
||||||
Utils.getClipboard(this)
|
Utils.getClipboard(this)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
|
LogUtil.e(AppConfig.TAG, "Failed to get clipboard content", e)
|
||||||
toastError(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
return@setPositiveButton
|
return@setPositiveButton
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class RoutingSettingRecyclerAdapter(
|
|||||||
val ruleset = rulesets[position]
|
val ruleset = rulesets[position]
|
||||||
|
|
||||||
holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks
|
holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks
|
||||||
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString()
|
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.process ?: ruleset.port)?.toString()
|
||||||
holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
|
holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
|
||||||
holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
|
holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
|
||||||
holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true
|
holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
|
|
||||||
|
class ScStartActivity : BaseActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
moveTaskToBack(true)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_none)
|
||||||
|
|
||||||
|
if (!V2RayServiceManager.isRunning()) {
|
||||||
|
V2RayServiceManager.startVServiceFromToggle(this)
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
|
|
||||||
|
class ScStopActivity : BaseActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
moveTaskToBack(true)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_none)
|
||||||
|
|
||||||
|
if (V2RayServiceManager.isRunning()) {
|
||||||
|
V2RayServiceManager.stopVService(this)
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
@@ -11,6 +10,7 @@ import com.v2ray.ang.R
|
|||||||
import com.v2ray.ang.databinding.ActivityNoneBinding
|
import com.v2ray.ang.databinding.ActivityNoneBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import io.github.g00fy2.quickie.QRResult
|
import io.github.g00fy2.quickie.QRResult
|
||||||
import io.github.g00fy2.quickie.ScanCustomCode
|
import io.github.g00fy2.quickie.ScanCustomCode
|
||||||
@@ -18,7 +18,7 @@ import io.github.g00fy2.quickie.config.BarcodeFormat
|
|||||||
import io.github.g00fy2.quickie.config.ScannerConfig
|
import io.github.g00fy2.quickie.config.ScannerConfig
|
||||||
|
|
||||||
class ScannerActivity : HelperBaseActivity() {
|
class ScannerActivity : HelperBaseActivity() {
|
||||||
private val binding by lazy { ActivityNoneBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityNoneBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
|
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ class ScannerActivity : HelperBaseActivity() {
|
|||||||
finished(text)
|
finished(text)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
|
LogUtil.e(AppConfig.TAG, "Failed to decode QR code from file", e)
|
||||||
toast(R.string.toast_decoding_failed)
|
toast(R.string.toast_decoding_failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.v2ray.ang.dto.ProfileItem
|
|||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import com.v2ray.ang.enums.NetworkType
|
import com.v2ray.ang.enums.NetworkType
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.extension.nullIfBlank
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
@@ -132,6 +133,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
|
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_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
|
||||||
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
|
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 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 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 container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) }
|
||||||
@@ -241,6 +243,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
else -> null
|
else -> null
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
)
|
)
|
||||||
|
et_fm?.text = Utils.getEditable(config?.finalMask)
|
||||||
|
|
||||||
layout_extra?.visibility =
|
layout_extra?.visibility =
|
||||||
when (networks[position]) {
|
when (networks[position]) {
|
||||||
@@ -489,6 +492,13 @@ class ServerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (et_fm?.text?.toString().isNotNullEmpty()) {
|
||||||
|
if (JsonUtil.parseString(et_fm?.text?.toString()) == null) {
|
||||||
|
toast(R.string.server_lab_final_mask)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveCommon(config)
|
saveCommon(config)
|
||||||
saveStreamSettings(config)
|
saveStreamSettings(config)
|
||||||
saveTls(config)
|
saveTls(config)
|
||||||
@@ -498,7 +508,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
||||||
config.subscriptionId = subscriptionId.orEmpty()
|
config.subscriptionId = subscriptionId.orEmpty()
|
||||||
}
|
}
|
||||||
//Log.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
|
//LogUtil.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
toastSuccess(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
@@ -533,7 +543,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
} else if (config.configType == EConfigType.HYSTERIA2) {
|
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
config.obfsPassword = et_obfs_password?.text?.toString()
|
config.obfsPassword = et_obfs_password?.text?.toString()
|
||||||
config.portHopping = et_port_hop?.text?.toString()
|
config.portHopping = et_port_hop?.text?.toString()
|
||||||
config.portHoppingInterval = et_port_hop_interval?.text?.toString()
|
config.portHoppingInterval = et_port_hop_interval?.text?.toString()?.trim()
|
||||||
config.bandwidthDown = et_bandwidth_down?.text?.toString()
|
config.bandwidthDown = et_bandwidth_down?.text?.toString()
|
||||||
config.bandwidthUp = et_bandwidth_up?.text?.toString()
|
config.bandwidthUp = et_bandwidth_up?.text?.toString()
|
||||||
}
|
}
|
||||||
@@ -557,7 +567,8 @@ class ServerActivity : BaseActivity() {
|
|||||||
profileItem.serviceName = path
|
profileItem.serviceName = path
|
||||||
profileItem.authority = requestHost
|
profileItem.authority = requestHost
|
||||||
profileItem.xhttpMode = transportTypes(networks[network])[type]
|
profileItem.xhttpMode = transportTypes(networks[network])[type]
|
||||||
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
|
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim().nullIfBlank()
|
||||||
|
profileItem.finalMask = et_fm?.text?.toString()?.trim()?.nullIfBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTls(config: ProfileItem) {
|
private fun saveTls(config: ProfileItem) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
@@ -18,6 +17,7 @@ import com.v2ray.ang.extension.toastSuccess
|
|||||||
import com.v2ray.ang.fmt.CustomFmt
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class ServerCustomConfigActivity : BaseActivity() {
|
class ServerCustomConfigActivity : BaseActivity() {
|
||||||
@@ -79,7 +79,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
val profileItem = try {
|
val profileItem = try {
|
||||||
CustomFmt.parse(binding.editor.text.toString())
|
CustomFmt.parse(binding.editor.text.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom configuration", e)
|
LogUtil.e(AppConfig.TAG, "Failed to parse custom configuration", e)
|
||||||
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
|
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class ServerGroupActivity : BaseActivity() {
|
|||||||
config.subscriptionId = subscriptionId.orEmpty()
|
config.subscriptionId = subscriptionId.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
config.description = "${binding.spPolicyGroupType.selectedItem} - ${binding.spPolicyGroupSubId.selectedItem} - ${config.policyGroupFilter}"
|
config.description = "${binding.spPolicyGroupType.selectedItem} - ${binding.spPolicyGroupSubId.selectedItem} - ${config.policyGroupFilter}"
|
||||||
|
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
toastSuccess(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
|
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 autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
|
||||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
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 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 hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) }
|
||||||
@@ -112,15 +114,28 @@ class SettingsActivity : BaseActivity() {
|
|||||||
updateHevTunSettings(newValue as Boolean)
|
updateHevTunSettings(newValue as Boolean)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dynamicSocksPort?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateDynamicSocksPort(newValue as Boolean)
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initPreferenceSummaries() {
|
private fun initPreferenceSummaries() {
|
||||||
fun updateSummary(pref: androidx.preference.Preference) {
|
fun updateSummary(pref: androidx.preference.Preference) {
|
||||||
when (pref) {
|
when (pref) {
|
||||||
is EditTextPreference -> {
|
is EditTextPreference -> {
|
||||||
pref.summary = pref.text.orEmpty()
|
if (pref.key == AppConfig.PREF_SOCKS_PASSWORD) {
|
||||||
|
pref.summary = if (pref.text.isNullOrEmpty()) "" else "******"
|
||||||
|
} else {
|
||||||
|
pref.summary = pref.text.orEmpty()
|
||||||
|
}
|
||||||
pref.setOnPreferenceChangeListener { p, newValue ->
|
pref.setOnPreferenceChangeListener { p, newValue ->
|
||||||
p.summary = (newValue as? String).orEmpty()
|
if (p.key == AppConfig.PREF_SOCKS_PASSWORD) {
|
||||||
|
p.summary = if ((newValue as? String).isNullOrEmpty()) "" else "******"
|
||||||
|
} else {
|
||||||
|
p.summary = (newValue as? String).orEmpty()
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +182,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
// Initialize auto-update interval state
|
// Initialize auto-update interval state
|
||||||
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
|
|
||||||
|
updateDynamicSocksPort(MmkvManager.decodeSettingsBool(AppConfig.PREF_DYNAMIC_SOCKS_PORT, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMode(value: String?) {
|
private fun updateMode(value: String?) {
|
||||||
@@ -258,6 +275,10 @@ class SettingsActivity : BaseActivity() {
|
|||||||
fragmentInterval?.isEnabled = enabled
|
fragmentInterval?.isEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateDynamicSocksPort(enabled: Boolean) {
|
||||||
|
socksPort?.isEnabled = !enabled
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateHevTunSettings(enabled: Boolean) {
|
private fun updateHevTunSettings(enabled: Boolean) {
|
||||||
hevTunLogLevel?.isEnabled = enabled
|
hevTunLogLevel?.isEnabled = enabled
|
||||||
hevTunRwTimeout?.isEnabled = enabled
|
hevTunRwTimeout?.isEnabled = enabled
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -21,6 +20,7 @@ import com.v2ray.ang.extension.toast
|
|||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.SubscriptionsViewModel
|
import com.v2ray.ang.viewmodel.SubscriptionsViewModel
|
||||||
@@ -91,6 +91,7 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
hideLoading()
|
hideLoading()
|
||||||
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
else -> ownerActivity.toast("else")
|
else -> ownerActivity.toast("else")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Share subscription failed", e)
|
LogUtil.e(AppConfig.TAG, "Share subscription failed", e)
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import android.text.TextUtils
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.contracts.BaseAdapterListener
|
import com.v2ray.ang.contracts.BaseAdapterListener
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -13,6 +12,7 @@ import com.v2ray.ang.AppConfig
|
|||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityTaskerBinding
|
import com.v2ray.ang.databinding.ActivityTaskerBinding
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
|
||||||
class TaskerActivity : BaseActivity() {
|
class TaskerActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) }
|
||||||
@@ -62,7 +62,7 @@ class TaskerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to initialize Tasker settings", e)
|
LogUtil.e(AppConfig.TAG, "Failed to initialize Tasker settings", e)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -11,6 +10,7 @@ import com.v2ray.ang.databinding.ActivityLogcatBinding
|
|||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastError
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -55,7 +55,7 @@ class UrlSchemeActivity : BaseActivity() {
|
|||||||
startActivity(Intent(this, MainActivity::class.java))
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error processing URL scheme", e)
|
LogUtil.e(AppConfig.TAG, "Error processing URL scheme", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class UrlSchemeActivity : BaseActivity() {
|
|||||||
if (uriString.isNullOrEmpty()) {
|
if (uriString.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.i(AppConfig.TAG, uriString)
|
LogUtil.i(AppConfig.TAG, uriString)
|
||||||
|
|
||||||
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
|
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
|
||||||
val uri = Uri.parse(decodedUrl)
|
val uri = Uri.parse(decodedUrl)
|
||||||
@@ -71,7 +71,7 @@ class UrlSchemeActivity : BaseActivity() {
|
|||||||
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
|
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
|
||||||
decodedUrl += "#${fragment}"
|
decodedUrl += "#${fragment}"
|
||||||
}
|
}
|
||||||
Log.i(AppConfig.TAG, decodedUrl)
|
LogUtil.i(AppConfig.TAG, decodedUrl)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
@@ -22,6 +21,7 @@ import com.v2ray.ang.extension.toastError
|
|||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.UserAssetViewModel
|
import com.v2ray.ang.viewmodel.UserAssetViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -84,7 +84,7 @@ class UserAssetActivity : HelperBaseActivity() {
|
|||||||
MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value)
|
MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value)
|
||||||
binding.tvGeoFilesSourcesSummary.text = value
|
binding.tvGeoFilesSourcesSummary.text = value
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to set geo files sources", e)
|
LogUtil.e(AppConfig.TAG, "Failed to set geo files sources", e)
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ class UserAssetActivity : HelperBaseActivity() {
|
|||||||
}.also { cursor.close() }
|
}.also { cursor.close() }
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to get cursor name", e)
|
LogUtil.e(AppConfig.TAG, "Failed to get cursor name", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ class UserAssetActivity : HelperBaseActivity() {
|
|||||||
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
|
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to import asset from URL", e)
|
LogUtil.e(AppConfig.TAG, "Failed to import asset from URL", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
@@ -13,6 +12,7 @@ import com.v2ray.ang.dto.AssetUrlItem
|
|||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
try {
|
try {
|
||||||
file.delete()
|
file.delete()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
|
LogUtil.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
@@ -89,11 +88,11 @@ object HttpUtil {
|
|||||||
|
|
||||||
val ipList = sortedAddresses.mapNotNull { it.hostAddress }
|
val ipList = sortedAddresses.mapNotNull { it.hostAddress }
|
||||||
|
|
||||||
Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}")
|
LogUtil.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}")
|
||||||
|
|
||||||
return ipList
|
return ipList
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to resolve host to IP", e)
|
LogUtil.e(AppConfig.TAG, "Failed to resolve host to IP", e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +127,7 @@ object HttpUtil {
|
|||||||
* @throws IOException If an I/O error occurs.
|
* @throws IOException If an I/O error occurs.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@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): String {
|
||||||
var currentUrl = url
|
var currentUrl = url
|
||||||
var redirects = 0
|
var redirects = 0
|
||||||
val maxRedirects = 3
|
val maxRedirects = 3
|
||||||
@@ -219,7 +218,7 @@ object HttpUtil {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to create proxy connection", e)
|
LogUtil.e(AppConfig.TAG, "Failed to create proxy connection", e)
|
||||||
// If an exception occurs, close the connection and return null
|
// If an exception occurs, close the connection and return null
|
||||||
conn?.disconnect()
|
conn?.disconnect()
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
@@ -72,7 +71,7 @@ object JsonUtil {
|
|||||||
try {
|
try {
|
||||||
return JsonParser.parseString(src).getAsJsonObject()
|
return JsonParser.parseString(src).getAsJsonObject()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse JSON string", e)
|
LogUtil.e(AppConfig.TAG, "Failed to parse JSON string", e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object LogUtil {
|
||||||
|
|
||||||
|
private const val DEFAULT_LEVEL = "warning"
|
||||||
|
private const val CACHE_UNSET = Int.MIN_VALUE
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var cachedMinPriority: Int = CACHE_UNSET
|
||||||
|
|
||||||
|
private fun parsePriority(level: String?): Int {
|
||||||
|
return when ((level ?: DEFAULT_LEVEL).lowercase(Locale.US)) {
|
||||||
|
"verbose" -> Log.VERBOSE
|
||||||
|
"debug" -> Log.DEBUG
|
||||||
|
"info" -> Log.INFO
|
||||||
|
"warn", "warning" -> Log.WARN
|
||||||
|
"error" -> Log.ERROR
|
||||||
|
"none", "off" -> Int.MAX_VALUE
|
||||||
|
else -> Log.WARN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun refreshLogLevel() {
|
||||||
|
cachedMinPriority = parsePriority(MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL, DEFAULT_LEVEL))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun minPriority(): Int {
|
||||||
|
val cached = cachedMinPriority
|
||||||
|
if (cached != CACHE_UNSET) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
return synchronized(this) {
|
||||||
|
val current = cachedMinPriority
|
||||||
|
if (current != CACHE_UNSET) {
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
parsePriority(MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL, DEFAULT_LEVEL)).also {
|
||||||
|
cachedMinPriority = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isEnabled(priority: Int): Boolean {
|
||||||
|
return priority >= minPriority()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun log(priority: Int, tag: String, message: String, throwable: Throwable? = null) {
|
||||||
|
if (!isEnabled(priority)) return
|
||||||
|
|
||||||
|
when {
|
||||||
|
throwable == null -> Log.println(priority, tag, message)
|
||||||
|
priority >= Log.ERROR -> Log.e(tag, message, throwable)
|
||||||
|
priority == Log.WARN -> Log.w(tag, message, throwable)
|
||||||
|
priority == Log.INFO -> Log.i(tag, message, throwable)
|
||||||
|
priority == Log.DEBUG -> Log.d(tag, message, throwable)
|
||||||
|
else -> Log.v(tag, message, throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun d(tag: String = AppConfig.TAG, message: String) = log(Log.DEBUG, tag, message)
|
||||||
|
fun i(tag: String = AppConfig.TAG, message: String) = log(Log.INFO, tag, message)
|
||||||
|
fun w(tag: String = AppConfig.TAG, message: String) = log(Log.WARN, tag, message)
|
||||||
|
fun e(tag: String = AppConfig.TAG, message: String) = log(Log.ERROR, tag, message)
|
||||||
|
|
||||||
|
fun d(tag: String = AppConfig.TAG, message: String, throwable: Throwable) = log(Log.DEBUG, tag, message, throwable)
|
||||||
|
fun i(tag: String = AppConfig.TAG, message: String, throwable: Throwable) = log(Log.INFO, tag, message, throwable)
|
||||||
|
fun w(tag: String = AppConfig.TAG, message: String, throwable: Throwable) = log(Log.WARN, tag, message, throwable)
|
||||||
|
fun e(tag: String = AppConfig.TAG, message: String, throwable: Throwable) = log(Log.ERROR, tag, message, throwable)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.util
|
|||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.TestServiceMessage
|
import com.v2ray.ang.dto.TestServiceMessage
|
||||||
import com.v2ray.ang.service.V2RayTestService
|
import com.v2ray.ang.service.V2RayTestService
|
||||||
@@ -47,7 +46,7 @@ object MessageUtil {
|
|||||||
intent.putExtra("content", message)
|
intent.putExtra("content", message)
|
||||||
ctx.startService(intent)
|
ctx.startService(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to send message to test service", e)
|
LogUtil.e(AppConfig.TAG, "Failed to send message to test service", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ object MessageUtil {
|
|||||||
intent.putExtra("content", content)
|
intent.putExtra("content", content)
|
||||||
ctx.sendBroadcast(intent)
|
ctx.sendBroadcast(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to send message with action: $action", e)
|
LogUtil.e(AppConfig.TAG, "Failed to send message with action: $action", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
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>()
|
||||||
|
|
||||||
|
val packageUidMap: Map<String, String>
|
||||||
|
get() = packageUidCache
|
||||||
|
|
||||||
|
fun packageNamesToUids(context: Context, packageNames: List<String>): List<String> {
|
||||||
|
return packageNames.mapNotNull { pkg ->
|
||||||
|
packageUidCache[pkg] ?: resolveUid(context, pkg)?.also { uid ->
|
||||||
|
packageUidCache[pkg] = uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uidsToPackageNames(uids: List<String>): List<String> {
|
||||||
|
if (uids.isEmpty()) return emptyList()
|
||||||
|
return packageUidCache.filterValues { it in uids }.keys.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uidToPackageName(uid: String): String? {
|
||||||
|
return packageUidCache.entries.firstOrNull { it.value == uid }?.key
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveUid(context: Context, packageName: String): String? {
|
||||||
|
return try {
|
||||||
|
val uid = context.packageManager.getPackageUid(packageName, 0).toString()
|
||||||
|
LogUtil.d(TAG, "Package: $packageName -> UID: $uid")
|
||||||
|
uid
|
||||||
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
|
LogUtil.w(TAG, "Package not found: $packageName")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ import android.os.LocaleList
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -79,7 +78,7 @@ object Utils {
|
|||||||
val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
cmb.primaryClip?.getItemAt(0)?.text.toString()
|
cmb.primaryClip?.getItemAt(0)?.text.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
|
LogUtil.e(AppConfig.TAG, "Failed to get clipboard content", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +95,7 @@ object Utils {
|
|||||||
val clipData = ClipData.newPlainText(null, content)
|
val clipData = ClipData.newPlainText(null, content)
|
||||||
cmb.setPrimaryClip(clipData)
|
cmb.setPrimaryClip(clipData)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to set clipboard content", e)
|
LogUtil.e(AppConfig.TAG, "Failed to set clipboard content", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,12 +121,12 @@ object Utils {
|
|||||||
try {
|
try {
|
||||||
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
|
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to decode standard base64", e)
|
LogUtil.e(AppConfig.TAG, "Failed to decode standard base64", e)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
|
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to decode URL-safe base64", e)
|
LogUtil.e(AppConfig.TAG, "Failed to decode URL-safe base64", e)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -139,7 +138,7 @@ object Utils {
|
|||||||
* @param removePadding
|
* @param removePadding
|
||||||
* @return The base64 encoded string, or an empty string if encoding fails.
|
* @return The base64 encoded string, or an empty string if encoding fails.
|
||||||
*/
|
*/
|
||||||
fun encode(text: String, removePadding : Boolean = false): String {
|
fun encode(text: String, removePadding: Boolean = false): String {
|
||||||
return try {
|
return try {
|
||||||
var encoded = Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
var encoded = Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||||
if (removePadding) {
|
if (removePadding) {
|
||||||
@@ -147,7 +146,7 @@ object Utils {
|
|||||||
}
|
}
|
||||||
encoded
|
encoded
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to encode text to base64", e)
|
LogUtil.e(AppConfig.TAG, "Failed to encode text to base64", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +189,7 @@ object Utils {
|
|||||||
|
|
||||||
return isIpv6Address(addr)
|
return isIpv6Address(addr)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to validate IP address", e)
|
LogUtil.e(AppConfig.TAG, "Failed to validate IP address", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,7 +270,7 @@ object Utils {
|
|||||||
Patterns.DOMAIN_NAME.matcher(value).matches() ||
|
Patterns.DOMAIN_NAME.matcher(value).matches() ||
|
||||||
URLUtil.isValidUrl(value)
|
URLUtil.isValidUrl(value)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to validate URL", e)
|
LogUtil.e(AppConfig.TAG, "Failed to validate URL", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,7 +286,7 @@ object Utils {
|
|||||||
val uri = uriString.toUri()
|
val uri = uriString.toUri()
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to open URI", e)
|
LogUtil.e(AppConfig.TAG, "Failed to open URI", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +299,7 @@ object Utils {
|
|||||||
return try {
|
return try {
|
||||||
UUID.randomUUID().toString().replace("-", "")
|
UUID.randomUUID().toString().replace("-", "")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to generate UUID", e)
|
LogUtil.e(AppConfig.TAG, "Failed to generate UUID", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,7 +314,7 @@ object Utils {
|
|||||||
return try {
|
return try {
|
||||||
URLDecoder.decode(url, Charsets.UTF_8.toString())
|
URLDecoder.decode(url, Charsets.UTF_8.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to decode URL", e)
|
LogUtil.e(AppConfig.TAG, "Failed to decode URL", e)
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,7 +329,7 @@ object Utils {
|
|||||||
return try {
|
return try {
|
||||||
URLEncoder.encode(url, Charsets.UTF_8.toString())
|
URLEncoder.encode(url, Charsets.UTF_8.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to encode URL", e)
|
LogUtil.e(AppConfig.TAG, "Failed to encode URL", e)
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,7 +348,7 @@ object Utils {
|
|||||||
// to avoid cross-language interoperability issues.
|
// to avoid cross-language interoperability issues.
|
||||||
URLDecoder.decode(url.replace("+", "%2B"), Charsets.UTF_8.toString())
|
URLDecoder.decode(url.replace("+", "%2B"), Charsets.UTF_8.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to decode encodeURIComponent", e)
|
LogUtil.e(AppConfig.TAG, "Failed to decode encodeURIComponent", e)
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +364,7 @@ object Utils {
|
|||||||
// Replace '+' with '%20' to conform to encodeURIComponent semantics.
|
// Replace '+' with '%20' to conform to encodeURIComponent semantics.
|
||||||
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
|
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to encode encodeURIComponent", e)
|
LogUtil.e(AppConfig.TAG, "Failed to encode encodeURIComponent", e)
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,7 +386,7 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e)
|
LogUtil.e(AppConfig.TAG, "Failed to read asset file: $fileName", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,7 +404,7 @@ object Utils {
|
|||||||
context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
|
context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
|
||||||
?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
|
?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to get user asset path", e)
|
LogUtil.e(AppConfig.TAG, "Failed to get user asset path", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,7 +419,7 @@ object Utils {
|
|||||||
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
|
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
|
||||||
Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to generate device ID", e)
|
LogUtil.e(AppConfig.TAG, "Failed to generate device ID", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -512,7 +511,7 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
|
LogUtil.e(AppConfig.TAG, "Failed to validate subscription URL", e)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -582,7 +581,7 @@ object Utils {
|
|||||||
// Check if they're in the same subnet
|
// Check if they're in the same subnet
|
||||||
return (ipLong and mask) == (cidrIpLong and mask)
|
return (ipLong and mask) == (cidrIpLong and mask)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e)
|
LogUtil.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -599,7 +598,7 @@ object Utils {
|
|||||||
val sdf = SimpleDateFormat(pattern, locale)
|
val sdf = SimpleDateFormat(pattern, locale)
|
||||||
sdf.format(Date(ts))
|
sdf.format(Date(ts))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to format timestamp", e)
|
LogUtil.e(AppConfig.TAG, "Failed to format timestamp", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -63,7 +62,7 @@ object ZipUtil {
|
|||||||
zos.closeEntry()
|
zos.closeEntry()
|
||||||
zos.close()
|
zos.close()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to zip folder", e)
|
LogUtil.e(AppConfig.TAG, "Failed to zip folder", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -99,7 +98,7 @@ object ZipUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to unzip file", e)
|
LogUtil.e(AppConfig.TAG, "Failed to unzip file", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.v2ray.ang.viewmodel
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class LogcatViewModel : ViewModel() {
|
class LogcatViewModel : ViewModel() {
|
||||||
@@ -28,7 +29,7 @@ class LogcatViewModel : ViewModel() {
|
|||||||
logsetsAll.addAll(allText)
|
logsetsAll.addAll(allText)
|
||||||
applyFilter()
|
applyFilter()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
android.util.Log.e(AppConfig.TAG, "Failed to get logcat", e)
|
LogUtil.e(AppConfig.TAG, "Failed to get logcat", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ class LogcatViewModel : ViewModel() {
|
|||||||
logsetsAll.clear()
|
logsetsAll.clear()
|
||||||
filteredLogs = emptyList()
|
filteredLogs = emptyList()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
android.util.Log.e(AppConfig.TAG, "Failed to clear logcat", e)
|
LogUtil.e(AppConfig.TAG, "Failed to clear logcat", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -19,6 +18,7 @@ import com.v2ray.ang.dto.ServersCache
|
|||||||
import com.v2ray.ang.dto.SubscriptionCache
|
import com.v2ray.ang.dto.SubscriptionCache
|
||||||
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
||||||
import com.v2ray.ang.dto.TestServiceMessage
|
import com.v2ray.ang.dto.TestServiceMessage
|
||||||
|
import com.v2ray.ang.extension.matchesPattern
|
||||||
import com.v2ray.ang.extension.serializable
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.extension.toastError
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
@@ -26,6 +26,7 @@ import com.v2ray.ang.handler.AngConfigManager
|
|||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.handler.SpeedtestManager
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -35,6 +36,7 @@ import kotlinx.coroutines.cancelChildren
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
import java.util.regex.PatternSyntaxException
|
||||||
|
|
||||||
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private var serverList = mutableListOf<String>() // MmkvManager.decodeServerList()
|
private var serverList = mutableListOf<String>() // MmkvManager.decodeServerList()
|
||||||
@@ -64,7 +66,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
||||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||||
SpeedtestManager.closeAllTcpSockets()
|
SpeedtestManager.closeAllTcpSockets()
|
||||||
Log.i(AppConfig.TAG, "Main ViewModel is cleared")
|
LogUtil.i(AppConfig.TAG, "Main ViewModel is cleared")
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +119,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun updateCache() {
|
fun updateCache() {
|
||||||
serversCache.clear()
|
serversCache.clear()
|
||||||
val kw = keywordFilter.trim().lowercase()
|
val kw = keywordFilter.trim()
|
||||||
|
val searchRegex = try {
|
||||||
|
if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null
|
||||||
|
} catch (e: PatternSyntaxException) {
|
||||||
|
null // Fallback to literal search if regex is invalid
|
||||||
|
}
|
||||||
for (guid in serverList) {
|
for (guid in serverList) {
|
||||||
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||||
if (kw.isEmpty()) {
|
if (kw.isEmpty()) {
|
||||||
@@ -125,11 +132,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val remarks = profile.remarks.lowercase()
|
val remarks = profile.remarks
|
||||||
val description = profile.description.orEmpty().lowercase()
|
val description = profile.description.orEmpty()
|
||||||
val server = profile.server.orEmpty().lowercase()
|
val server = profile.server.orEmpty()
|
||||||
|
val protocol = profile.configType.name
|
||||||
if (remarks.contains(kw) || description.contains(kw) || server.contains(kw)) {
|
if (remarks.matchesPattern(searchRegex, kw)
|
||||||
|
|| description.matchesPattern(searchRegex, kw)
|
||||||
|
|| server.matchesPattern(searchRegex, kw)
|
||||||
|
|| protocol.matchesPattern(searchRegex, kw)
|
||||||
|
) {
|
||||||
serversCache.add(ServersCache(guid, profile))
|
serversCache.add(ServersCache(guid, profile))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,6 +413,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
reloadServerList()
|
reloadServerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findSubscriptionIdBySelect(): String? {
|
||||||
|
// Get the selected server GUID
|
||||||
|
val selectedGuid = MmkvManager.getSelectServer()
|
||||||
|
if (selectedGuid.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val config = MmkvManager.decodeServerConfig(selectedGuid)
|
||||||
|
return config?.subscriptionId
|
||||||
|
}
|
||||||
|
|
||||||
fun onTestsFinished() {
|
fun onTestsFinished() {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_REMOVE_INVALID_AFTER_TEST)) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_REMOVE_INVALID_AFTER_TEST)) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.v2ray.ang.viewmodel
|
package com.v2ray.ang.viewmodel
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.AssetUrlCache
|
import com.v2ray.ang.dto.AssetUrlCache
|
||||||
@@ -8,6 +7,7 @@ import com.v2ray.ang.dto.AssetUrlItem
|
|||||||
import com.v2ray.ang.extension.concatUrl
|
import com.v2ray.ang.extension.concatUrl
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.HttpUtil
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -95,7 +95,7 @@ class UserAssetViewModel : ViewModel() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
|
LogUtil.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
|
||||||
} finally {
|
} finally {
|
||||||
conn.disconnect()
|
conn.disconnect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M7,10l5,5 5,-5z" />
|
||||||
|
</vector>
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M0,512C0,229.23 229.81,0 512,0 794.77,0 1024,229.81 1024,512 1024,794.77 794.19,1024 512,1024 229.23,1024 0,794.19 0,512Z" />
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M213.333333 789.461333V234.538667C213.333333 168.533333 285.013333 127.530667 341.930667 160.981333l471.68 277.461334c56.106667 32.981333 56.106667 114.133333 0 147.114666L341.930667 863.018667C285.056 896.469333 213.333333 855.466667 213.333333 789.461333z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M0,512C0,229.23 229.81,0 512,0 794.77,0 1024,229.81 1024,512 1024,794.77 794.19,1024 512,1024 229.23,1024 0,794.19 0,512Z" />
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M256 192h512c35.392 0 64 28.608 64 64v512c0 35.392-28.608 64-64 64H256c-35.392 0-64-28.608-64-64V256c0-35.392 28.608-64 64-64z" />
|
||||||
|
</vector>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
tools:context=".ui.LogcatActivity">
|
tools:context=".ui.LogcatActivity">
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/refreshLayout"
|
android:id="@+id/refresh_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginBottom="-16dp"
|
||||||
android:layout_marginEnd="@dimen/padding_spacing_dp16">
|
android:layout_marginEnd="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
@@ -108,6 +109,7 @@
|
|||||||
android:nextFocusLeft="@+id/layout_test"
|
android:nextFocusLeft="@+id/layout_test"
|
||||||
android:src="@drawable/ic_play_24dp"
|
android:src="@drawable/ic_play_24dp"
|
||||||
app:tint="@color/colorWhite"
|
app:tint="@color/colorWhite"
|
||||||
|
app:useCompatPadding="true"
|
||||||
app:layout_anchorGravity="bottom|right|end" />
|
app:layout_anchorGravity="bottom|right|end" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -103,6 +103,26 @@
|
|||||||
android:maxLines="10" />
|
android:maxLines="10" />
|
||||||
</LinearLayout>
|
</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/routing_settings_process" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_process"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/routing_settings_tips"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:maxLines="10" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -170,13 +190,33 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/routing_settings_outbound_tag" />
|
android:text="@string/routing_settings_outbound_tag" />
|
||||||
|
|
||||||
<Spinner
|
<RelativeLayout
|
||||||
android:id="@+id/sp_outbound_tag"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||||
android:layout_marginBottom="@dimen/padding_spacing_dp8"
|
android:layout_marginBottom="@dimen/padding_spacing_dp8">
|
||||||
android:entries="@array/outbound_tag" />
|
|
||||||
|
<AutoCompleteTextView
|
||||||
|
android:id="@+id/sp_outbound_tag"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:completionThreshold="0"
|
||||||
|
android:hint="@string/routing_settings_outbound_tag_hint"
|
||||||
|
android:inputType="text"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:paddingEnd="40dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_outbound_tag_dropdown"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/routing_settings_outbound_tag"
|
||||||
|
android:src="@drawable/ic_arrow_drop_down" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -85,8 +85,7 @@
|
|||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_port_hop_interval"
|
android:id="@+id/et_port_hop_interval"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content" />
|
||||||
android:inputType="number" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,17 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/refresh_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:scrollbars="vertical" />
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -118,4 +118,25 @@
|
|||||||
android:minLines="4" />
|
android:minLines="4" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_fm"
|
||||||
|
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_final_mask" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_fm"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="top"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:maxLines="20"
|
||||||
|
android:minLines="4" />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -98,6 +98,10 @@
|
|||||||
android:id="@+id/sort_by_test_results"
|
android:id="@+id/sort_by_test_results"
|
||||||
android:title="@string/title_sort_by_test_results"
|
android:title="@string/title_sort_by_test_results"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/locate_selected_config"
|
||||||
|
android:title="@string/title_locate_selected_config"
|
||||||
|
app:showAsAction="never" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/sub_update"
|
android:id="@+id/sub_update"
|
||||||
android:title="@string/title_sub_update"
|
android:title="@string/title_sub_update"
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
<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_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -185,8 +186,11 @@
|
|||||||
<string name="title_pref_fake_dns_enabled">تفعيل DNS وهمي</string>
|
<string name="title_pref_fake_dns_enabled">تفعيل DNS وهمي</string>
|
||||||
<string name="summary_pref_fake_dns_enabled">يعيد DNS المحلي عنوان IP وهمي (أسرع، ولكنه قد لا يعمل مع بعض التطبيقات)</string>
|
<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 والمسارات إلى واجهة VPN</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">تفضيل IPv6</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="title_pref_remote_dns">DNS البعيد (udp/tcp/https/quic) (اختياري)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
@@ -218,6 +222,12 @@
|
|||||||
|
|
||||||
<string name="title_pref_socks_port">منفذ بروكسي Local</string>
|
<string name="title_pref_socks_port">منفذ بروكسي Local</string>
|
||||||
<string name="summary_pref_socks_port">منفذ بروكسي Local</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>
|
||||||
|
<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_local_dns_port">منفذ DNS المحلي</string>
|
<string name="title_pref_local_dns_port">منفذ DNS المحلي</string>
|
||||||
<string name="summary_pref_local_dns_port">منفذ DNS المحلي</string>
|
<string name="summary_pref_local_dns_port">منفذ DNS المحلي</string>
|
||||||
@@ -242,6 +252,7 @@
|
|||||||
<string name="summary_pref_tg_group">الانضمام إلى مجموعة Telegram</string>
|
<string name="summary_pref_tg_group">الانضمام إلى مجموعة Telegram</string>
|
||||||
<string name="toast_tg_app_not_found">لم يتم العثور على تطبيق Telegram</string>
|
<string name="toast_tg_app_not_found">لم يتم العثور على تطبيق Telegram</string>
|
||||||
<string name="title_privacy_policy">سياسة الخصوصية</string>
|
<string name="title_privacy_policy">سياسة الخصوصية</string>
|
||||||
|
<string name="title_qr_code">QR code</string>
|
||||||
<string name="title_about">حول\nترجمة م. ابراهيم قاسم</string>
|
<string name="title_about">حول\nترجمة م. ابراهيم قاسم</string>
|
||||||
<string name="title_source_code">الكود المصدري</string>
|
<string name="title_source_code">الكود المصدري</string>
|
||||||
<string name="title_oss_license">Open Source licenses</string>
|
<string name="title_oss_license">Open Source licenses</string>
|
||||||
@@ -299,13 +310,17 @@
|
|||||||
<string name="title_update_config_count">Update %d configurations</string>
|
<string name="title_update_config_count">Update %d configurations</string>
|
||||||
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||||
<string name="title_update_subscription_no_subscription">No subscriptions</string>
|
<string name="title_update_subscription_no_subscription">No subscriptions</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="tasker_start_service">بدء الخدمة</string>
|
<string name="tasker_start_service">بدء الخدمة</string>
|
||||||
<string name="tasker_setting_confirm">تأكيد</string>
|
<string name="tasker_setting_confirm">تأكيد</string>
|
||||||
|
|
||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">استراتيجية النطاق</string>
|
<string name="routing_settings_domain_strategy">استراتيجية النطاق</string>
|
||||||
<string name="routing_settings_title">إعدادات التوجيه</string>
|
<string name="routing_settings_title">إعدادات التوجيه</string>
|
||||||
<string name="routing_settings_tips">مفصولة بفواصل (،)، تذكر الحفظ</string>
|
<string name="routing_settings_tips">افصل بعلامة الفاصلة (,) واختر أحد: domain أو ip أو process</string>
|
||||||
<string name="routing_settings_save">حفظ</string>
|
<string name="routing_settings_save">حفظ</string>
|
||||||
<string name="routing_settings_delete">مسح</string>
|
<string name="routing_settings_delete">مسح</string>
|
||||||
<string name="routing_settings_rule_title">Routing Rule Settings</string>
|
<string name="routing_settings_rule_title">Routing Rule Settings</string>
|
||||||
@@ -316,6 +331,15 @@
|
|||||||
<string name="routing_settings_import_rulesets_from_qrcode">استيراد مجموعة قواعد من رمز الاستجابة السريعة</string>
|
<string name="routing_settings_import_rulesets_from_qrcode">استيراد مجموعة قواعد من رمز الاستجابة السريعة</string>
|
||||||
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
|
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
|
||||||
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
|
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
|
||||||
|
<string name="routing_settings_domain">domain</string>
|
||||||
|
<string name="routing_settings_ip">ip</string>
|
||||||
|
<string name="routing_settings_process">process (Package Name — only supported when using Xray TUN, routeOnly enabled, and Android 10+)</string>
|
||||||
|
<string name="routing_settings_port">port</string>
|
||||||
|
<string name="routing_settings_protocol">protocol</string>
|
||||||
|
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
||||||
|
<string name="routing_settings_network">network</string>
|
||||||
|
<string name="routing_settings_network_tip">[udp|tcp]</string>
|
||||||
|
<string name="routing_settings_outbound_tag">outboundTag</string>
|
||||||
|
|
||||||
<string name="connection_test_pending">التحقق من الاتصال</string>
|
<string name="connection_test_pending">التحقق من الاتصال</string>
|
||||||
<string name="connection_test_testing">يجري الاختبار…</string>
|
<string name="connection_test_testing">يجري الاختبار…</string>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
<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_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -185,8 +186,11 @@
|
|||||||
<string name="title_pref_fake_dns_enabled">ভুয়া DNS সক্রিয় করুন</string>
|
<string name="title_pref_fake_dns_enabled">ভুয়া DNS সক্রিয় করুন</string>
|
||||||
<string name="summary_pref_fake_dns_enabled">স্থানীয় DNS মিথ্যা আইপি ঠিকানা ফেরত দেয় (দ্রুত, তবে এটি কিছু অ্যাপের জন্য কাজ নাও করতে পারে)</string>
|
<string name="summary_pref_fake_dns_enabled">স্থানীয় DNS মিথ্যা আইপি ঠিকানা ফেরত দেয় (দ্রুত, তবে এটি কিছু অ্যাপের জন্য কাজ নাও করতে পারে)</string>
|
||||||
|
|
||||||
|
<string name="title_pref_ipv6_enabled">IPv6 সক্রিয় করুন</string>
|
||||||
|
<string name="summary_pref_ipv6_enabled">VPN ইন্টারফেসে IPv6 ঠিকানা এবং রুট যোগ করুন</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">IPv6 অগ্রাধিকার দিন</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="title_pref_remote_dns">রিমোট DNS (udp/tcp/https/quic)(ঐচ্ছিক)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
@@ -218,6 +222,12 @@
|
|||||||
|
|
||||||
<string name="title_pref_socks_port">Local প্রক্সি পোর্ট</string>
|
<string name="title_pref_socks_port">Local প্রক্সি পোর্ট</string>
|
||||||
<string name="summary_pref_socks_port">Local প্রক্সি পোর্ট</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>
|
||||||
|
<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_local_dns_port">স্থানীয় DNS পোর্ট</string>
|
<string name="title_pref_local_dns_port">স্থানীয় DNS পোর্ট</string>
|
||||||
<string name="summary_pref_local_dns_port">স্থানীয় DNS পোর্ট</string>
|
<string name="summary_pref_local_dns_port">স্থানীয় DNS পোর্ট</string>
|
||||||
@@ -242,6 +252,7 @@
|
|||||||
<string name="summary_pref_tg_group">টেলিগ্রাম গ্রুপে যোগদান করুন</string>
|
<string name="summary_pref_tg_group">টেলিগ্রাম গ্রুপে যোগদান করুন</string>
|
||||||
<string name="toast_tg_app_not_found">টেলিগ্রাম অ্যাপ পাওয়া যায়নি</string>
|
<string name="toast_tg_app_not_found">টেলিগ্রাম অ্যাপ পাওয়া যায়নি</string>
|
||||||
<string name="title_privacy_policy">গোপনীয়তা নীতি</string>
|
<string name="title_privacy_policy">গোপনীয়তা নীতি</string>
|
||||||
|
<string name="title_qr_code">QR code</string>
|
||||||
<string name="title_about">সম্পর্কিত</string>
|
<string name="title_about">সম্পর্কিত</string>
|
||||||
<string name="title_source_code">সোর্স কোড</string>
|
<string name="title_source_code">সোর্স কোড</string>
|
||||||
<string name="title_oss_license">Open Source licenses</string>
|
<string name="title_oss_license">Open Source licenses</string>
|
||||||
@@ -298,13 +309,17 @@
|
|||||||
<string name="title_update_config_count">Update %d configurations</string>
|
<string name="title_update_config_count">Update %d configurations</string>
|
||||||
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||||
<string name="title_update_subscription_no_subscription">No subscriptions</string>
|
<string name="title_update_subscription_no_subscription">No subscriptions</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="tasker_start_service">সার্ভিস শুরু করুন</string>
|
<string name="tasker_start_service">সার্ভিস শুরু করুন</string>
|
||||||
<string name="tasker_setting_confirm">নিশ্চিত করুন</string>
|
<string name="tasker_setting_confirm">নিশ্চিত করুন</string>
|
||||||
|
|
||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">ডোমেইন কৌশল</string>
|
<string name="routing_settings_domain_strategy">ডোমেইন কৌশল</string>
|
||||||
<string name="routing_settings_title">রাউটিং সেটিংস</string>
|
<string name="routing_settings_title">রাউটিং সেটিংস</string>
|
||||||
<string name="routing_settings_tips">কমা (,) দ্বারা আলাদা করুন, মনে রাখবেন সেভ করতে</string>
|
<string name="routing_settings_tips">কমা (,) দিয়ে আলাদা করুন। domain, ip বা process-এ একটি নির্বাচন করুন</string>
|
||||||
<string name="routing_settings_save">সেভ করুন</string>
|
<string name="routing_settings_save">সেভ করুন</string>
|
||||||
<string name="routing_settings_delete">মুছে ফেলুন</string>
|
<string name="routing_settings_delete">মুছে ফেলুন</string>
|
||||||
<string name="routing_settings_rule_title">Routing Rule Settings</string>
|
<string name="routing_settings_rule_title">Routing Rule Settings</string>
|
||||||
@@ -315,6 +330,15 @@
|
|||||||
<string name="routing_settings_import_rulesets_from_qrcode">QRcode থেকে রুলসেট আমদানি করুন</string>
|
<string name="routing_settings_import_rulesets_from_qrcode">QRcode থেকে রুলসেট আমদানি করুন</string>
|
||||||
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
|
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
|
||||||
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
|
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
|
||||||
|
<string name="routing_settings_domain">domain</string>
|
||||||
|
<string name="routing_settings_ip">ip</string>
|
||||||
|
<string name="routing_settings_process">process (Package Name — only supported when using Xray TUN, routeOnly enabled, and Android 10+)</string>
|
||||||
|
<string name="routing_settings_port">port</string>
|
||||||
|
<string name="routing_settings_protocol">protocol</string>
|
||||||
|
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
||||||
|
<string name="routing_settings_network">network</string>
|
||||||
|
<string name="routing_settings_network_tip">[udp|tcp]</string>
|
||||||
|
<string name="routing_settings_outbound_tag">outboundTag</string>
|
||||||
|
|
||||||
<string name="connection_test_pending">সংযোগ পরীক্ষা করুন</string>
|
<string name="connection_test_pending">সংযোগ পরীক্ষা করুন</string>
|
||||||
<string name="connection_test_testing">পরীক্ষা চলছে…</string>
|
<string name="connection_test_testing">পরীক্ষা চলছে…</string>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</string>
|
<string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</string>
|
||||||
<string name="server_lab_xhttp_mode">هالت XHTTP</string>
|
<string name="server_lab_xhttp_mode">هالت XHTTP</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra خام JSON، قالوو: { XHTTPObject }</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_ech_config_list">نومگه کانفیگ Ech</string>
|
<string name="server_lab_ech_config_list">نومگه کانفیگ Ech</string>
|
||||||
<string name="server_lab_ech_force_query">پورس وو جۊ اجباری Ech</string>
|
<string name="server_lab_ech_force_query">پورس وو جۊ اجباری Ech</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -185,8 +186,11 @@
|
|||||||
<string name="title_pref_fake_dns_enabled">ره وندن DNS جئلی</string>
|
<string name="title_pref_fake_dns_enabled">ره وندن DNS جئلی</string>
|
||||||
<string name="summary_pref_fake_dns_enabled">DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومه یل کار نکونه)</string>
|
<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="title_pref_prefer_ipv6">ترجی داڌن IPv6</string>
|
<string name="title_pref_prefer_ipv6">ترجی داڌن IPv6</string>
|
||||||
<string name="summary_pref_prefer_ipv6">تورا 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="title_pref_remote_dns">DNS ز ره دیر (اختیاری) (udp/tcp/https/quic) (اختیاری)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
@@ -218,6 +222,12 @@
|
|||||||
|
|
||||||
<string name="title_pref_socks_port">پورت پروکسی مهلی</string>
|
<string name="title_pref_socks_port">پورت پروکسی مهلی</string>
|
||||||
<string name="summary_pref_socks_port">پورت پروکسی مهلی</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_local_dns_port">پورت DNS مهلی</string>
|
<string name="title_pref_local_dns_port">پورت DNS مهلی</string>
|
||||||
<string name="summary_pref_local_dns_port">پورت DNS مهلی</string>
|
<string name="summary_pref_local_dns_port">پورت DNS مهلی</string>
|
||||||
@@ -242,6 +252,7 @@
|
|||||||
<string name="summary_pref_tg_group">ٱووڌن من بونکۊ تلگرام</string>
|
<string name="summary_pref_tg_group">ٱووڌن من بونکۊ تلگرام</string>
|
||||||
<string name="toast_tg_app_not_found">برنومه تلگرامن نجوست</string>
|
<string name="toast_tg_app_not_found">برنومه تلگرامن نجوست</string>
|
||||||
<string name="title_privacy_policy">هریم سیخومی</string>
|
<string name="title_privacy_policy">هریم سیخومی</string>
|
||||||
|
<string name="title_qr_code">QR code</string>
|
||||||
<string name="title_about">زبار</string>
|
<string name="title_about">زبار</string>
|
||||||
<string name="title_source_code">کود بونچک</string>
|
<string name="title_source_code">کود بونچک</string>
|
||||||
<string name="title_oss_license">موجوزا کود بونچک</string>
|
<string name="title_oss_license">موجوزا کود بونچک</string>
|
||||||
@@ -299,13 +310,17 @@
|
|||||||
<string name="title_update_config_count">ورۊ کردن %d کانفیگ</string>
|
<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_result">%1$d کانفیگ ورۊ وابی (مووفق: %2$d، شکست خرده: %3$d، رڌ وابیڌه: %4$d)</string>
|
||||||
<string name="title_update_subscription_no_subscription">بؽ اشتراک</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="tasker_start_service">ره وندن خدمات</string>
|
<string name="tasker_start_service">ره وندن خدمات</string>
|
||||||
<string name="tasker_setting_confirm">قوۊل</string>
|
<string name="tasker_setting_confirm">قوۊل</string>
|
||||||
|
|
||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">نشقه دامنه</string>
|
<string name="routing_settings_domain_strategy">نشقه دامنه</string>
|
||||||
<string name="routing_settings_title">سامووا تور جوستن</string>
|
<string name="routing_settings_title">سامووا تور جوستن</string>
|
||||||
<string name="routing_settings_tips">وا کاما ز یک جوڌا ابۊن (،) پسند دامنه یا آی پی</string>
|
<string name="routing_settings_tips">وا کاما ز یک جوڌا ابۊن (،)؛ یکی را وارد کنید: domain، ip یا process</string>
|
||||||
<string name="routing_settings_save">زفت کردن</string>
|
<string name="routing_settings_save">زفت کردن</string>
|
||||||
<string name="routing_settings_delete">روفتن</string>
|
<string name="routing_settings_delete">روفتن</string>
|
||||||
<string name="routing_settings_rule_title">سامووا قانۉݩ تور جوستن</string>
|
<string name="routing_settings_rule_title">سامووا قانۉݩ تور جوستن</string>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">افزایش پهنای باند (واحد)</string>
|
<string name="server_lab_bandwidth_up">افزایش پهنای باند (واحد)</string>
|
||||||
<string name="server_lab_xhttp_mode">حالت XHTTP</string>
|
<string name="server_lab_xhttp_mode">حالت XHTTP</string>
|
||||||
<string name="server_lab_xhttp_extra">خام JSON XHTTP Extra، قالب: { XHTTPObject }</string>
|
<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_config_list">لیست کانفیگ Ech</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -183,8 +184,11 @@
|
|||||||
<string name="title_pref_fake_dns_enabled">فعال کردن DNS جعلی</string>
|
<string name="title_pref_fake_dns_enabled">فعال کردن DNS جعلی</string>
|
||||||
<string name="summary_pref_fake_dns_enabled">دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند)</string>
|
<string name="summary_pref_fake_dns_enabled">دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند)</string>
|
||||||
|
|
||||||
|
<string name="title_pref_ipv6_enabled">فعال کردن IPv6</string>
|
||||||
|
<string name="summary_pref_ipv6_enabled">آدرس و مسیرهای IPv6 را به رابط VPN اضافه کنید</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">ترجیح دادن IPV6</string>
|
<string name="title_pref_prefer_ipv6">ترجیح دادن IPV6</string>
|
||||||
<string name="summary_pref_prefer_ipv6">مسیرهای 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="title_pref_remote_dns">DNS از راه دور (اختیاری) (udp/tcp/https/quic)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
@@ -216,6 +220,12 @@
|
|||||||
|
|
||||||
<string name="title_pref_socks_port">پورت پروکسی محلی</string>
|
<string name="title_pref_socks_port">پورت پروکسی محلی</string>
|
||||||
<string name="summary_pref_socks_port">پورت پروکسی محلی</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_local_dns_port">پورت DNS محلی</string>
|
<string name="title_pref_local_dns_port">پورت DNS محلی</string>
|
||||||
<string name="summary_pref_local_dns_port">پورت DNS محلی</string>
|
<string name="summary_pref_local_dns_port">پورت DNS محلی</string>
|
||||||
@@ -241,6 +251,7 @@
|
|||||||
<string name="toast_tg_app_not_found">برنامه تلگرام پیدا نشد</string>
|
<string name="toast_tg_app_not_found">برنامه تلگرام پیدا نشد</string>
|
||||||
|
|
||||||
<string name="title_privacy_policy">حریم خصوصی</string>
|
<string name="title_privacy_policy">حریم خصوصی</string>
|
||||||
|
<string name="title_qr_code">QR code</string>
|
||||||
<string name="title_about">درباره</string>
|
<string name="title_about">درباره</string>
|
||||||
<string name="title_source_code">کد منبع</string>
|
<string name="title_source_code">کد منبع</string>
|
||||||
<string name="title_oss_license">مجوز های منبع باز</string>
|
<string name="title_oss_license">مجوز های منبع باز</string>
|
||||||
@@ -296,13 +307,17 @@
|
|||||||
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
|
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
|
||||||
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||||
<string name="title_update_subscription_no_subscription">No subscriptions</string>
|
<string name="title_update_subscription_no_subscription">No subscriptions</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="tasker_start_service">شروع خدمات</string>
|
<string name="tasker_start_service">شروع خدمات</string>
|
||||||
<string name="tasker_setting_confirm">تایید</string>
|
<string name="tasker_setting_confirm">تایید</string>
|
||||||
|
|
||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">استراتژی دامنه</string>
|
<string name="routing_settings_domain_strategy">استراتژی دامنه</string>
|
||||||
<string name="routing_settings_title">تنظیمات مسیریابی</string>
|
<string name="routing_settings_title">تنظیمات مسیریابی</string>
|
||||||
<string name="routing_settings_tips">با کاما (،) از هم جدا شوند، ذخیره کردن فراموش نشود</string>
|
<string name="routing_settings_tips">با کاما (,) جدا کنید. یکی از موارد domain، ip یا process را وارد کنید</string>
|
||||||
<string name="routing_settings_save">ذخیره</string>
|
<string name="routing_settings_save">ذخیره</string>
|
||||||
<string name="routing_settings_delete">حذف</string>
|
<string name="routing_settings_delete">حذف</string>
|
||||||
<string name="routing_settings_rule_title">تنظیمات قانون مسیریابی</string>
|
<string name="routing_settings_rule_title">تنظیمات قانون مسیریابی</string>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">Режим XHTTP</string>
|
<string name="server_lab_xhttp_mode">Режим XHTTP</string>
|
||||||
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
|
<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_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
|
||||||
@@ -184,8 +185,11 @@
|
|||||||
<string name="title_pref_fake_dns_enabled">Использовать поддельную DNS</string>
|
<string name="title_pref_fake_dns_enabled">Использовать поддельную DNS</string>
|
||||||
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
|
<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-адрес и маршруты в VPN-интерфейс</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
|
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
|
||||||
<string name="summary_pref_prefer_ipv6">Использовать маршрутизацию 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="title_pref_remote_dns">Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
@@ -217,6 +221,12 @@
|
|||||||
|
|
||||||
<string name="title_pref_socks_port">Порт локального прокси</string>
|
<string name="title_pref_socks_port">Порт локального прокси</string>
|
||||||
<string name="summary_pref_socks_port">Порт локального прокси</string>
|
<string name="summary_pref_socks_port">Порт локального прокси</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="title_pref_local_dns_port">Порт локальной DNS</string>
|
||||||
<string name="summary_pref_local_dns_port">Порт локальной DNS</string>
|
<string name="summary_pref_local_dns_port">Порт локальной DNS</string>
|
||||||
@@ -241,6 +251,7 @@
|
|||||||
<string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string>
|
<string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string>
|
||||||
<string name="toast_tg_app_not_found">Приложение Telegram не найдено</string>
|
<string name="toast_tg_app_not_found">Приложение Telegram не найдено</string>
|
||||||
<string name="title_privacy_policy">Политика конфиденциальности</string>
|
<string name="title_privacy_policy">Политика конфиденциальности</string>
|
||||||
|
<string name="title_qr_code">QR-код</string>
|
||||||
<string name="title_about">О приложении</string>
|
<string name="title_about">О приложении</string>
|
||||||
<string name="title_source_code">Исходный код</string>
|
<string name="title_source_code">Исходный код</string>
|
||||||
<string name="title_oss_license">Лицензии открытого исходного кода</string>
|
<string name="title_oss_license">Лицензии открытого исходного кода</string>
|
||||||
@@ -297,6 +308,9 @@
|
|||||||
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
<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_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
|
||||||
<string name="title_update_subscription_no_subscription">Нет подписок</string>
|
<string name="title_update_subscription_no_subscription">Нет подписок</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_start_service">Запуск службы</string>
|
||||||
<string name="tasker_setting_confirm">Подтвердить</string>
|
<string name="tasker_setting_confirm">Подтвердить</string>
|
||||||
@@ -304,7 +318,7 @@
|
|||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">Доменная стратегия</string>
|
<string name="routing_settings_domain_strategy">Доменная стратегия</string>
|
||||||
<string name="routing_settings_title">Маршрутизация</string>
|
<string name="routing_settings_title">Маршрутизация</string>
|
||||||
<string name="routing_settings_tips">Введите требуемые домены/IP через запятую</string>
|
<string name="routing_settings_tips">Разделяйте запятой (,). Укажите только одно: domain, ip или process</string>
|
||||||
<string name="routing_settings_save">Сохранить</string>
|
<string name="routing_settings_save">Сохранить</string>
|
||||||
<string name="routing_settings_delete">Очистить</string>
|
<string name="routing_settings_delete">Очистить</string>
|
||||||
<string name="routing_settings_rule_title">Настройка правил маршрутизации</string>
|
<string name="routing_settings_rule_title">Настройка правил маршрутизации</string>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
<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_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -185,8 +186,11 @@
|
|||||||
<string name="title_pref_fake_dns_enabled">Bật FakeDNS</string>
|
<string name="title_pref_fake_dns_enabled">Bật FakeDNS</string>
|
||||||
<string name="summary_pref_fake_dns_enabled">FakeDNS lấy tên miền mục tiêu bằng cách giả mạo DNS (Nhanh hơn, nhưng có thể không hoạt động cho một số ứng dụng).</string>
|
<string name="summary_pref_fake_dns_enabled">FakeDNS lấy tên miền mục tiêu bằng cách giả mạo DNS (Nhanh hơn, nhưng có thể không hoạt động cho một số ứng dụng).</string>
|
||||||
|
|
||||||
|
<string name="title_pref_ipv6_enabled">Bật IPv6</string>
|
||||||
|
<string name="summary_pref_ipv6_enabled">Thêm địa chỉ và tuyến IPv6 vào giao diện VPN</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">Ưu tiên IPv6</string>
|
<string name="title_pref_prefer_ipv6">Ưu tiên IPv6</string>
|
||||||
<string name="summary_pref_prefer_ipv6">Ưu tiên sử dụng địa chỉ IPv6 cho kết nối và định tuyến.</string>
|
<string name="summary_pref_prefer_ipv6">Ưu tiên địa chỉ IPv6 khi phân giải tên miền</string>
|
||||||
|
|
||||||
<string name="title_pref_remote_dns">DNS ngoại quốc (UDP / TCP / HTTPS / QUIC) (Không bắt buộc)</string>
|
<string name="title_pref_remote_dns">DNS ngoại quốc (UDP / TCP / HTTPS / QUIC) (Không bắt buộc)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
@@ -218,6 +222,12 @@
|
|||||||
|
|
||||||
<string name="title_pref_socks_port">Cổng Proxy Local</string>
|
<string name="title_pref_socks_port">Cổng Proxy Local</string>
|
||||||
<string name="summary_pref_socks_port">Cổng Proxy Local</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>
|
||||||
|
<string name="title_pref_socks_username">Tên người dùng Proxy Local (Không bắt buộc)</string>
|
||||||
|
<string name="summary_pref_socks_username">Tên người dùng</string>
|
||||||
|
<string name="title_pref_socks_password">Mật khẩu Proxy Local (Không bắt buộc)</string>
|
||||||
|
<string name="summary_pref_socks_password">Mật khẩu</string>
|
||||||
|
|
||||||
<string name="title_pref_local_dns_port">Cổng Local DNS</string>
|
<string name="title_pref_local_dns_port">Cổng Local DNS</string>
|
||||||
<string name="summary_pref_local_dns_port">Cổng Local DNS</string>
|
<string name="summary_pref_local_dns_port">Cổng Local DNS</string>
|
||||||
@@ -243,6 +253,7 @@
|
|||||||
<string name="toast_tg_app_not_found">Không tìm thấy ứng dụng Telegram!</string>
|
<string name="toast_tg_app_not_found">Không tìm thấy ứng dụng Telegram!</string>
|
||||||
|
|
||||||
<string name="title_privacy_policy">Chính sách bảo mật</string>
|
<string name="title_privacy_policy">Chính sách bảo mật</string>
|
||||||
|
<string name="title_qr_code">Mã QR</string>
|
||||||
<string name="title_about">Giới thiệu</string>
|
<string name="title_about">Giới thiệu</string>
|
||||||
<string name="title_source_code">Mã nguồn</string>
|
<string name="title_source_code">Mã nguồn</string>
|
||||||
<string name="title_oss_license">Open Source licenses</string>
|
<string name="title_oss_license">Open Source licenses</string>
|
||||||
@@ -299,13 +310,17 @@
|
|||||||
<string name="title_update_config_count">Update %d configurations</string>
|
<string name="title_update_config_count">Update %d configurations</string>
|
||||||
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||||
<string name="title_update_subscription_no_subscription">No subscriptions</string>
|
<string name="title_update_subscription_no_subscription">No subscriptions</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="tasker_start_service">Khởi động v2rayNG</string>
|
<string name="tasker_start_service">Khởi động v2rayNG</string>
|
||||||
<string name="tasker_setting_confirm">Xác nhận</string>
|
<string name="tasker_setting_confirm">Xác nhận</string>
|
||||||
|
|
||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">Chiến lược tên miền (DomainStrategy)</string>
|
<string name="routing_settings_domain_strategy">Chiến lược tên miền (DomainStrategy)</string>
|
||||||
<string name="routing_settings_title">Cài đặt định tuyến</string>
|
<string name="routing_settings_title">Cài đặt định tuyến</string>
|
||||||
<string name="routing_settings_tips">Phân cách bằng dấu phẩy (,). Có thể tải xuống Rules mặc định để tham khảo ở menu ba chấm.</string>
|
<string name="routing_settings_tips">Phân cách bằng dấu phẩy (,). Chọn một trong: domain, ip hoặc process</string>
|
||||||
<string name="routing_settings_save">Lưu lại</string>
|
<string name="routing_settings_save">Lưu lại</string>
|
||||||
<string name="routing_settings_delete">Xoá</string>
|
<string name="routing_settings_delete">Xoá</string>
|
||||||
<string name="routing_settings_rule_title">Routing Rule Settings</string>
|
<string name="routing_settings_rule_title">Routing Rule Settings</string>
|
||||||
@@ -316,6 +331,15 @@
|
|||||||
<string name="routing_settings_import_rulesets_from_qrcode">Nhập bộ quy tắc từ QRcode</string>
|
<string name="routing_settings_import_rulesets_from_qrcode">Nhập bộ quy tắc từ QRcode</string>
|
||||||
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
|
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
|
||||||
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
|
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
|
||||||
|
<string name="routing_settings_domain">domain</string>
|
||||||
|
<string name="routing_settings_ip">ip</string>
|
||||||
|
<string name="routing_settings_process">process (Package Name — only supported when using Xray TUN, routeOnly enabled, and Android 10+)</string>
|
||||||
|
<string name="routing_settings_port">port</string>
|
||||||
|
<string name="routing_settings_protocol">protocol</string>
|
||||||
|
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
||||||
|
<string name="routing_settings_network">network</string>
|
||||||
|
<string name="routing_settings_network_tip">[udp|tcp]</string>
|
||||||
|
<string name="routing_settings_outbound_tag">outboundTag</string>
|
||||||
|
|
||||||
<string name="connection_test_pending">Kiểm tra kết nối</string>
|
<string name="connection_test_pending">Kiểm tra kết nối</string>
|
||||||
<string name="connection_test_testing">Đang kiểm tra kết nối mạng...</string>
|
<string name="connection_test_testing">Đang kiểm tra kết nối mạng...</string>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
<string name="toast_permission_denied">无法取得权限</string>
|
<string name="toast_permission_denied">无法取得权限</string>
|
||||||
<string name="toast_permission_denied_notification">无法取得通知权限</string>
|
<string name="toast_permission_denied_notification">无法取得通知权限</string>
|
||||||
<string name="notification_action_more">点击了解更多</string>
|
<string name="notification_action_more">点击了解更多</string>
|
||||||
<string name="toast_services_start">启动服务中</string>
|
<string name="toast_services_start">启动服务</string>
|
||||||
<string name="toast_services_stop">关闭中</string>
|
<string name="toast_services_stop">关闭服务</string>
|
||||||
<string name="toast_services_success">启动服务成功</string>
|
<string name="toast_services_success">启动服务成功</string>
|
||||||
<string name="toast_services_failure">启动服务失败</string>
|
<string name="toast_services_failure">启动服务失败</string>
|
||||||
|
|
||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">带宽上行 (支持的单位 k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">带宽上行 (支持的单位 k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
|
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
|
<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_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
|
||||||
@@ -182,8 +183,11 @@
|
|||||||
<string name="title_pref_fake_dns_enabled">启用虚拟 DNS</string>
|
<string name="title_pref_fake_dns_enabled">启用虚拟 DNS</string>
|
||||||
<string name="summary_pref_fake_dns_enabled">本地返回虚构解析结果 (减低延时 但个别应用可能无法使用)</string>
|
<string name="summary_pref_fake_dns_enabled">本地返回虚构解析结果 (减低延时 但个别应用可能无法使用)</string>
|
||||||
|
|
||||||
|
<string name="title_pref_ipv6_enabled">启用 IPv6</string>
|
||||||
|
<string name="summary_pref_ipv6_enabled">为 VPN 接口添加 IPv6 地址和路由</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">IPv6 优先</string>
|
<string name="title_pref_prefer_ipv6">IPv6 优先</string>
|
||||||
<string name="summary_pref_prefer_ipv6">App 优先使用 IPv6 地址连接服务器, 同时开启 VPN 的 IPv6 路由</string>
|
<string name="summary_pref_prefer_ipv6">解析域名时优先使用 IPv6 地址</string>
|
||||||
|
|
||||||
<string name="title_pref_remote_dns">远程 DNS (udp/tcp/https/quic)(可选)</string>
|
<string name="title_pref_remote_dns">远程 DNS (udp/tcp/https/quic)(可选)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
@@ -213,10 +217,16 @@
|
|||||||
<string name="title_pref_allow_insecure">跳过证书验证 (allowInsecure)</string>
|
<string name="title_pref_allow_insecure">跳过证书验证 (allowInsecure)</string>
|
||||||
<string name="summary_pref_allow_insecure">传输层安全选 tls 时,默认跳过证书验证 (allowInsecure)</string>
|
<string name="summary_pref_allow_insecure">传输层安全选 tls 时,默认跳过证书验证 (allowInsecure)</string>
|
||||||
|
|
||||||
<string name="title_pref_socks_port">本地代理端口</string>
|
<string name="title_pref_socks_port">本地代理端口</string>
|
||||||
<string name="summary_pref_socks_port">本地代理端口</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>
|
||||||
|
<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="title_pref_local_dns_port">本地 DNS 端口</string>
|
||||||
<string name="summary_pref_local_dns_port">本地 DNS 端口</string>
|
<string name="summary_pref_local_dns_port">本地 DNS 端口</string>
|
||||||
|
|
||||||
<string name="title_pref_confirm_remove">删除配置确认</string>
|
<string name="title_pref_confirm_remove">删除配置确认</string>
|
||||||
@@ -240,6 +250,7 @@
|
|||||||
<string name="summary_pref_tg_group">加入 Telegram Group</string>
|
<string name="summary_pref_tg_group">加入 Telegram Group</string>
|
||||||
<string name="toast_tg_app_not_found">未找到 Telegram app</string>
|
<string name="toast_tg_app_not_found">未找到 Telegram app</string>
|
||||||
<string name="title_privacy_policy">隐私权政策</string>
|
<string name="title_privacy_policy">隐私权政策</string>
|
||||||
|
<string name="title_qr_code">二维码</string>
|
||||||
<string name="title_about">关于</string>
|
<string name="title_about">关于</string>
|
||||||
<string name="title_source_code">源代码</string>
|
<string name="title_source_code">源代码</string>
|
||||||
<string name="title_oss_license">Open Source licenses</string>
|
<string name="title_oss_license">Open Source licenses</string>
|
||||||
@@ -297,13 +308,17 @@
|
|||||||
<string name="title_update_config_count">更新 %d 个配置</string>
|
<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_result">更新了 %1$d 个配置(%2$d 个成功,%3$d 个失败,%4$d 个跳过)</string>
|
||||||
<string name="title_update_subscription_no_subscription">无订阅</string>
|
<string name="title_update_subscription_no_subscription">无订阅</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_start_service">启动服务</string>
|
||||||
<string name="tasker_setting_confirm">确定</string>
|
<string name="tasker_setting_confirm">确定</string>
|
||||||
|
|
||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">域名策略</string>
|
<string name="routing_settings_domain_strategy">域名策略</string>
|
||||||
<string name="routing_settings_title">路由设置</string>
|
<string name="routing_settings_title">路由设置</string>
|
||||||
<string name="routing_settings_tips">用逗号(,)隔开, domain 和 ip 二选一填写</string>
|
<string name="routing_settings_tips">用逗号(,)分隔,选择填写 domain、ip 或 process 中的一项</string>
|
||||||
<string name="routing_settings_save">保存</string>
|
<string name="routing_settings_save">保存</string>
|
||||||
<string name="routing_settings_delete">清空</string>
|
<string name="routing_settings_delete">清空</string>
|
||||||
<string name="routing_settings_rule_title">路由规则设置</string>
|
<string name="routing_settings_rule_title">路由规则设置</string>
|
||||||
@@ -314,6 +329,15 @@
|
|||||||
<string name="routing_settings_import_rulesets_from_qrcode">从 QRcode 导入规则集</string>
|
<string name="routing_settings_import_rulesets_from_qrcode">从 QRcode 导入规则集</string>
|
||||||
<string name="routing_settings_export_rulesets_to_clipboard">导出规则集至剪贴板</string>
|
<string name="routing_settings_export_rulesets_to_clipboard">导出规则集至剪贴板</string>
|
||||||
<string name="routing_settings_locked">锁定中,导入预设时不删除此规则</string>
|
<string name="routing_settings_locked">锁定中,导入预设时不删除此规则</string>
|
||||||
|
<string name="routing_settings_domain">domain</string>
|
||||||
|
<string name="routing_settings_ip">ip</string>
|
||||||
|
<string name="routing_settings_process">process (包名 — 仅在使用 Xray 的 TUN、开启 routeOnly 且 Android 10+ 时可用)</string>
|
||||||
|
<string name="routing_settings_port">port</string>
|
||||||
|
<string name="routing_settings_protocol">protocol</string>
|
||||||
|
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
||||||
|
<string name="routing_settings_network">network</string>
|
||||||
|
<string name="routing_settings_network_tip">[udp|tcp]</string>
|
||||||
|
<string name="routing_settings_outbound_tag">outboundTag</string>
|
||||||
|
|
||||||
<string name="connection_test_pending">"检查网络连接"</string>
|
<string name="connection_test_pending">"检查网络连接"</string>
|
||||||
<string name="connection_test_testing">"测试中…"</string>
|
<string name="connection_test_testing">"测试中…"</string>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">頻寬上行 (支持的單位 k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">頻寬上行 (支持的單位 k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
|
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
|
<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_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string>
|
||||||
@@ -184,8 +185,11 @@
|
|||||||
<string name="title_pref_fake_dns_enabled">啟用假 DNS</string>
|
<string name="title_pref_fake_dns_enabled">啟用假 DNS</string>
|
||||||
<string name="summary_pref_fake_dns_enabled">本機退回假解析結果 (減低延時,但個別應用可能無法使用)</string>
|
<string name="summary_pref_fake_dns_enabled">本機退回假解析結果 (減低延時,但個別應用可能無法使用)</string>
|
||||||
|
|
||||||
|
<string name="title_pref_ipv6_enabled">啟用 IPv6</string>
|
||||||
|
<string name="summary_pref_ipv6_enabled">為 VPN 介面新增 IPv6 位址和路由</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">IPv6 偏好</string>
|
<string name="title_pref_prefer_ipv6">IPv6 偏好</string>
|
||||||
<string name="summary_pref_prefer_ipv6">App 優先使用 IPv6 位址連線伺服器,同時開啟 VPN 的 IPv6 路由</string>
|
<string name="summary_pref_prefer_ipv6">解析網域名稱時優先使用 IPv6 位址</string>
|
||||||
|
|
||||||
<string name="title_pref_remote_dns">遠端 DNS (udp/tcp/https/quic)(可選)</string>
|
<string name="title_pref_remote_dns">遠端 DNS (udp/tcp/https/quic)(可選)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
@@ -216,6 +220,12 @@
|
|||||||
|
|
||||||
<string name="title_pref_socks_port">本地代理埠</string>
|
<string name="title_pref_socks_port">本地代理埠</string>
|
||||||
<string name="summary_pref_socks_port">本地代理埠</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>
|
||||||
|
<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="title_pref_local_dns_port">本機 DNS 埠</string>
|
||||||
<string name="summary_pref_local_dns_port">本機 DNS 埠</string>
|
<string name="summary_pref_local_dns_port">本機 DNS 埠</string>
|
||||||
@@ -241,6 +251,7 @@
|
|||||||
<string name="summary_pref_tg_group">加入 Telegram 群組</string>
|
<string name="summary_pref_tg_group">加入 Telegram 群組</string>
|
||||||
<string name="toast_tg_app_not_found">未找到 Telegram 應用程式</string>
|
<string name="toast_tg_app_not_found">未找到 Telegram 應用程式</string>
|
||||||
<string name="title_privacy_policy">隱私權政策</string>
|
<string name="title_privacy_policy">隱私權政策</string>
|
||||||
|
<string name="title_qr_code">QR 碼</string>
|
||||||
<string name="title_about">關於</string>
|
<string name="title_about">關於</string>
|
||||||
<string name="title_source_code">原始碼</string>
|
<string name="title_source_code">原始碼</string>
|
||||||
<string name="title_oss_license">Open Source licenses</string>
|
<string name="title_oss_license">Open Source licenses</string>
|
||||||
@@ -297,13 +308,17 @@
|
|||||||
<string name="title_update_config_count">更新 %d 個配置</string>
|
<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_result">更新了 %1$d 個配置(%2$d 個成功,%3$d 個失敗,%4$d 個跳過)</string>
|
||||||
<string name="title_update_subscription_no_subscription">無訂閱</string>
|
<string name="title_update_subscription_no_subscription">無訂閱</string>
|
||||||
|
<string name="toast_server_not_found_in_group">當前分組中未找到選中的伺服器</string>
|
||||||
|
<string name="toast_fragment_not_available">定位所選配置</string>
|
||||||
|
<string name="title_locate_selected_config">Locate the selected config</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">啟動服務</string>
|
<string name="tasker_start_service">啟動服務</string>
|
||||||
<string name="tasker_setting_confirm">確定</string>
|
<string name="tasker_setting_confirm">確定</string>
|
||||||
|
|
||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">網域策略</string>
|
<string name="routing_settings_domain_strategy">網域策略</string>
|
||||||
<string name="routing_settings_title">轉送設定</string>
|
<string name="routing_settings_title">轉送設定</string>
|
||||||
<string name="routing_settings_tips">以半形逗號「,」分隔,domain和ip二選一填寫</string>
|
<string name="routing_settings_tips">用逗號(,)分隔,選擇填寫 domain、ip 或 process 中的一項</string>
|
||||||
<string name="routing_settings_save">儲存</string>
|
<string name="routing_settings_save">儲存</string>
|
||||||
<string name="routing_settings_delete">清除</string>
|
<string name="routing_settings_delete">清除</string>
|
||||||
<string name="routing_settings_rule_title">路由規則設定</string>
|
<string name="routing_settings_rule_title">路由規則設定</string>
|
||||||
@@ -314,6 +329,15 @@
|
|||||||
<string name="routing_settings_import_rulesets_from_qrcode">從二維碼匯入規則集</string>
|
<string name="routing_settings_import_rulesets_from_qrcode">從二維碼匯入規則集</string>
|
||||||
<string name="routing_settings_export_rulesets_to_clipboard">匯出規則集至剪貼簿</string>
|
<string name="routing_settings_export_rulesets_to_clipboard">匯出規則集至剪貼簿</string>
|
||||||
<string name="routing_settings_locked">鎖定中,匯入預設時不刪除此規則</string>
|
<string name="routing_settings_locked">鎖定中,匯入預設時不刪除此規則</string>
|
||||||
|
<string name="routing_settings_domain">domain</string>
|
||||||
|
<string name="routing_settings_ip">ip</string>
|
||||||
|
<string name="routing_settings_process">process (套件名稱 — 僅在使用 Xray 的 TUN、開啟 routeOnly 且 Android 10+ 時可用)</string>
|
||||||
|
<string name="routing_settings_port">port</string>
|
||||||
|
<string name="routing_settings_protocol">protocol</string>
|
||||||
|
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
||||||
|
<string name="routing_settings_network">network</string>
|
||||||
|
<string name="routing_settings_network_tip">[udp|tcp]</string>
|
||||||
|
<string name="routing_settings_outbound_tag">outboundTag</string>
|
||||||
|
|
||||||
<string name="connection_test_pending">"測試連線能力"</string>
|
<string name="connection_test_pending">"測試連線能力"</string>
|
||||||
<string name="connection_test_testing">"測試中……"</string>
|
<string name="connection_test_testing">"測試中……"</string>
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
<dimen name="view_height_dp36">36dp</dimen>
|
<dimen name="view_height_dp36">36dp</dimen>
|
||||||
<dimen name="view_height_dp48">48dp</dimen>
|
<dimen name="view_height_dp48">48dp</dimen>
|
||||||
<dimen name="view_height_dp64">64dp</dimen>
|
<dimen name="view_height_dp64">64dp</dimen>
|
||||||
|
<dimen name="view_height_dp120">120dp</dimen>
|
||||||
<dimen name="view_height_dp160">160dp</dimen>
|
<dimen name="view_height_dp160">160dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<string name="server_lab_path_kcp">kcp seed</string>
|
<string name="server_lab_path_kcp">kcp seed</string>
|
||||||
<string name="server_lab_path_grpc">gRPC serviceName</string>
|
<string name="server_lab_path_grpc">gRPC serviceName</string>
|
||||||
<string name="server_lab_stream_security">TLS</string>
|
<string name="server_lab_stream_security">TLS</string>
|
||||||
<string name="server_lab_stream_fingerprint" translatable="false">Fingerprint</string>
|
<string name="server_lab_stream_fingerprint">Fingerprint</string>
|
||||||
<string name="server_lab_stream_alpn">Alpn</string>
|
<string name="server_lab_stream_alpn">Alpn</string>
|
||||||
<string name="server_lab_allow_insecure">allowInsecure</string>
|
<string name="server_lab_allow_insecure">allowInsecure</string>
|
||||||
<string name="server_lab_sni">SNI</string>
|
<string name="server_lab_sni">SNI</string>
|
||||||
@@ -113,6 +113,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
<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_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -186,8 +187,11 @@
|
|||||||
<string name="title_pref_fake_dns_enabled">Enable fake DNS</string>
|
<string name="title_pref_fake_dns_enabled">Enable fake DNS</string>
|
||||||
<string name="summary_pref_fake_dns_enabled">Local DNS returns fake IP addresses (faster, but it may not work for some apps)</string>
|
<string name="summary_pref_fake_dns_enabled">Local DNS returns fake IP addresses (faster, but it may not work for some apps)</string>
|
||||||
|
|
||||||
|
<string name="title_pref_ipv6_enabled">Enable IPv6</string>
|
||||||
|
<string name="summary_pref_ipv6_enabled">Add IPv6 address and routes to the VPN interface</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">Prefer IPv6</string>
|
<string name="title_pref_prefer_ipv6">Prefer IPv6</string>
|
||||||
<string name="summary_pref_prefer_ipv6">Enable IPv6 routes and Prefer IPv6 addresses</string>
|
<string name="summary_pref_prefer_ipv6">Prefer IPv6 addresses when resolving domain names</string>
|
||||||
|
|
||||||
<string name="title_pref_remote_dns">Remote DNS (udp/tcp/https/quic)(Optional)</string>
|
<string name="title_pref_remote_dns">Remote DNS (udp/tcp/https/quic)(Optional)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
@@ -221,6 +225,12 @@
|
|||||||
|
|
||||||
<string name="title_pref_socks_port">Local proxy port</string>
|
<string name="title_pref_socks_port">Local proxy port</string>
|
||||||
<string name="summary_pref_socks_port">Local proxy port</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>
|
||||||
|
<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_local_dns_port">Local DNS port</string>
|
<string name="title_pref_local_dns_port">Local DNS port</string>
|
||||||
<string name="summary_pref_local_dns_port">Local DNS port</string>
|
<string name="summary_pref_local_dns_port">Local DNS port</string>
|
||||||
@@ -303,6 +313,9 @@
|
|||||||
<string name="title_update_config_count">Update %d configs</string>
|
<string name="title_update_config_count">Update %d configs</string>
|
||||||
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||||
<string name="title_update_subscription_no_subscription">No subscriptions</string>
|
<string name="title_update_subscription_no_subscription">No subscriptions</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="tasker_start_service">Start Service</string>
|
<string name="tasker_start_service">Start Service</string>
|
||||||
<string name="tasker_setting_confirm">Confirm</string>
|
<string name="tasker_setting_confirm">Confirm</string>
|
||||||
@@ -310,7 +323,7 @@
|
|||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">Domain strategy</string>
|
<string name="routing_settings_domain_strategy">Domain strategy</string>
|
||||||
<string name="routing_settings_title">Routing Settings</string>
|
<string name="routing_settings_title">Routing Settings</string>
|
||||||
<string name="routing_settings_tips">Separated by commas(,), choose domain or ip</string>
|
<string name="routing_settings_tips">Separated by commas(,), \nchoose domain or ip or process</string>
|
||||||
<string name="routing_settings_save">Save</string>
|
<string name="routing_settings_save">Save</string>
|
||||||
<string name="routing_settings_delete">Clear</string>
|
<string name="routing_settings_delete">Clear</string>
|
||||||
<string name="routing_settings_rule_title">Routing Rule Settings</string>
|
<string name="routing_settings_rule_title">Routing Rule Settings</string>
|
||||||
@@ -321,14 +334,16 @@
|
|||||||
<string name="routing_settings_import_rulesets_from_qrcode">Import ruleset from QRcode</string>
|
<string name="routing_settings_import_rulesets_from_qrcode">Import ruleset from QRcode</string>
|
||||||
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
|
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
|
||||||
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
|
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
|
||||||
<string name="routing_settings_domain" translatable="false">domain</string>
|
<string name="routing_settings_domain">domain</string>
|
||||||
<string name="routing_settings_ip" translatable="false">ip</string>
|
<string name="routing_settings_ip">ip</string>
|
||||||
<string name="routing_settings_port" translatable="false">port</string>
|
<string name="routing_settings_process">process (Package Name — only supported when using Xray TUN, routeOnly enabled, and Android 10+)</string>
|
||||||
<string name="routing_settings_protocol" translatable="false">protocol</string>
|
<string name="routing_settings_port">port</string>
|
||||||
<string name="routing_settings_protocol_tip" translatable="false">[http,tls,bittorrent]</string>
|
<string name="routing_settings_protocol">protocol</string>
|
||||||
<string name="routing_settings_network" translatable="false">network</string>
|
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
||||||
<string name="routing_settings_network_tip" translatable="false">[udp|tcp]</string>
|
<string name="routing_settings_network">network</string>
|
||||||
<string name="routing_settings_outbound_tag" translatable="false">outboundTag</string>
|
<string name="routing_settings_network_tip">[udp|tcp]</string>
|
||||||
|
<string name="routing_settings_outbound_tag_hint" translatable="false">proxy / direct / block / <remarks></string>
|
||||||
|
<string name="routing_settings_outbound_tag">outboundTag</string>
|
||||||
|
|
||||||
<string name="connection_test_pending">Check Connectivity</string>
|
<string name="connection_test_pending">Check Connectivity</string>
|
||||||
<string name="connection_test_testing">Testing…</string>
|
<string name="connection_test_testing">Testing…</string>
|
||||||
|
|||||||
@@ -47,6 +47,11 @@
|
|||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/title_vpn_settings">
|
<PreferenceCategory android:title="@string/title_vpn_settings">
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:key="pref_ipv6_enabled"
|
||||||
|
android:summary="@string/summary_pref_ipv6_enabled"
|
||||||
|
android:title="@string/title_pref_ipv6_enabled" />
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:key="pref_prefer_ipv6"
|
android:key="pref_prefer_ipv6"
|
||||||
android:summary="@string/summary_pref_prefer_ipv6"
|
android:summary="@string/summary_pref_prefer_ipv6"
|
||||||
@@ -157,6 +162,23 @@
|
|||||||
android:summary="10808"
|
android:summary="10808"
|
||||||
android:title="@string/title_pref_socks_port" />
|
android:title="@string/title_pref_socks_port" />
|
||||||
|
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="pref_dynamic_socks_port"
|
||||||
|
android:summary="@string/summary_pref_dynamic_socks_port"
|
||||||
|
android:title="@string/title_pref_dynamic_socks_port" />
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:key="pref_socks_username"
|
||||||
|
android:summary="@string/summary_pref_socks_username"
|
||||||
|
android:title="@string/title_pref_socks_username" />
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:key="pref_socks_password"
|
||||||
|
android:summary="@string/summary_pref_socks_password"
|
||||||
|
android:title="@string/title_pref_socks_password" />
|
||||||
|
|
||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
android:key="pref_remote_dns"
|
android:key="pref_remote_dns"
|
||||||
android:summary="@string/summary_pref_remote_dns"
|
android:summary="@string/summary_pref_remote_dns"
|
||||||
|
|||||||
@@ -28,4 +28,32 @@
|
|||||||
android:targetPackage="com.v2ray.ang" />
|
android:targetPackage="com.v2ray.ang" />
|
||||||
<categories android:name="android.shortcut.conversation" />
|
<categories android:name="android.shortcut.conversation" />
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_qu_start_24dp"
|
||||||
|
android:shortcutDisabledMessage="@string/toast_services_start"
|
||||||
|
android:shortcutId="shortcuts_start"
|
||||||
|
android:shortcutLongLabel="@string/toast_services_start"
|
||||||
|
android:shortcutShortLabel="@string/toast_services_start">
|
||||||
|
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetClass="com.v2ray.ang.ui.ScStartActivity"
|
||||||
|
android:targetPackage="com.v2ray.ang" />
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_qu_stop_24dp"
|
||||||
|
android:shortcutDisabledMessage="@string/toast_services_stop"
|
||||||
|
android:shortcutId="shortcuts_stop"
|
||||||
|
android:shortcutLongLabel="@string/toast_services_stop"
|
||||||
|
android:shortcutShortLabel="@string/toast_services_stop">
|
||||||
|
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetClass="com.v2ray.ang.ui.ScStopActivity"
|
||||||
|
android:targetPackage="com.v2ray.ang" />
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
</shortcuts>
|
</shortcuts>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.v2ray.ang.fmt
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import com.v2ray.ang.util.LogUtil
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user