Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55bc2bf934 | ||
|
|
f22454da5d | ||
|
|
4a87549fa7 | ||
|
|
d447adc97f | ||
|
|
3773962b64 | ||
|
|
be0a2506ce | ||
|
|
7f9cb8dfdd | ||
|
|
71a5b6e480 | ||
|
|
02e53ced50 | ||
|
|
42c27a5e7e | ||
|
|
af04bbcf87 | ||
|
|
9bedfe8a7b | ||
|
|
2fdf684ee7 | ||
|
|
5b79951da7 | ||
|
|
06aa680d45 | ||
|
|
cdb9b1811c | ||
|
|
0fc1f2f5d3 | ||
|
|
ef1bb3dd34 | ||
|
|
1bca321d3f | ||
|
|
247e2b3ba3 | ||
|
|
41fd2b0cfb | ||
|
|
72da42ee40 | ||
|
|
c130d55e8f | ||
|
|
5ae84f7eac | ||
|
|
df5ea251e1 | ||
|
|
8890d9f004 | ||
|
|
4fcb3f9d06 | ||
|
|
5bf7c98cd3 | ||
|
|
46bc1a49df | ||
|
|
21175f41ec | ||
|
|
864c63987e | ||
|
|
4ac0547e22 | ||
|
|
12a9ee262c | ||
|
|
cfa9c19c94 | ||
|
|
56e33e6cdd | ||
|
|
02421072c1 | ||
|
|
b862a0dc65 | ||
|
|
1f25d6a000 | ||
|
|
e1def0616a | ||
|
|
83fd6efc17 | ||
|
|
f0c0e2e83a | ||
|
|
6ca3eb769e | ||
|
|
963d24ab66 | ||
|
|
cfd81441fa | ||
|
|
4084ae2938 | ||
|
|
3f9bc098ec | ||
|
|
9cb28ed969 | ||
|
|
773ddc5373 | ||
|
|
38193b5621 | ||
|
|
358713a2a3 | ||
|
|
5b9f24c1f0 | ||
|
|
c47c2c3666 | ||
|
|
49f7c3e7d7 | ||
|
|
423e5de2c6 | ||
|
|
3e3387e63e | ||
|
|
debddace8b | ||
|
|
160b412e0a | ||
|
|
0f3e0a0ea2 | ||
|
|
c4cf90e807 | ||
|
|
5db46e81b7 | ||
|
|
1ef80a3a96 | ||
|
|
a46d9d0d2a | ||
|
|
7b80536e1e | ||
|
|
5733ecf20e | ||
|
|
eae33b61cf | ||
|
|
9e55b525f1 | ||
|
|
678b3cb505 | ||
|
|
b4c833b039 | ||
|
|
597bd021b8 | ||
|
|
ba03118a43 | ||
|
|
82148408b0 | ||
|
|
042900e065 | ||
|
|
874fccc351 | ||
|
|
14f36872e7 | ||
|
|
3b6ad3052a | ||
|
|
194fc6b6ed | ||
|
|
0275ad54ac | ||
|
|
7ca4044467 | ||
|
|
1672494ee9 | ||
|
|
bbbbc72d22 |
Submodule AndroidLibXrayLite updated: c8a6ca7c5e...ddcaecad0a
@@ -9,10 +9,6 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
|
|||||||
[](https://github.com/2dust/v2rayNG/releases)
|
[](https://github.com/2dust/v2rayNG/releases)
|
||||||
[](https://t.me/v2rayn)
|
[](https://t.me/v2rayn)
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
|
||||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
### Telegram Channel
|
### Telegram Channel
|
||||||
[github_2dust](https://t.me/github_2dust)
|
[github_2dust](https://t.me/github_2dust)
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 644
|
versionCode = 653
|
||||||
versionName = "1.9.44"
|
versionName = "1.10.3"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"routing": {
|
"routing": {
|
||||||
"domainStrategy": "IPIfNonMatch",
|
"domainStrategy": "AsIs",
|
||||||
"rules": []
|
"rules": []
|
||||||
},
|
},
|
||||||
"dns": {
|
"dns": {
|
||||||
|
|||||||
@@ -39,5 +39,9 @@ class AngApplication : MultiDexApplication() {
|
|||||||
WorkManager.initialize(this, workManagerConfiguration)
|
WorkManager.initialize(this, workManagerConfiguration)
|
||||||
|
|
||||||
SettingsManager.initRoutingRulesets(this)
|
SettingsManager.initRoutingRulesets(this)
|
||||||
|
|
||||||
|
es.dmoral.toasty.Toasty.Config.getInstance()
|
||||||
|
.setGravity(android.view.Gravity.BOTTOM, 0, 200)
|
||||||
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,13 +57,15 @@ object AppConfig {
|
|||||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||||
const val PREF_MODE = "pref_mode"
|
const val PREF_MODE = "pref_mode"
|
||||||
const val PREF_IS_BOOTED = "pref_is_booted"
|
const val PREF_IS_BOOTED = "pref_is_booted"
|
||||||
|
const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release"
|
||||||
|
const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources"
|
||||||
|
|
||||||
/** Cache keys. */
|
/** Cache keys. */
|
||||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||||
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
||||||
|
|
||||||
/** Protocol identifiers. */
|
/** Protocol identifiers. */
|
||||||
const val PROTOCOL_FREEDOM: String = "freedom"
|
const val PROTOCOL_FREEDOM = "freedom"
|
||||||
|
|
||||||
/** Broadcast actions. */
|
/** Broadcast actions. */
|
||||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
||||||
@@ -88,19 +90,20 @@ object AppConfig {
|
|||||||
const val DOWNLINK = "downlink"
|
const val DOWNLINK = "downlink"
|
||||||
|
|
||||||
/** URLs for various resources. */
|
/** URLs for various resources. */
|
||||||
const val androidpackagenamelistUrl =
|
const val GITHUB_URL = "https://github.com"
|
||||||
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
|
||||||
const val v2rayCustomRoutingListUrl =
|
const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
|
||||||
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
|
||||||
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
|
const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
|
||||||
const val v2rayNGIssues = "$v2rayNGUrl/issues"
|
const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
|
||||||
const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode"
|
const val APP_ISSUES_URL = "$APP_URL/issues"
|
||||||
const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md"
|
const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
|
||||||
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
||||||
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
|
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
||||||
const val TgChannelUrl = "https://t.me/github_2dust"
|
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
||||||
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
|
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||||
const val DelayTestUrl2 = "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"
|
||||||
|
|
||||||
/** DNS server addresses. */
|
/** DNS server addresses. */
|
||||||
const val DNS_PROXY = "1.1.1.1"
|
const val DNS_PROXY = "1.1.1.1"
|
||||||
@@ -170,14 +173,6 @@ object AppConfig {
|
|||||||
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
||||||
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
||||||
|
|
||||||
|
|
||||||
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
|
||||||
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
|
||||||
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
|
||||||
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
|
||||||
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
|
|
||||||
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
|
|
||||||
|
|
||||||
const val DEFAULT_PORT = 443
|
const val DEFAULT_PORT = 443
|
||||||
const val DEFAULT_SECURITY = "auto"
|
const val DEFAULT_SECURITY = "auto"
|
||||||
const val DEFAULT_LEVEL = 8
|
const val DEFAULT_LEVEL = 8
|
||||||
@@ -186,4 +181,62 @@ object AppConfig {
|
|||||||
const val REALITY = "reality"
|
const val REALITY = "reality"
|
||||||
const val HEADER_TYPE_HTTP = "http"
|
const val HEADER_TYPE_HTTP = "http"
|
||||||
|
|
||||||
|
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
||||||
|
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||||
|
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||||
|
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
||||||
|
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
|
||||||
|
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
|
||||||
|
|
||||||
|
//minimum list https://serverfault.com/a/304791
|
||||||
|
val BYPASS_PRIVATE_IP_LIST = arrayListOf(
|
||||||
|
"0.0.0.0/5",
|
||||||
|
"8.0.0.0/7",
|
||||||
|
"11.0.0.0/8",
|
||||||
|
"12.0.0.0/6",
|
||||||
|
"16.0.0.0/4",
|
||||||
|
"32.0.0.0/3",
|
||||||
|
"64.0.0.0/2",
|
||||||
|
"128.0.0.0/3",
|
||||||
|
"160.0.0.0/5",
|
||||||
|
"168.0.0.0/6",
|
||||||
|
"172.0.0.0/12",
|
||||||
|
"172.32.0.0/11",
|
||||||
|
"172.64.0.0/10",
|
||||||
|
"172.128.0.0/9",
|
||||||
|
"173.0.0.0/8",
|
||||||
|
"174.0.0.0/7",
|
||||||
|
"176.0.0.0/4",
|
||||||
|
"192.0.0.0/9",
|
||||||
|
"192.128.0.0/11",
|
||||||
|
"192.160.0.0/13",
|
||||||
|
"192.169.0.0/16",
|
||||||
|
"192.170.0.0/15",
|
||||||
|
"192.172.0.0/14",
|
||||||
|
"192.176.0.0/12",
|
||||||
|
"192.192.0.0/10",
|
||||||
|
"193.0.0.0/8",
|
||||||
|
"194.0.0.0/7",
|
||||||
|
"196.0.0.0/6",
|
||||||
|
"200.0.0.0/5",
|
||||||
|
"208.0.0.0/4",
|
||||||
|
"240.0.0.0/4"
|
||||||
|
)
|
||||||
|
|
||||||
|
val PRIVATE_IP_LIST = arrayListOf(
|
||||||
|
"0.0.0.0/8",
|
||||||
|
"10.0.0.0/8",
|
||||||
|
"127.0.0.0/8",
|
||||||
|
"172.16.0.0/12",
|
||||||
|
"192.168.0.0/16",
|
||||||
|
"169.254.0.0/16",
|
||||||
|
"224.0.0.0/4"
|
||||||
|
)
|
||||||
|
|
||||||
|
val GEO_FILES_SOURCES = arrayListOf(
|
||||||
|
"Loyalsoldier/v2ray-rules-dat",
|
||||||
|
"runetfreedom/russia-v2ray-rules-dat",
|
||||||
|
"Chocolate4U/Iran-v2ray-rules"
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ data class AssetUrlItem(
|
|||||||
var remarks: String = "",
|
var remarks: String = "",
|
||||||
var url: String = "",
|
var url: String = "",
|
||||||
val addedTime: Long = System.currentTimeMillis(),
|
val addedTime: Long = System.currentTimeMillis(),
|
||||||
var lastUpdated: Long = -1
|
var lastUpdated: Long = -1,
|
||||||
|
var locked: Boolean? = false,
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class CheckUpdateResult(
|
||||||
|
val hasUpdate: Boolean,
|
||||||
|
val latestVersion: String? = null,
|
||||||
|
val releaseNotes: String? = null,
|
||||||
|
val downloadUrl: String? = null,
|
||||||
|
val error: String? = null,
|
||||||
|
val isPreRelease: Boolean = false
|
||||||
|
)
|
||||||
@@ -4,6 +4,6 @@ data class ConfigResult(
|
|||||||
var status: Boolean,
|
var status: Boolean,
|
||||||
var guid: String? = null,
|
var guid: String? = null,
|
||||||
var content: String = "",
|
var content: String = "",
|
||||||
var domainPort: String? = null,
|
var socksPort: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
23
V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
Normal file
23
V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class GitHubRelease(
|
||||||
|
@SerializedName("tag_name")
|
||||||
|
val tagName: String,
|
||||||
|
@SerializedName("body")
|
||||||
|
val body: String,
|
||||||
|
@SerializedName("assets")
|
||||||
|
val assets: List<Asset>,
|
||||||
|
@SerializedName("prerelease")
|
||||||
|
val prerelease: Boolean = false,
|
||||||
|
@SerializedName("published_at")
|
||||||
|
val publishedAt: String = ""
|
||||||
|
) {
|
||||||
|
data class Asset(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("browser_download_url")
|
||||||
|
val browserDownloadUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
12
V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
Normal file
12
V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class IPAPIInfo(
|
||||||
|
var ip: String? = null,
|
||||||
|
var clientIp: String? = null,
|
||||||
|
var ip_addr: String? = null,
|
||||||
|
var query: String? = null,
|
||||||
|
var country: String? = null,
|
||||||
|
var country_name: String? = null,
|
||||||
|
var country_code: String? = null,
|
||||||
|
var countryCode: String? = null
|
||||||
|
)
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ProfileLiteItem(
|
|
||||||
val configType: EConfigType,
|
|
||||||
var subscriptionId: String = "",
|
|
||||||
var remarks: String = "",
|
|
||||||
var server: String?,
|
|
||||||
var serverPort: Int?,
|
|
||||||
)
|
|
||||||
@@ -11,5 +11,6 @@ data class SubscriptionItem(
|
|||||||
var prevProfile: String? = null,
|
var prevProfile: String? = null,
|
||||||
var nextProfile: String? = null,
|
var nextProfile: String? = null,
|
||||||
var filter: String? = null,
|
var filter: String? = null,
|
||||||
|
var allowInsecureUrl: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
package com.v2ray.ang.dto
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.ServersBean
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
data class V2rayConfig(
|
data class V2rayConfig(
|
||||||
var remarks: String? = null,
|
var remarks: String? = null,
|
||||||
var stats: Any? = null,
|
var stats: Any? = null,
|
||||||
val log: LogBean,
|
val log: LogBean,
|
||||||
var policy: PolicyBean?,
|
var policy: PolicyBean? = null,
|
||||||
val inbounds: ArrayList<InboundBean>,
|
val inbounds: ArrayList<InboundBean>,
|
||||||
var outbounds: ArrayList<OutboundBean>,
|
var outbounds: ArrayList<OutboundBean>,
|
||||||
var dns: DnsBean? = null,
|
var dns: DnsBean? = null,
|
||||||
@@ -34,9 +23,9 @@ data class V2rayConfig(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
data class LogBean(
|
data class LogBean(
|
||||||
val access: String,
|
val access: String? = null,
|
||||||
val error: String,
|
val error: String? = null,
|
||||||
var loglevel: String?,
|
var loglevel: String? = null,
|
||||||
val dnsLog: Boolean? = null
|
val dnsLog: Boolean? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,7 +35,7 @@ data class V2rayConfig(
|
|||||||
var protocol: String,
|
var protocol: String,
|
||||||
var listen: String? = null,
|
var listen: String? = null,
|
||||||
val settings: Any? = null,
|
val settings: Any? = null,
|
||||||
val sniffing: SniffingBean?,
|
val sniffing: SniffingBean? = null,
|
||||||
val streamSettings: Any? = null,
|
val streamSettings: Any? = null,
|
||||||
val allocate: Any? = null
|
val allocate: Any? = null
|
||||||
) {
|
) {
|
||||||
@@ -77,50 +66,6 @@ data class V2rayConfig(
|
|||||||
val sendThrough: String? = null,
|
val sendThrough: String? = null,
|
||||||
var mux: MuxBean? = MuxBean(false)
|
var mux: MuxBean? = MuxBean(false)
|
||||||
) {
|
) {
|
||||||
companion object {
|
|
||||||
fun create(configType: EConfigType): OutboundBean? {
|
|
||||||
return when (configType) {
|
|
||||||
EConfigType.VMESS,
|
|
||||||
EConfigType.VLESS ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutSettingsBean(
|
|
||||||
vnext = listOf(
|
|
||||||
VnextBean(
|
|
||||||
users = listOf(UsersBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
streamSettings = StreamSettingsBean()
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.SHADOWSOCKS,
|
|
||||||
EConfigType.SOCKS,
|
|
||||||
EConfigType.HTTP,
|
|
||||||
EConfigType.TROJAN,
|
|
||||||
EConfigType.HYSTERIA2 ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutSettingsBean(
|
|
||||||
servers = listOf(ServersBean())
|
|
||||||
),
|
|
||||||
streamSettings = StreamSettingsBean()
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.WIREGUARD ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutSettingsBean(
|
|
||||||
secretKey = "",
|
|
||||||
peers = listOf(WireGuardBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.CUSTOM -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class OutSettingsBean(
|
data class OutSettingsBean(
|
||||||
var vnext: List<VnextBean>? = null,
|
var vnext: List<VnextBean>? = null,
|
||||||
var fragment: FragmentBean? = null,
|
var fragment: FragmentBean? = null,
|
||||||
@@ -197,7 +142,7 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class WireGuardBean(
|
data class WireGuardBean(
|
||||||
var publicKey: String = "",
|
var publicKey: String = "",
|
||||||
var preSharedKey: String = "",
|
var preSharedKey: String? = null,
|
||||||
var endpoint: String = ""
|
var endpoint: String = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -299,7 +244,8 @@ data class V2rayConfig(
|
|||||||
var tcpFastOpen: Boolean? = null,
|
var tcpFastOpen: Boolean? = null,
|
||||||
var tproxy: String? = null,
|
var tproxy: String? = null,
|
||||||
var mark: Int? = null,
|
var mark: Int? = null,
|
||||||
var dialerProxy: String? = null
|
var dialerProxy: String? = null,
|
||||||
|
var domainStrategy: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TlsSettingsBean(
|
data class TlsSettingsBean(
|
||||||
@@ -349,139 +295,6 @@ data class V2rayConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateTransportSettings(
|
|
||||||
transport: String,
|
|
||||||
headerType: String?,
|
|
||||||
host: String?,
|
|
||||||
path: String?,
|
|
||||||
seed: String?,
|
|
||||||
quicSecurity: String?,
|
|
||||||
key: String?,
|
|
||||||
mode: String?,
|
|
||||||
serviceName: String?,
|
|
||||||
authority: String?
|
|
||||||
): String? {
|
|
||||||
var sni: String? = null
|
|
||||||
network = if (transport.isEmpty()) NetworkType.TCP.type else transport
|
|
||||||
when (network) {
|
|
||||||
NetworkType.TCP.type -> {
|
|
||||||
val tcpSetting = TcpSettingsBean()
|
|
||||||
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
|
||||||
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
|
||||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
|
||||||
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
|
|
||||||
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
tcpSetting.header.request = requestObj
|
|
||||||
sni = requestObj.headers.Host?.getOrNull(0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tcpSetting.header.type = "none"
|
|
||||||
sni = host
|
|
||||||
}
|
|
||||||
tcpSettings = tcpSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.KCP.type -> {
|
|
||||||
val kcpsetting = KcpSettingsBean()
|
|
||||||
kcpsetting.header.type = headerType ?: "none"
|
|
||||||
if (seed.isNullOrEmpty()) {
|
|
||||||
kcpsetting.seed = null
|
|
||||||
} else {
|
|
||||||
kcpsetting.seed = seed
|
|
||||||
}
|
|
||||||
if (host.isNullOrEmpty()) {
|
|
||||||
kcpsetting.header.domain = null
|
|
||||||
} else {
|
|
||||||
kcpsetting.header.domain = host
|
|
||||||
}
|
|
||||||
kcpSettings = kcpsetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.WS.type -> {
|
|
||||||
val wssetting = WsSettingsBean()
|
|
||||||
wssetting.headers.Host = host.orEmpty()
|
|
||||||
sni = host
|
|
||||||
wssetting.path = path ?: "/"
|
|
||||||
wsSettings = wssetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.HTTP_UPGRADE.type -> {
|
|
||||||
val httpupgradeSetting = HttpupgradeSettingsBean()
|
|
||||||
httpupgradeSetting.host = host.orEmpty()
|
|
||||||
sni = host
|
|
||||||
httpupgradeSetting.path = path ?: "/"
|
|
||||||
httpupgradeSettings = httpupgradeSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.XHTTP.type -> {
|
|
||||||
val xhttpSetting = XhttpSettingsBean()
|
|
||||||
xhttpSetting.host = host.orEmpty()
|
|
||||||
sni = host
|
|
||||||
xhttpSetting.path = path ?: "/"
|
|
||||||
xhttpSettings = xhttpSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.H2.type, NetworkType.HTTP.type -> {
|
|
||||||
network = NetworkType.H2.type
|
|
||||||
val h2Setting = HttpSettingsBean()
|
|
||||||
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
sni = h2Setting.host.getOrNull(0)
|
|
||||||
h2Setting.path = path ?: "/"
|
|
||||||
httpSettings = h2Setting
|
|
||||||
}
|
|
||||||
|
|
||||||
// "quic" -> {
|
|
||||||
// val quicsetting = QuicSettingBean()
|
|
||||||
// quicsetting.security = quicSecurity ?: "none"
|
|
||||||
// quicsetting.key = key.orEmpty()
|
|
||||||
// quicsetting.header.type = headerType ?: "none"
|
|
||||||
// quicSettings = quicsetting
|
|
||||||
// }
|
|
||||||
|
|
||||||
NetworkType.GRPC.type -> {
|
|
||||||
val grpcSetting = GrpcSettingsBean()
|
|
||||||
grpcSetting.multiMode = mode == "multi"
|
|
||||||
grpcSetting.serviceName = serviceName.orEmpty()
|
|
||||||
grpcSetting.authority = authority.orEmpty()
|
|
||||||
grpcSetting.idle_timeout = 60
|
|
||||||
grpcSetting.health_check_timeout = 20
|
|
||||||
sni = authority
|
|
||||||
grpcSettings = grpcSetting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sni
|
|
||||||
}
|
|
||||||
|
|
||||||
fun populateTlsSettings(
|
|
||||||
streamSecurity: String,
|
|
||||||
allowInsecure: Boolean,
|
|
||||||
sni: String?,
|
|
||||||
fingerprint: String?,
|
|
||||||
alpns: String?,
|
|
||||||
publicKey: String?,
|
|
||||||
shortId: String?,
|
|
||||||
spiderX: String?
|
|
||||||
) {
|
|
||||||
security = if (streamSecurity.isEmpty()) null else streamSecurity
|
|
||||||
if (security == null) return
|
|
||||||
val tlsSetting = TlsSettingsBean(
|
|
||||||
allowInsecure = allowInsecure,
|
|
||||||
serverName = if (sni.isNullOrEmpty()) null else sni,
|
|
||||||
fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
|
|
||||||
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
|
||||||
publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
|
|
||||||
shortId = if (shortId.isNullOrEmpty()) null else shortId,
|
|
||||||
spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
|
|
||||||
)
|
|
||||||
if (security == AppConfig.TLS) {
|
|
||||||
tlsSettings = tlsSetting
|
|
||||||
realitySettings = null
|
|
||||||
} else if (security == AppConfig.REALITY) {
|
|
||||||
tlsSettings = null
|
|
||||||
realitySettings = tlsSetting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MuxBean(
|
data class MuxBean(
|
||||||
@@ -647,6 +460,18 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ensureSockopt(): V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean {
|
||||||
|
val stream = streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean().also {
|
||||||
|
streamSettings = it
|
||||||
|
}
|
||||||
|
|
||||||
|
val sockopt = stream.sockopt ?: V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean().also {
|
||||||
|
stream.sockopt = it
|
||||||
|
}
|
||||||
|
|
||||||
|
return sockopt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DnsBean(
|
data class DnsBean(
|
||||||
@@ -723,15 +548,9 @@ data class V2rayConfig(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toPrettyPrinting(): String {
|
fun getAllProxyOutbound(): List<OutboundBean> {
|
||||||
return GsonBuilder()
|
return outbounds.filter { outbound ->
|
||||||
.setPrettyPrinting()
|
EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
|
||||||
.disableHtmlEscaping()
|
}
|
||||||
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
|
|
||||||
object : TypeToken<Double>() {}.type,
|
|
||||||
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
|
|
||||||
)
|
|
||||||
.create()
|
|
||||||
.toJson(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,4 +196,17 @@ inline fun <reified T : Serializable> Intent.serializable(key: String): T? = whe
|
|||||||
*
|
*
|
||||||
* @return True if the CharSequence is not null and not empty, false otherwise.
|
* @return True if the CharSequence is not null and not empty, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
|
fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty()
|
||||||
|
|
||||||
|
fun String.concatUrl(vararg paths: String): String {
|
||||||
|
val builder = StringBuilder(this.trimEnd('/'))
|
||||||
|
|
||||||
|
paths.forEach { path ->
|
||||||
|
val trimmedPath = path.trim('/')
|
||||||
|
if (trimmedPath.isNotEmpty()) {
|
||||||
|
builder.append('/').append(trimmedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString()
|
||||||
|
}
|
||||||
@@ -18,9 +18,9 @@ open class FmtBase {
|
|||||||
*/
|
*/
|
||||||
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
|
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
|
||||||
val query = if (dicQuery != null)
|
val query = if (dicQuery != null)
|
||||||
("?" + dicQuery.toList().joinToString(
|
"?" + dicQuery.toList().joinToString(
|
||||||
separator = "&",
|
separator = "&",
|
||||||
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
|
transform = { it.first + "=" + Utils.urlEncode(it.second) })
|
||||||
else ""
|
else ""
|
||||||
|
|
||||||
val url = String.format(
|
val url = String.format(
|
||||||
@@ -148,4 +148,7 @@ open class FmtBase {
|
|||||||
|
|
||||||
return dicQuery
|
return dicQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.v2ray.ang.dto.EConfigType
|
|||||||
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.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
|
|
||||||
object HttpFmt : FmtBase() {
|
object HttpFmt : FmtBase() {
|
||||||
/**
|
/**
|
||||||
@@ -13,7 +14,7 @@ object HttpFmt : FmtBase() {
|
|||||||
* @return the converted OutboundBean object, or null if conversion fails
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
*/
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = profileItem.server.orEmpty()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
@@ -144,7 +145,7 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
* @return the converted OutboundBean object, or null if conversion fails
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
*/
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2)
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.NetworkType
|
|||||||
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.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
if (uri.port <= 0) return null
|
if (uri.port <= 0) return null
|
||||||
if (uri.userInfo.isNullOrEmpty()) return null
|
if (uri.userInfo.isNullOrEmpty()) return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
@@ -131,7 +132,7 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
* @return the converted OutboundBean object, or null if conversion fails
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
*/
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = profileItem.server.orEmpty()
|
||||||
@@ -140,29 +141,13 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
server.method = profileItem.method
|
server.method = profileItem.method
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
profileItem.network.orEmpty(),
|
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||||
profileItem.headerType,
|
}
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.let {
|
||||||
profileItem.security.orEmpty(),
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
profileItem.insecure == true,
|
}
|
||||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
profileItem.publicKey,
|
|
||||||
profileItem.shortId,
|
|
||||||
profileItem.spiderX,
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.v2ray.ang.dto.ProfileItem
|
|||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ object SocksFmt : FmtBase() {
|
|||||||
if (uri.idnHost.isEmpty()) return null
|
if (uri.idnHost.isEmpty()) return null
|
||||||
if (uri.port <= 0) return null
|
if (uri.port <= 0) return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ object SocksFmt : FmtBase() {
|
|||||||
* @return the converted OutboundBean object, or null if conversion fails
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
*/
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = profileItem.server.orEmpty()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem
|
|||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ object TrojanFmt : FmtBase() {
|
|||||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
@@ -60,7 +61,7 @@ object TrojanFmt : FmtBase() {
|
|||||||
* @return the converted OutboundBean object, or null if conversion fails
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
*/
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = profileItem.server.orEmpty()
|
||||||
@@ -69,29 +70,13 @@ object TrojanFmt : FmtBase() {
|
|||||||
server.flow = profileItem.flow
|
server.flow = profileItem.flow
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
profileItem.network.orEmpty(),
|
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||||
profileItem.headerType,
|
}
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.let {
|
||||||
profileItem.security.orEmpty(),
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
profileItem.insecure == true,
|
}
|
||||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
profileItem.publicKey,
|
|
||||||
profileItem.shortId,
|
|
||||||
profileItem.spiderX,
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import com.v2ray.ang.dto.ProfileItem
|
|||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
|
|||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
@@ -57,7 +57,7 @@ object VlessFmt : FmtBase() {
|
|||||||
* @return the converted OutboundBean object, or null if conversion fails
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
*/
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
vnext.address = profileItem.server.orEmpty()
|
vnext.address = profileItem.server.orEmpty()
|
||||||
@@ -67,31 +67,13 @@ object VlessFmt : FmtBase() {
|
|||||||
vnext.users[0].flow = profileItem.flow
|
vnext.users[0].flow = profileItem.flow
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
profileItem.network.orEmpty(),
|
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||||
profileItem.headerType,
|
}
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
|
|
||||||
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.let {
|
||||||
profileItem.security.orEmpty(),
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
profileItem.insecure == true,
|
}
|
||||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
profileItem.publicKey,
|
|
||||||
profileItem.shortId,
|
|
||||||
profileItem.spiderX,
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.v2ray.ang.dto.VmessQRCode
|
|||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -150,7 +151,7 @@ object VmessFmt : FmtBase() {
|
|||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
@@ -168,7 +169,7 @@ object VmessFmt : FmtBase() {
|
|||||||
* @return the converted OutboundBean object, or null if conversion fails
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
*/
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
vnext.address = profileItem.server.orEmpty()
|
vnext.address = profileItem.server.orEmpty()
|
||||||
@@ -177,29 +178,13 @@ object VmessFmt : FmtBase() {
|
|||||||
vnext.users[0].security = profileItem.method
|
vnext.users[0].security = profileItem.method
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
profileItem.network.orEmpty(),
|
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||||
profileItem.headerType,
|
}
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.let {
|
||||||
profileItem.security.orEmpty(),
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
profileItem.insecure == true,
|
}
|
||||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem
|
|||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.extension.removeWhiteSpace
|
import com.v2ray.ang.extension.removeWhiteSpace
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@@ -24,16 +25,16 @@ object WireguardFmt : FmtBase() {
|
|||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
config.secretKey = uri.userInfo.orEmpty()
|
config.secretKey = uri.userInfo.orEmpty()
|
||||||
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
config.localAddress = queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
config.publicKey = queryParam["publickey"].orEmpty()
|
config.publicKey = queryParam["publickey"].orEmpty()
|
||||||
config.preSharedKey = queryParam["presharedkey"].orEmpty()
|
config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() }
|
||||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||||
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
config.reserved = queryParam["reserved"] ?: "0,0,0"
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,7 @@ object WireguardFmt : FmtBase() {
|
|||||||
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||||
config.publicKey = peerParams["publickey"].orEmpty()
|
config.publicKey = peerParams["publickey"].orEmpty()
|
||||||
config.preSharedKey = peerParams["presharedkey"].orEmpty()
|
config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() }
|
||||||
val endpoint = peerParams["endpoint"].orEmpty()
|
val endpoint = peerParams["endpoint"].orEmpty()
|
||||||
val endpointParts = endpoint.split(":", limit = 2)
|
val endpointParts = endpoint.split(":", limit = 2)
|
||||||
if (endpointParts.size == 2) {
|
if (endpointParts.size == 2) {
|
||||||
@@ -105,18 +106,18 @@ object WireguardFmt : FmtBase() {
|
|||||||
* @return the converted OutboundBean object, or null if conversion fails
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
*/
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
outboundBean?.settings?.let { wireguard ->
|
outboundBean?.settings?.let { wireguard ->
|
||||||
wireguard.secretKey = profileItem.secretKey
|
wireguard.secretKey = profileItem.secretKey
|
||||||
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||||
peer.publicKey = profileItem.publicKey.orEmpty()
|
peer.publicKey = profileItem.publicKey.orEmpty()
|
||||||
peer.preSharedKey = profileItem.preSharedKey.orEmpty()
|
peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() }
|
||||||
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||||
}
|
}
|
||||||
wireguard.mtu = profileItem.mtu
|
wireguard.mtu = profileItem.mtu
|
||||||
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
|
wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
|
||||||
}
|
}
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ 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
|
||||||
import com.v2ray.ang.dto.*
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
import com.v2ray.ang.fmt.CustomFmt
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||||
import com.v2ray.ang.fmt.ShadowsocksFmt
|
import com.v2ray.ang.fmt.ShadowsocksFmt
|
||||||
@@ -417,6 +419,11 @@ object AngConfigManager {
|
|||||||
if (!Utils.isValidUrl(url)) {
|
if (!Utils.isValidUrl(url)) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
if (!it.second.allowInsecureUrl) {
|
||||||
|
if (!Utils.isValidSubUrl(url)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
Log.i(AppConfig.TAG, url)
|
Log.i(AppConfig.TAG, url)
|
||||||
|
|
||||||
var configText = try {
|
var configText = try {
|
||||||
@@ -430,7 +437,7 @@ object AngConfigManager {
|
|||||||
configText = try {
|
configText = try {
|
||||||
HttpUtil.getUrlContentWithUserAgent(url)
|
HttpUtil.getUrlContentWithUserAgent(url)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to get URL content with user agent", e)
|
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ object SettingsManager {
|
|||||||
* @return The ProfileItem.
|
* @return The ProfileItem.
|
||||||
*/
|
*/
|
||||||
fun getServerViaRemarks(remarks: String?): ProfileItem? {
|
fun getServerViaRemarks(remarks: String?): ProfileItem? {
|
||||||
if (remarks == null) {
|
if (remarks.isNullOrEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val serverList = decodeServerList()
|
val serverList = decodeServerList()
|
||||||
@@ -242,7 +242,7 @@ object SettingsManager {
|
|||||||
* @return The HTTP port.
|
* @return The HTTP port.
|
||||||
*/
|
*/
|
||||||
fun getHttpPort(): Int {
|
fun getHttpPort(): Int {
|
||||||
return getSocksPort() + (if (Utils.isXray()) 0 else 1)
|
return getSocksPort() + if (Utils.isXray()) 0 else 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -316,10 +316,10 @@ object SettingsManager {
|
|||||||
*/
|
*/
|
||||||
fun getDelayTestUrl(second: Boolean = false): String {
|
fun getDelayTestUrl(second: Boolean = false): String {
|
||||||
return if (second) {
|
return if (second) {
|
||||||
AppConfig.DelayTestUrl2
|
AppConfig.DELAY_TEST_URL2
|
||||||
} else {
|
} else {
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
||||||
?: AppConfig.DelayTestUrl
|
?: AppConfig.DELAY_TEST_URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import android.text.TextUtils
|
|||||||
import android.util.Log
|
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.extension.responseLength
|
import com.v2ray.ang.extension.responseLength
|
||||||
import com.v2ray.ang.util.HttpUtil
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -164,6 +166,17 @@ object SpeedtestManager {
|
|||||||
return Pair(elapsed, result)
|
return Pair(elapsed, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRemoteIPInfo(): String? {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null
|
||||||
|
|
||||||
|
var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
|
||||||
|
var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query
|
||||||
|
var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode
|
||||||
|
|
||||||
|
return "(${country ?: "unknown"}) $ip"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the version of the V2Ray library.
|
* Gets the version of the V2Ray library.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.BuildConfig
|
||||||
|
import com.v2ray.ang.dto.CheckUpdateResult
|
||||||
|
import com.v2ray.ang.dto.GitHubRelease
|
||||||
|
import com.v2ray.ang.extension.concatUrl
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
object UpdateCheckerManager {
|
||||||
|
suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val url = if (includePreRelease) {
|
||||||
|
AppConfig.APP_API_URL
|
||||||
|
} else {
|
||||||
|
AppConfig.APP_API_URL.concatUrl("latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = HttpUtil.getUrlContent(url, 5000)
|
||||||
|
if (response.isNullOrEmpty()) {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
response = HttpUtil.getUrlContent(url, 5000, httpPort) ?: throw IllegalStateException("Failed to get response")
|
||||||
|
}
|
||||||
|
|
||||||
|
val latestRelease = if (includePreRelease) {
|
||||||
|
JsonUtil.fromJson(response, Array<GitHubRelease>::class.java)
|
||||||
|
.firstOrNull()
|
||||||
|
?: throw IllegalStateException("No pre-release found")
|
||||||
|
} else {
|
||||||
|
JsonUtil.fromJson(response, GitHubRelease::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
val latestVersion = latestRelease.tagName.removePrefix("v")
|
||||||
|
Log.i(AppConfig.TAG, "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})")
|
||||||
|
|
||||||
|
return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) {
|
||||||
|
val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0])
|
||||||
|
CheckUpdateResult(
|
||||||
|
hasUpdate = true,
|
||||||
|
latestVersion = latestVersion,
|
||||||
|
releaseNotes = latestRelease.body,
|
||||||
|
downloadUrl = downloadUrl,
|
||||||
|
isPreRelease = latestRelease.prerelease
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CheckUpdateResult(hasUpdate = false)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
|
||||||
|
return@withContext CheckUpdateResult(hasUpdate = false, error = e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
|
||||||
|
?: throw IllegalStateException("Failed to create connection")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val apkFile = File(context.cacheDir, "update.apk")
|
||||||
|
Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
|
||||||
|
|
||||||
|
FileOutputStream(apkFile).use { outputStream ->
|
||||||
|
connection.inputStream.use { inputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(AppConfig.TAG, "APK download completed")
|
||||||
|
return@withContext apkFile
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
|
||||||
|
return@withContext null
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
connection.disconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error closing connection: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareVersions(version1: String, version2: String): Int {
|
||||||
|
val v1 = version1.split(".")
|
||||||
|
val v2 = version2.split(".")
|
||||||
|
|
||||||
|
for (i in 0 until maxOf(v1.size, v2.size)) {
|
||||||
|
val num1 = if (i < v1.size) v1[i].toInt() else 0
|
||||||
|
val num2 = if (i < v2.size) v2[i].toInt() else 0
|
||||||
|
if (num1 != num2) return num1 - num2
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDownloadUrl(release: GitHubRelease, abi: String): String {
|
||||||
|
return release.assets.find { it.name.contains(abi) }?.browserDownloadUrl
|
||||||
|
?: release.assets.firstOrNull()?.browserDownloadUrl
|
||||||
|
?: throw IllegalStateException("No compatible APK found")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,39 +4,15 @@ import android.content.Context
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_ADDRESSES
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_DOMAIN
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_DNSPOD_ADDRESSES
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_DNSPOD_DOMAIN
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_QUAD9_ADDRESSES
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_QUAD9_DOMAIN
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_YANDEX_ADDRESSES
|
|
||||||
import com.v2ray.ang.AppConfig.DNS_YANDEX_DOMAIN
|
|
||||||
import com.v2ray.ang.AppConfig.GEOIP_CN
|
|
||||||
import com.v2ray.ang.AppConfig.GEOSITE_CN
|
|
||||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
|
||||||
import com.v2ray.ang.AppConfig.GOOGLEAPIS_CN_DOMAIN
|
|
||||||
import com.v2ray.ang.AppConfig.GOOGLEAPIS_COM_DOMAIN
|
|
||||||
import com.v2ray.ang.AppConfig.HEADER_TYPE_HTTP
|
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
|
||||||
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
|
||||||
import com.v2ray.ang.dto.ConfigResult
|
import com.v2ray.ang.dto.ConfigResult
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.NetworkType
|
import com.v2ray.ang.dto.NetworkType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean
|
||||||
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
|
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.fmt.HttpFmt
|
import com.v2ray.ang.fmt.HttpFmt
|
||||||
@@ -47,12 +23,15 @@ import com.v2ray.ang.fmt.TrojanFmt
|
|||||||
import com.v2ray.ang.fmt.VlessFmt
|
import com.v2ray.ang.fmt.VlessFmt
|
||||||
import com.v2ray.ang.fmt.VmessFmt
|
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.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
object V2rayConfigManager {
|
object V2rayConfigManager {
|
||||||
private var initConfigCache: String? = null
|
private var initConfigCache: String? = null
|
||||||
|
|
||||||
|
//region get config function
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the V2ray configuration for the given GUID.
|
* Retrieves the V2ray configuration for the given GUID.
|
||||||
*
|
*
|
||||||
@@ -104,8 +83,7 @@ object V2rayConfigManager {
|
|||||||
*/
|
*/
|
||||||
private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult {
|
private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult {
|
||||||
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
|
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
|
||||||
val domainPort = config.getServerAddressAndPort()
|
return ConfigResult(true, guid, raw)
|
||||||
return ConfigResult(true, guid, raw, domainPort)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,29 +109,33 @@ object V2rayConfigManager {
|
|||||||
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
||||||
v2rayConfig.remarks = config.remarks
|
v2rayConfig.remarks = config.remarks
|
||||||
|
|
||||||
inbounds(v2rayConfig)
|
getInbounds(v2rayConfig)
|
||||||
|
|
||||||
val isPlugin = config.configType == EConfigType.HYSTERIA2
|
if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
|
result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result
|
||||||
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
|
} else {
|
||||||
|
getOutbounds(v2rayConfig, config) ?: return result
|
||||||
|
getMoreOutbounds(v2rayConfig, config.subscriptionId)
|
||||||
|
}
|
||||||
|
|
||||||
routing(v2rayConfig)
|
getRouting(v2rayConfig)
|
||||||
|
|
||||||
fakedns(v2rayConfig)
|
getFakeDns(v2rayConfig)
|
||||||
|
|
||||||
dns(v2rayConfig)
|
getDns(v2rayConfig)
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||||
customLocalDns(v2rayConfig)
|
getCustomLocalDns(v2rayConfig)
|
||||||
}
|
}
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
|
||||||
v2rayConfig.stats = null
|
v2rayConfig.stats = null
|
||||||
v2rayConfig.policy = null
|
v2rayConfig.policy = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveOutboundDomainsToHosts(v2rayConfig)
|
||||||
|
|
||||||
result.status = true
|
result.status = true
|
||||||
result.content = v2rayConfig.toPrettyPrinting()
|
result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: ""
|
||||||
result.domainPort = if (retMore.first) retMore.second else retOut.second
|
|
||||||
result.guid = guid
|
result.guid = guid
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -179,9 +161,12 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
val v2rayConfig = initV2rayConfig(context) ?: return result
|
val v2rayConfig = initV2rayConfig(context) ?: return result
|
||||||
|
|
||||||
val isPlugin = config.configType == EConfigType.HYSTERIA2
|
if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
|
result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result
|
||||||
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
|
} else {
|
||||||
|
getOutbounds(v2rayConfig, config) ?: return result
|
||||||
|
getMoreOutbounds(v2rayConfig, config.subscriptionId)
|
||||||
|
}
|
||||||
|
|
||||||
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
||||||
v2rayConfig.inbounds.clear()
|
v2rayConfig.inbounds.clear()
|
||||||
@@ -196,8 +181,7 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.status = true
|
result.status = true
|
||||||
result.content = v2rayConfig.toPrettyPrinting()
|
result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: ""
|
||||||
result.domainPort = if (retMore.first) retMore.second else retOut.second
|
|
||||||
result.guid = guid
|
result.guid = guid
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -212,7 +196,6 @@ object V2rayConfigManager {
|
|||||||
* @param context Android context used to access application assets
|
* @param context Android context used to access application assets
|
||||||
* @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty
|
* @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private fun initV2rayConfig(context: Context): V2rayConfig? {
|
private fun initV2rayConfig(context: Context): V2rayConfig? {
|
||||||
val assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
val assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||||
if (TextUtils.isEmpty(assets)) {
|
if (TextUtils.isEmpty(assets)) {
|
||||||
@@ -223,14 +206,28 @@ object V2rayConfigManager {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region some sub function
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the inbound settings for V2ray.
|
||||||
|
*
|
||||||
|
* This function sets up the listening ports, sniffing options, and other inbound-related configurations.
|
||||||
|
*
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
* @return true if inbound configuration was successful, false otherwise
|
||||||
|
*/
|
||||||
|
private fun getInbounds(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
val socksPort = SettingsManager.getSocksPort()
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
|
|
||||||
v2rayConfig.inbounds.forEach { curInbound ->
|
v2rayConfig.inbounds.forEach { curInbound ->
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
||||||
//bind all inbounds to localhost if the user requests
|
//bind all inbounds to localhost if the user requests
|
||||||
curInbound.listen = LOOPBACK
|
curInbound.listen = AppConfig.LOOPBACK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
v2rayConfig.inbounds[0].port = socksPort
|
v2rayConfig.inbounds[0].port = socksPort
|
||||||
@@ -261,44 +258,14 @@ object V2rayConfigManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun outbounds(v2rayConfig: V2rayConfig, config: ProfileItem, isPlugin: Boolean): Pair<Boolean, String>? {
|
/**
|
||||||
if (isPlugin) {
|
* Configures the fake DNS settings if enabled.
|
||||||
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
|
*
|
||||||
val outboundNew = V2rayConfig.OutboundBean(
|
* Adds FakeDNS configuration to v2rayConfig if both local DNS and fake DNS are enabled.
|
||||||
mux = null,
|
*
|
||||||
protocol = EConfigType.SOCKS.name.lowercase(),
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
*/
|
||||||
servers = listOf(
|
private fun getFakeDns(v2rayConfig: V2rayConfig) {
|
||||||
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean(
|
|
||||||
address = LOOPBACK,
|
|
||||||
port = socksPort
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (v2rayConfig.outbounds.isNotEmpty()) {
|
|
||||||
v2rayConfig.outbounds[0] = outboundNew
|
|
||||||
} else {
|
|
||||||
v2rayConfig.outbounds.add(outboundNew)
|
|
||||||
}
|
|
||||||
return Pair(true, outboundNew.getServerAddressAndPort())
|
|
||||||
}
|
|
||||||
|
|
||||||
val outbound = getProxyOutbound(config) ?: return null
|
|
||||||
val ret = updateOutboundWithGlobalSettings(outbound)
|
|
||||||
if (!ret) return null
|
|
||||||
|
|
||||||
if (v2rayConfig.outbounds.isNotEmpty()) {
|
|
||||||
v2rayConfig.outbounds[0] = outbound
|
|
||||||
} else {
|
|
||||||
v2rayConfig.outbounds.add(outbound)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOutboundFragment(v2rayConfig)
|
|
||||||
return Pair(true, config.getServerAddressAndPort())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fakedns(v2rayConfig: V2rayConfig) {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|
||||||
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
||||||
) {
|
) {
|
||||||
@@ -306,16 +273,24 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun routing(v2rayConfig: V2rayConfig): Boolean {
|
/**
|
||||||
|
* Configures routing settings for V2ray.
|
||||||
|
*
|
||||||
|
* Sets up the domain strategy and adds routing rules from saved rulesets.
|
||||||
|
*
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
* @return true if routing configuration was successful, false otherwise
|
||||||
|
*/
|
||||||
|
private fun getRouting(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)
|
||||||
?: "IPIfNonMatch"
|
?: "AsIs"
|
||||||
|
|
||||||
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
rulesetItems?.forEach { key ->
|
rulesetItems?.forEach { key ->
|
||||||
routingUserRule(key, v2rayConfig)
|
getRoutingUserRule(key, v2rayConfig)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to configure routing", e)
|
Log.e(AppConfig.TAG, "Failed to configure routing", e)
|
||||||
@@ -324,7 +299,13 @@ object V2rayConfigManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun routingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
|
/**
|
||||||
|
* Adds a specific ruleset item to the routing configuration.
|
||||||
|
*
|
||||||
|
* @param item The ruleset item to add
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
*/
|
||||||
|
private fun getRoutingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
|
||||||
try {
|
try {
|
||||||
if (item == null || !item.enabled) {
|
if (item == null || !item.enabled) {
|
||||||
return
|
return
|
||||||
@@ -339,14 +320,22 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun userRule2Domain(tag: String): ArrayList<String> {
|
/**
|
||||||
|
* Retrieves domain rules for a specific outbound tag.
|
||||||
|
*
|
||||||
|
* Searches through all rulesets to find domains targeting the specified tag.
|
||||||
|
*
|
||||||
|
* @param tag The outbound tag to search for
|
||||||
|
* @return ArrayList of domain rules matching the tag
|
||||||
|
*/
|
||||||
|
private fun getUserRule2Domain(tag: String): ArrayList<String> {
|
||||||
val domain = ArrayList<String>()
|
val domain = ArrayList<String>()
|
||||||
|
|
||||||
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
rulesetItems?.forEach { key ->
|
rulesetItems?.forEach { key ->
|
||||||
if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
|
if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
|
||||||
key.domain?.forEach {
|
key.domain?.forEach {
|
||||||
if (it != GEOSITE_PRIVATE
|
if (it != AppConfig.GEOSITE_PRIVATE
|
||||||
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
|
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
|
||||||
) {
|
) {
|
||||||
domain.add(it)
|
domain.add(it)
|
||||||
@@ -358,12 +347,20 @@ object V2rayConfigManager {
|
|||||||
return domain
|
return domain
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
|
/**
|
||||||
|
* Configures custom local DNS settings.
|
||||||
|
*
|
||||||
|
* Sets up DNS inbound, outbound, and routing rules for local DNS resolution.
|
||||||
|
*
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
* @return true if custom local DNS configuration was successful, false otherwise
|
||||||
|
*/
|
||||||
|
private fun getCustomLocalDns(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
||||||
val geositeCn = arrayListOf(GEOSITE_CN)
|
val geositeCn = arrayListOf(AppConfig.GEOSITE_CN)
|
||||||
val proxyDomain = userRule2Domain(TAG_PROXY)
|
val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
|
||||||
val directDomain = userRule2Domain(TAG_DIRECT)
|
val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
|
||||||
// fakedns with all domains to make it always top priority
|
// fakedns with all domains to make it always top priority
|
||||||
v2rayConfig.dns?.servers?.add(
|
v2rayConfig.dns?.servers?.add(
|
||||||
0,
|
0,
|
||||||
@@ -391,7 +388,7 @@ object V2rayConfigManager {
|
|||||||
V2rayConfig.InboundBean(
|
V2rayConfig.InboundBean(
|
||||||
tag = "dns-in",
|
tag = "dns-in",
|
||||||
port = localDnsPort,
|
port = localDnsPort,
|
||||||
listen = LOOPBACK,
|
listen = AppConfig.LOOPBACK,
|
||||||
protocol = "dokodemo-door",
|
protocol = "dokodemo-door",
|
||||||
settings = dnsInboundSettings,
|
settings = dnsInboundSettings,
|
||||||
sniffing = null
|
sniffing = null
|
||||||
@@ -427,14 +424,22 @@ object V2rayConfigManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dns(v2rayConfig: V2rayConfig): Boolean {
|
/**
|
||||||
|
* Configures the DNS settings for V2ray.
|
||||||
|
*
|
||||||
|
* Sets up DNS servers, hosts, and routing rules for DNS resolution.
|
||||||
|
*
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
* @return true if DNS configuration was successful, false otherwise
|
||||||
|
*/
|
||||||
|
private fun getDns(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
val hosts = mutableMapOf<String, Any>()
|
val hosts = mutableMapOf<String, Any>()
|
||||||
val servers = ArrayList<Any>()
|
val servers = ArrayList<Any>()
|
||||||
|
|
||||||
//remote Dns
|
//remote Dns
|
||||||
val remoteDns = SettingsManager.getRemoteDnsServers()
|
val remoteDns = SettingsManager.getRemoteDnsServers()
|
||||||
val proxyDomain = userRule2Domain(TAG_PROXY)
|
val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
|
||||||
remoteDns.forEach {
|
remoteDns.forEach {
|
||||||
servers.add(it)
|
servers.add(it)
|
||||||
}
|
}
|
||||||
@@ -449,9 +454,9 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
// domestic DNS
|
// domestic DNS
|
||||||
val domesticDns = SettingsManager.getDomesticDnsServers()
|
val domesticDns = SettingsManager.getDomesticDnsServers()
|
||||||
val directDomain = userRule2Domain(TAG_DIRECT)
|
val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
|
||||||
val isCnRoutingMode = directDomain.contains(GEOSITE_CN)
|
val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN)
|
||||||
val geoipCn = arrayListOf(GEOIP_CN)
|
val geoipCn = arrayListOf(AppConfig.GEOIP_CN)
|
||||||
if (directDomain.isNotEmpty()) {
|
if (directDomain.isNotEmpty()) {
|
||||||
servers.add(
|
servers.add(
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
@@ -466,7 +471,7 @@ object V2rayConfigManager {
|
|||||||
if (Utils.isPureIpAddress(domesticDns.first())) {
|
if (Utils.isPureIpAddress(domesticDns.first())) {
|
||||||
v2rayConfig.routing.rules.add(
|
v2rayConfig.routing.rules.add(
|
||||||
0, RulesBean(
|
0, RulesBean(
|
||||||
outboundTag = TAG_DIRECT,
|
outboundTag = AppConfig.TAG_DIRECT,
|
||||||
port = "53",
|
port = "53",
|
||||||
ip = arrayListOf(domesticDns.first()),
|
ip = arrayListOf(domesticDns.first()),
|
||||||
domain = null
|
domain = null
|
||||||
@@ -489,21 +494,21 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//block dns
|
//block dns
|
||||||
val blkDomain = userRule2Domain(TAG_BLOCKED)
|
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
|
||||||
if (blkDomain.isNotEmpty()) {
|
if (blkDomain.isNotEmpty()) {
|
||||||
hosts.putAll(blkDomain.map { it to LOOPBACK })
|
hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
|
||||||
}
|
}
|
||||||
|
|
||||||
// hardcode googleapi rule to fix play store problems
|
// hardcode googleapi rule to fix play store problems
|
||||||
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
|
hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN
|
||||||
|
|
||||||
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
||||||
hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
|
hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
|
||||||
hosts[DNS_CLOUDFLARE_DOMAIN] = DNS_CLOUDFLARE_ADDRESSES
|
hosts[AppConfig.DNS_CLOUDFLARE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ADDRESSES
|
||||||
hosts[DNS_DNSPOD_DOMAIN] = DNS_DNSPOD_ADDRESSES
|
hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES
|
||||||
hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
|
hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES
|
||||||
hosts[DNS_QUAD9_DOMAIN] = DNS_QUAD9_ADDRESSES
|
hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
|
||||||
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
|
hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
|
||||||
|
|
||||||
|
|
||||||
// DNS dns
|
// DNS dns
|
||||||
@@ -516,7 +521,7 @@ object V2rayConfigManager {
|
|||||||
if (Utils.isPureIpAddress(remoteDns.first())) {
|
if (Utils.isPureIpAddress(remoteDns.first())) {
|
||||||
v2rayConfig.routing.rules.add(
|
v2rayConfig.routing.rules.add(
|
||||||
0, RulesBean(
|
0, RulesBean(
|
||||||
outboundTag = TAG_PROXY,
|
outboundTag = AppConfig.TAG_PROXY,
|
||||||
port = "53",
|
port = "53",
|
||||||
ip = arrayListOf(remoteDns.first()),
|
ip = arrayListOf(remoteDns.first()),
|
||||||
domain = null
|
domain = null
|
||||||
@@ -530,6 +535,138 @@ object V2rayConfigManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region outbound related functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the primary outbound connection.
|
||||||
|
*
|
||||||
|
* Converts the profile to an outbound configuration and applies global settings.
|
||||||
|
*
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
* @param config The profile item containing connection details
|
||||||
|
* @return true if outbound configuration was successful, null if there was an error
|
||||||
|
*/
|
||||||
|
private fun getOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Boolean? {
|
||||||
|
val outbound = convertProfile2Outbound(config) ?: return null
|
||||||
|
val ret = updateOutboundWithGlobalSettings(outbound)
|
||||||
|
if (!ret) return null
|
||||||
|
|
||||||
|
if (v2rayConfig.outbounds.isNotEmpty()) {
|
||||||
|
v2rayConfig.outbounds[0] = outbound
|
||||||
|
} else {
|
||||||
|
v2rayConfig.outbounds.add(outbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOutboundFragment(v2rayConfig)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures special outbound settings for Hysteria2 protocol.
|
||||||
|
*
|
||||||
|
* Creates a SOCKS outbound connection on a free port for protocols requiring special handling.
|
||||||
|
*
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
* @param config The profile item containing connection details
|
||||||
|
* @return The port number for the SOCKS connection, or null if there was an error
|
||||||
|
*/
|
||||||
|
private fun getPlusOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Int? {
|
||||||
|
try {
|
||||||
|
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
|
||||||
|
|
||||||
|
val outboundNew = OutboundBean(
|
||||||
|
mux = null,
|
||||||
|
protocol = EConfigType.SOCKS.name.lowercase(),
|
||||||
|
settings = OutSettingsBean(
|
||||||
|
servers = listOf(
|
||||||
|
OutSettingsBean.ServersBean(
|
||||||
|
address = AppConfig.LOOPBACK,
|
||||||
|
port = socksPort
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (v2rayConfig.outbounds.isNotEmpty()) {
|
||||||
|
v2rayConfig.outbounds[0] = outboundNew
|
||||||
|
} else {
|
||||||
|
v2rayConfig.outbounds.add(outboundNew)
|
||||||
|
}
|
||||||
|
|
||||||
|
return socksPort
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to configure plusOutbound", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures additional outbound connections for proxy chaining.
|
||||||
|
*
|
||||||
|
* Sets up previous and next proxies in a subscription for advanced routing capabilities.
|
||||||
|
*
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
* @param subscriptionId The subscription ID to look up related proxies
|
||||||
|
* @return true if additional outbounds were configured successfully, false otherwise
|
||||||
|
*/
|
||||||
|
private fun getMoreOutbounds(v2rayConfig: V2rayConfig, subscriptionId: String): Boolean {
|
||||||
|
//fragment proxy
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptionId.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return false
|
||||||
|
|
||||||
|
//current proxy
|
||||||
|
val outbound = v2rayConfig.outbounds[0]
|
||||||
|
|
||||||
|
//Previous proxy
|
||||||
|
val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
|
||||||
|
if (prevNode != null) {
|
||||||
|
val prevOutbound = convertProfile2Outbound(prevNode)
|
||||||
|
if (prevOutbound != null) {
|
||||||
|
updateOutboundWithGlobalSettings(prevOutbound)
|
||||||
|
prevOutbound.tag = AppConfig.TAG_PROXY + "2"
|
||||||
|
v2rayConfig.outbounds.add(prevOutbound)
|
||||||
|
outbound.ensureSockopt().dialerProxy = prevOutbound.tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Next proxy
|
||||||
|
val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
|
||||||
|
if (nextNode != null) {
|
||||||
|
val nextOutbound = convertProfile2Outbound(nextNode)
|
||||||
|
if (nextOutbound != null) {
|
||||||
|
updateOutboundWithGlobalSettings(nextOutbound)
|
||||||
|
nextOutbound.tag = AppConfig.TAG_PROXY
|
||||||
|
v2rayConfig.outbounds.add(0, nextOutbound)
|
||||||
|
outbound.tag = AppConfig.TAG_PROXY + "1"
|
||||||
|
nextOutbound.ensureSockopt().dialerProxy = outbound.tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to configure more outbounds", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates outbound settings based on global preferences.
|
||||||
|
*
|
||||||
|
* Applies multiplexing and protocol-specific settings to an outbound connection.
|
||||||
|
*
|
||||||
|
* @param outbound The outbound connection to update
|
||||||
|
* @return true if the update was successful, false otherwise
|
||||||
|
*/
|
||||||
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
|
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
|
||||||
try {
|
try {
|
||||||
var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
|
var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
|
||||||
@@ -561,7 +698,7 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
var localTunAddr = if (outbound.settings?.address == null) {
|
var localTunAddr = if (outbound.settings?.address == null) {
|
||||||
listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6)
|
listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
|
||||||
} else {
|
} else {
|
||||||
outbound.settings?.address as List<*>
|
outbound.settings?.address as List<*>
|
||||||
}
|
}
|
||||||
@@ -571,8 +708,8 @@ object V2rayConfigManager {
|
|||||||
outbound.settings?.address = localTunAddr
|
outbound.settings?.address = localTunAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outbound.streamSettings?.network == DEFAULT_NETWORK
|
if (outbound.streamSettings?.network == AppConfig.DEFAULT_NETWORK
|
||||||
&& outbound.streamSettings?.tcpSettings?.header?.type == HEADER_TYPE_HTTP
|
&& outbound.streamSettings?.tcpSettings?.header?.type == AppConfig.HEADER_TYPE_HTTP
|
||||||
) {
|
) {
|
||||||
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
|
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
|
||||||
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
||||||
@@ -582,7 +719,7 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
||||||
requestString,
|
requestString,
|
||||||
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
||||||
)
|
)
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request?.path =
|
outbound.streamSettings?.tcpSettings?.header?.request?.path =
|
||||||
if (path.isNullOrEmpty()) {
|
if (path.isNullOrEmpty()) {
|
||||||
@@ -601,6 +738,14 @@ object V2rayConfigManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the outbound with fragment settings for traffic optimization.
|
||||||
|
*
|
||||||
|
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
|
||||||
|
*
|
||||||
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
|
* @return true if fragment configuration was successful, false otherwise
|
||||||
|
*/
|
||||||
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
|
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
||||||
@@ -614,8 +759,8 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
val fragmentOutbound =
|
val fragmentOutbound =
|
||||||
V2rayConfig.OutboundBean(
|
V2rayConfig.OutboundBean(
|
||||||
protocol = PROTOCOL_FREEDOM,
|
protocol = AppConfig.PROTOCOL_FREEDOM,
|
||||||
tag = TAG_FRAGMENT,
|
tag = AppConfig.TAG_FRAGMENT,
|
||||||
mux = null
|
mux = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -631,8 +776,8 @@ object V2rayConfigManager {
|
|||||||
packets = "tlshello"
|
packets = "tlshello"
|
||||||
}
|
}
|
||||||
|
|
||||||
fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
fragmentOutbound.settings = OutboundBean.OutSettingsBean(
|
||||||
fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean(
|
fragment = OutboundBean.OutSettingsBean.FragmentBean(
|
||||||
packets = packets,
|
packets = packets,
|
||||||
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
||||||
?: "50-100",
|
?: "50-100",
|
||||||
@@ -640,15 +785,15 @@ object V2rayConfigManager {
|
|||||||
?: "10-20"
|
?: "10-20"
|
||||||
),
|
),
|
||||||
noises = listOf(
|
noises = listOf(
|
||||||
V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
|
OutboundBean.OutSettingsBean.NoiseBean(
|
||||||
type = "rand",
|
type = "rand",
|
||||||
packet = "10-20",
|
packet = "10-20",
|
||||||
delay = "10-16",
|
delay = "10-16",
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
|
fragmentOutbound.streamSettings = StreamSettingsBean(
|
||||||
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
sockopt = StreamSettingsBean.SockoptBean(
|
||||||
TcpNoDelay = true,
|
TcpNoDelay = true,
|
||||||
mark = 255
|
mark = 255
|
||||||
)
|
)
|
||||||
@@ -657,8 +802,8 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
//proxy chain
|
//proxy chain
|
||||||
v2rayConfig.outbounds[0].streamSettings?.sockopt =
|
v2rayConfig.outbounds[0].streamSettings?.sockopt =
|
||||||
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
StreamSettingsBean.SockoptBean(
|
||||||
dialerProxy = TAG_FRAGMENT
|
dialerProxy = AppConfig.TAG_FRAGMENT
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
Log.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
||||||
@@ -667,80 +812,47 @@ object V2rayConfigManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun moreOutbounds(
|
/**
|
||||||
v2rayConfig: V2rayConfig,
|
* Resolves domain names to IP addresses in outbound connections.
|
||||||
subscriptionId: String,
|
*
|
||||||
isPlugin: Boolean
|
* Pre-resolves domains to improve connection speed and reliability.
|
||||||
): Pair<Boolean, String> {
|
*
|
||||||
val returnPair = Pair(false, "")
|
* @param v2rayConfig The V2ray configuration object to be modified
|
||||||
var domainPort: String = ""
|
*/
|
||||||
|
private fun resolveOutboundDomainsToHosts(v2rayConfig: V2rayConfig) {
|
||||||
|
val proxyOutboundList = v2rayConfig.getAllProxyOutbound()
|
||||||
|
val dns = v2rayConfig.dns ?: return
|
||||||
|
val newHosts = dns.hosts?.toMutableMap() ?: mutableMapOf()
|
||||||
|
val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true
|
||||||
|
|
||||||
if (isPlugin) {
|
for (item in proxyOutboundList) {
|
||||||
return returnPair
|
val domain = item.getServerAddress()
|
||||||
}
|
if (domain.isNullOrEmpty()) continue
|
||||||
//fragment proxy
|
if (newHosts.containsKey(domain)) continue
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
|
|
||||||
return returnPair
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscriptionId.isEmpty()) {
|
val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6)
|
||||||
return returnPair
|
if (resolvedIps.isNullOrEmpty()) continue
|
||||||
}
|
|
||||||
try {
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return returnPair
|
|
||||||
|
|
||||||
//current proxy
|
item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6"
|
||||||
val outbound = v2rayConfig.outbounds[0]
|
newHosts[domain] = if (resolvedIps.size == 1) {
|
||||||
|
resolvedIps[0]
|
||||||
//Previous proxy
|
} else {
|
||||||
val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
|
resolvedIps
|
||||||
if (prevNode != null) {
|
|
||||||
val prevOutbound = getProxyOutbound(prevNode)
|
|
||||||
if (prevOutbound != null) {
|
|
||||||
updateOutboundWithGlobalSettings(prevOutbound)
|
|
||||||
prevOutbound.tag = TAG_PROXY + "2"
|
|
||||||
v2rayConfig.outbounds.add(prevOutbound)
|
|
||||||
outbound.streamSettings?.sockopt =
|
|
||||||
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
|
||||||
dialerProxy = prevOutbound.tag
|
|
||||||
)
|
|
||||||
domainPort = prevNode.getServerAddressAndPort()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Next proxy
|
dns.hosts = newHosts
|
||||||
val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
|
|
||||||
if (nextNode != null) {
|
|
||||||
val nextOutbound = getProxyOutbound(nextNode)
|
|
||||||
if (nextOutbound != null) {
|
|
||||||
updateOutboundWithGlobalSettings(nextOutbound)
|
|
||||||
nextOutbound.tag = TAG_PROXY
|
|
||||||
v2rayConfig.outbounds.add(0, nextOutbound)
|
|
||||||
outbound.tag = TAG_PROXY + "1"
|
|
||||||
nextOutbound.streamSettings?.sockopt =
|
|
||||||
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
|
||||||
dialerProxy = outbound.tag
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to configure more outbounds", e)
|
|
||||||
return returnPair
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domainPort.isNotEmpty()) {
|
|
||||||
return Pair(true, domainPort)
|
|
||||||
}
|
|
||||||
return returnPair
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the proxy outbound configuration for the given profile item.
|
* Converts a profile item to an outbound configuration.
|
||||||
*
|
*
|
||||||
* @param profileItem The profile item for which to get the proxy outbound configuration.
|
* Creates appropriate outbound settings based on the protocol type.
|
||||||
* @return The proxy outbound configuration as a V2rayConfig.OutboundBean, or null if not found.
|
*
|
||||||
|
* @param profileItem The profile item to convert
|
||||||
|
* @return OutboundBean configuration for the profile, or null if not supported
|
||||||
*/
|
*/
|
||||||
fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
|
private fun convertProfile2Outbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
|
||||||
return when (profileItem.configType) {
|
return when (profileItem.configType) {
|
||||||
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
|
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
|
||||||
EConfigType.CUSTOM -> null
|
EConfigType.CUSTOM -> null
|
||||||
@@ -749,10 +861,216 @@ object V2rayConfigManager {
|
|||||||
EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
|
EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
|
||||||
EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
|
EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
|
||||||
EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
|
EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
|
||||||
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toOutbound(profileItem)
|
EConfigType.HYSTERIA2 -> null
|
||||||
EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
|
EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an initial outbound configuration for a specific protocol type.
|
||||||
|
*
|
||||||
|
* Provides a template configuration for different protocol types.
|
||||||
|
*
|
||||||
|
* @param configType The type of configuration to create
|
||||||
|
* @return An initial OutboundBean for the specified configuration type, or null for custom types
|
||||||
|
*/
|
||||||
|
fun createInitOutbound(configType: EConfigType): OutboundBean? {
|
||||||
|
return when (configType) {
|
||||||
|
EConfigType.VMESS,
|
||||||
|
EConfigType.VLESS ->
|
||||||
|
return OutboundBean(
|
||||||
|
protocol = configType.name.lowercase(),
|
||||||
|
settings = OutSettingsBean(
|
||||||
|
vnext = listOf(
|
||||||
|
OutSettingsBean.VnextBean(
|
||||||
|
users = listOf(OutSettingsBean.VnextBean.UsersBean())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
streamSettings = StreamSettingsBean()
|
||||||
|
)
|
||||||
|
|
||||||
|
EConfigType.SHADOWSOCKS,
|
||||||
|
EConfigType.SOCKS,
|
||||||
|
EConfigType.HTTP,
|
||||||
|
EConfigType.TROJAN,
|
||||||
|
EConfigType.HYSTERIA2 ->
|
||||||
|
return OutboundBean(
|
||||||
|
protocol = configType.name.lowercase(),
|
||||||
|
settings = OutSettingsBean(
|
||||||
|
servers = listOf(OutSettingsBean.ServersBean())
|
||||||
|
),
|
||||||
|
streamSettings = StreamSettingsBean()
|
||||||
|
)
|
||||||
|
|
||||||
|
EConfigType.WIREGUARD ->
|
||||||
|
return OutboundBean(
|
||||||
|
protocol = configType.name.lowercase(),
|
||||||
|
settings = OutSettingsBean(
|
||||||
|
secretKey = "",
|
||||||
|
peers = listOf(OutSettingsBean.WireGuardBean())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
EConfigType.CUSTOM -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures transport settings for an outbound connection.
|
||||||
|
*
|
||||||
|
* Sets up protocol-specific transport options based on the profile settings.
|
||||||
|
*
|
||||||
|
* @param streamSettings The stream settings to configure
|
||||||
|
* @param profileItem The profile containing transport configuration
|
||||||
|
* @return The Server Name Indication (SNI) value to use, or null if not applicable
|
||||||
|
*/
|
||||||
|
fun populateTransportSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem): String? {
|
||||||
|
val transport = profileItem.network.orEmpty()
|
||||||
|
val headerType = profileItem.headerType
|
||||||
|
val host = profileItem.host
|
||||||
|
val path = profileItem.path
|
||||||
|
val seed = profileItem.seed
|
||||||
|
// val quicSecurity = profileItem.quicSecurity
|
||||||
|
// val key = profileItem.quicKey
|
||||||
|
val mode = profileItem.mode
|
||||||
|
val serviceName = profileItem.serviceName
|
||||||
|
val authority = profileItem.authority
|
||||||
|
val xhttpMode = profileItem.xhttpMode
|
||||||
|
val xhttpExtra = profileItem.xhttpExtra
|
||||||
|
|
||||||
|
var sni: String? = null
|
||||||
|
streamSettings.network = if (transport.isEmpty()) NetworkType.TCP.type else transport
|
||||||
|
when (streamSettings.network) {
|
||||||
|
NetworkType.TCP.type -> {
|
||||||
|
val tcpSetting = StreamSettingsBean.TcpSettingsBean()
|
||||||
|
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
||||||
|
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
||||||
|
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
||||||
|
val requestObj = StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
|
||||||
|
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
tcpSetting.header.request = requestObj
|
||||||
|
sni = requestObj.headers.Host?.getOrNull(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tcpSetting.header.type = "none"
|
||||||
|
sni = host
|
||||||
|
}
|
||||||
|
streamSettings.tcpSettings = tcpSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.KCP.type -> {
|
||||||
|
val kcpsetting = StreamSettingsBean.KcpSettingsBean()
|
||||||
|
kcpsetting.header.type = headerType ?: "none"
|
||||||
|
if (seed.isNullOrEmpty()) {
|
||||||
|
kcpsetting.seed = null
|
||||||
|
} else {
|
||||||
|
kcpsetting.seed = seed
|
||||||
|
}
|
||||||
|
if (host.isNullOrEmpty()) {
|
||||||
|
kcpsetting.header.domain = null
|
||||||
|
} else {
|
||||||
|
kcpsetting.header.domain = host
|
||||||
|
}
|
||||||
|
streamSettings.kcpSettings = kcpsetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.WS.type -> {
|
||||||
|
val wssetting = StreamSettingsBean.WsSettingsBean()
|
||||||
|
wssetting.headers.Host = host.orEmpty()
|
||||||
|
sni = host
|
||||||
|
wssetting.path = path ?: "/"
|
||||||
|
streamSettings.wsSettings = wssetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.HTTP_UPGRADE.type -> {
|
||||||
|
val httpupgradeSetting = StreamSettingsBean.HttpupgradeSettingsBean()
|
||||||
|
httpupgradeSetting.host = host.orEmpty()
|
||||||
|
sni = host
|
||||||
|
httpupgradeSetting.path = path ?: "/"
|
||||||
|
streamSettings.httpupgradeSettings = httpupgradeSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.XHTTP.type -> {
|
||||||
|
val xhttpSetting = StreamSettingsBean.XhttpSettingsBean()
|
||||||
|
xhttpSetting.host = host.orEmpty()
|
||||||
|
sni = host
|
||||||
|
xhttpSetting.path = path ?: "/"
|
||||||
|
xhttpSetting.mode = xhttpMode
|
||||||
|
xhttpSetting.extra = JsonUtil.parseString(xhttpExtra)
|
||||||
|
streamSettings.xhttpSettings = xhttpSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.H2.type, NetworkType.HTTP.type -> {
|
||||||
|
streamSettings.network = NetworkType.H2.type
|
||||||
|
val h2Setting = StreamSettingsBean.HttpSettingsBean()
|
||||||
|
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
sni = h2Setting.host.getOrNull(0)
|
||||||
|
h2Setting.path = path ?: "/"
|
||||||
|
streamSettings.httpSettings = h2Setting
|
||||||
|
}
|
||||||
|
|
||||||
|
// "quic" -> {
|
||||||
|
// val quicsetting = QuicSettingBean()
|
||||||
|
// quicsetting.security = quicSecurity ?: "none"
|
||||||
|
// quicsetting.key = key.orEmpty()
|
||||||
|
// quicsetting.header.type = headerType ?: "none"
|
||||||
|
// quicSettings = quicsetting
|
||||||
|
// }
|
||||||
|
|
||||||
|
NetworkType.GRPC.type -> {
|
||||||
|
val grpcSetting = StreamSettingsBean.GrpcSettingsBean()
|
||||||
|
grpcSetting.multiMode = mode == "multi"
|
||||||
|
grpcSetting.serviceName = serviceName.orEmpty()
|
||||||
|
grpcSetting.authority = authority.orEmpty()
|
||||||
|
grpcSetting.idle_timeout = 60
|
||||||
|
grpcSetting.health_check_timeout = 20
|
||||||
|
sni = authority
|
||||||
|
streamSettings.grpcSettings = grpcSetting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sni
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures TLS or REALITY security settings for an outbound connection.
|
||||||
|
*
|
||||||
|
* Sets up security-related parameters like certificates, fingerprints, and SNI.
|
||||||
|
*
|
||||||
|
* @param streamSettings The stream settings to configure
|
||||||
|
* @param profileItem The profile containing security configuration
|
||||||
|
* @param sniExt An external SNI value to use if the profile doesn't specify one
|
||||||
|
*/
|
||||||
|
fun populateTlsSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
|
||||||
|
val streamSecurity = profileItem.security.orEmpty()
|
||||||
|
val allowInsecure = profileItem.insecure == true
|
||||||
|
val sni = if (profileItem.sni.isNullOrEmpty()) sniExt else profileItem.sni
|
||||||
|
val fingerprint = profileItem.fingerPrint
|
||||||
|
val alpns = profileItem.alpn
|
||||||
|
val publicKey = profileItem.publicKey
|
||||||
|
val shortId = profileItem.shortId
|
||||||
|
val spiderX = profileItem.spiderX
|
||||||
|
|
||||||
|
streamSettings.security = if (streamSecurity.isEmpty()) null else streamSecurity
|
||||||
|
if (streamSettings.security == null) return
|
||||||
|
val tlsSetting = StreamSettingsBean.TlsSettingsBean(
|
||||||
|
allowInsecure = allowInsecure,
|
||||||
|
serverName = if (sni.isNullOrEmpty()) null else sni,
|
||||||
|
fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
|
||||||
|
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
||||||
|
publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
|
||||||
|
shortId = if (shortId.isNullOrEmpty()) null else shortId,
|
||||||
|
spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
|
||||||
|
)
|
||||||
|
if (streamSettings.security == AppConfig.TLS) {
|
||||||
|
streamSettings.tlsSettings = tlsSetting
|
||||||
|
streamSettings.realitySettings = null
|
||||||
|
} else if (streamSettings.security == AppConfig.REALITY) {
|
||||||
|
streamSettings.tlsSettings = null
|
||||||
|
streamSettings.realitySettings = tlsSetting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
|
|||||||
addUpdateListener { animation ->
|
addUpdateListener { animation ->
|
||||||
val value = animation.animatedValue as Float
|
val value = animation.animatedValue as Float
|
||||||
viewHolder.itemView.translationX = value
|
viewHolder.itemView.translationX = value
|
||||||
viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD))
|
viewHolder.itemView.alpha = 1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD)
|
||||||
}
|
}
|
||||||
interpolator = DecelerateInterpolator()
|
interpolator = DecelerateInterpolator()
|
||||||
duration = ANIMATION_DURATION
|
duration = ANIMATION_DURATION
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ object NotificationService {
|
|||||||
mBuilder = null
|
mBuilder = null
|
||||||
speedNotificationJob?.cancel()
|
speedNotificationJob?.cancel()
|
||||||
speedNotificationJob = null
|
speedNotificationJob = null
|
||||||
|
mNotificationManager = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,7 +27,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 {
|
||||||
V2RayServiceManager.startV2rayPoint()
|
V2RayServiceManager.startCoreLoop()
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
|||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
V2RayServiceManager.stopV2rayPoint()
|
V2RayServiceManager.stopCoreLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.v2ray.ang.dto.ProfileItem
|
|||||||
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.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.PluginUtil
|
import com.v2ray.ang.util.PluginUtil
|
||||||
@@ -23,14 +24,14 @@ import go.Seq
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import libv2ray.CoreCallbackHandler
|
||||||
|
import libv2ray.CoreController
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import libv2ray.V2RayPoint
|
|
||||||
import libv2ray.V2RayVPNServiceSupportsSet
|
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
|
|
||||||
object V2RayServiceManager {
|
object V2RayServiceManager {
|
||||||
|
|
||||||
private val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback())
|
||||||
private val mMsgReceive = ReceiveMessageHandler()
|
private val mMsgReceive = ReceiveMessageHandler()
|
||||||
private var currentConfig: ProfileItem? = null
|
private var currentConfig: ProfileItem? = null
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ object V2RayServiceManager {
|
|||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
||||||
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +81,7 @@ object V2RayServiceManager {
|
|||||||
* Checks if the V2Ray service is running.
|
* Checks if the V2Ray service is running.
|
||||||
* @return True if the service is running, false otherwise.
|
* @return True if the service is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isRunning() = v2rayPoint.isRunning
|
fun isRunning() = coreController.isRunning
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the name of the currently running server.
|
* Gets the name of the currently running server.
|
||||||
@@ -90,10 +91,13 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the context service for V2Ray.
|
* Starts the context service for V2Ray.
|
||||||
|
* Chooses between VPN service or Proxy-only service based on user settings.
|
||||||
* @param context The context from which the service is started.
|
* @param context The context from which the service is started.
|
||||||
*/
|
*/
|
||||||
private fun startContextService(context: Context) {
|
private fun startContextService(context: Context) {
|
||||||
if (v2rayPoint.isRunning) return
|
if (coreController.isRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val guid = MmkvManager.getSelectServer() ?: return
|
val guid = MmkvManager.getSelectServer() ?: return
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||||
if (config.configType != EConfigType.CUSTOM
|
if (config.configType != EConfigType.CUSTOM
|
||||||
@@ -123,18 +127,19 @@ object V2RayServiceManager {
|
|||||||
/**
|
/**
|
||||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
* Starts the V2Ray point.
|
* Starts the V2Ray core service.
|
||||||
*/
|
*/
|
||||||
fun startV2rayPoint() {
|
fun startCoreLoop(): Boolean {
|
||||||
val service = getService() ?: return
|
if (coreController.isRunning) {
|
||||||
val guid = MmkvManager.getSelectServer() ?: return
|
return false
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
|
||||||
if (v2rayPoint.isRunning) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val service = getService() ?: return false
|
||||||
|
val guid = MmkvManager.getSelectServer() ?: return false
|
||||||
|
val config = MmkvManager.decodeServerConfig(guid) ?: return false
|
||||||
val result = V2rayConfigManager.getV2rayConfig(service, guid)
|
val result = V2rayConfigManager.getV2rayConfig(service, guid)
|
||||||
if (!result.status)
|
if (!result.status)
|
||||||
return
|
return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||||
@@ -144,39 +149,49 @@ object V2RayServiceManager {
|
|||||||
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
|
Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
v2rayPoint.configureFileContent = result.content
|
|
||||||
v2rayPoint.domainName = result.domainPort
|
|
||||||
currentConfig = config
|
currentConfig = config
|
||||||
|
|
||||||
try {
|
try {
|
||||||
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
coreController.startLoop(result.content)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to start V2Ray loop", e)
|
Log.e(AppConfig.TAG, "Failed to start Core loop", e)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (v2rayPoint.isRunning) {
|
if (coreController.isRunning == false) {
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
|
||||||
NotificationService.showNotification(currentConfig)
|
|
||||||
|
|
||||||
PluginUtil.runPlugin(service, config, result.domainPort)
|
|
||||||
} else {
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
||||||
NotificationService.cancelNotification()
|
NotificationService.cancelNotification()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||||
|
NotificationService.showNotification(currentConfig)
|
||||||
|
NotificationService.startSpeedNotification(currentConfig)
|
||||||
|
|
||||||
|
PluginUtil.runPlugin(service, config, result.socksPort)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to startup service", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the V2Ray point.
|
* Stops the V2Ray core service.
|
||||||
|
* Unregisters broadcast receivers, stops notifications, and shuts down plugins.
|
||||||
|
* @return True if the core was stopped successfully, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun stopV2rayPoint() {
|
fun stopCoreLoop(): Boolean {
|
||||||
val service = getService() ?: return
|
val service = getService() ?: return false
|
||||||
|
|
||||||
if (v2rayPoint.isRunning) {
|
if (coreController.isRunning) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
v2rayPoint.stopLoop()
|
coreController.stopLoop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
|
Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
|
||||||
}
|
}
|
||||||
@@ -192,6 +207,8 @@ object V2RayServiceManager {
|
|||||||
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
|
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
|
||||||
}
|
}
|
||||||
PluginUtil.stopPlugin()
|
PluginUtil.stopPlugin()
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,40 +218,52 @@ object V2RayServiceManager {
|
|||||||
* @return The statistics value.
|
* @return The statistics value.
|
||||||
*/
|
*/
|
||||||
fun queryStats(tag: String, link: String): Long {
|
fun queryStats(tag: String, link: String): Long {
|
||||||
return v2rayPoint.queryStats(tag, link)
|
return coreController.queryStats(tag, link)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measures the delay for V2Ray.
|
* Measures the connection delay for the current V2Ray configuration.
|
||||||
|
* Tests with primary URL first, then falls back to alternative URL if needed.
|
||||||
|
* Also fetches remote IP information if the delay test was successful.
|
||||||
*/
|
*/
|
||||||
private fun measureV2rayDelay() {
|
private fun measureV2rayDelay() {
|
||||||
|
if (coreController.isRunning == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val service = getService() ?: return@launch
|
val service = getService() ?: return@launch
|
||||||
var time = -1L
|
var time = -1L
|
||||||
var errstr = ""
|
var errorStr = ""
|
||||||
if (v2rayPoint.isRunning) {
|
|
||||||
try {
|
try {
|
||||||
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl())
|
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
|
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
|
||||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
}
|
}
|
||||||
if (time == -1L) {
|
if (time == -1L) {
|
||||||
try {
|
try {
|
||||||
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl(true))
|
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
|
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
|
||||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
val result = if (time == -1L) {
|
|
||||||
service.getString(R.string.connection_test_error, errstr)
|
|
||||||
} else {
|
|
||||||
service.getString(R.string.connection_test_available, time)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val result = if (time >= 0) {
|
||||||
|
service.getString(R.string.connection_test_available, time)
|
||||||
|
} else {
|
||||||
|
service.getString(R.string.connection_test_error, errorStr)
|
||||||
|
}
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
||||||
|
|
||||||
|
// Only fetch IP info if the delay test was successful
|
||||||
|
if (time >= 0) {
|
||||||
|
SpeedtestManager.getRemoteIPInfo()?.let { ip ->
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,10 +275,25 @@ object V2RayServiceManager {
|
|||||||
return serviceControl?.get()?.getService()
|
return serviceControl?.get()?.getService()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class V2RayCallback : V2RayVPNServiceSupportsSet {
|
/**
|
||||||
|
* Core callback handler implementation for handling V2Ray core events.
|
||||||
|
* Handles startup, shutdown, socket protection, and status emission.
|
||||||
|
*/
|
||||||
|
private class CoreCallback : CoreCallbackHandler {
|
||||||
|
/**
|
||||||
|
* Called when V2Ray core starts up.
|
||||||
|
* @return 0 for success, any other value for failure.
|
||||||
|
*/
|
||||||
|
override fun startup(): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when V2Ray core shuts down.
|
||||||
|
* @return 0 for success, any other value for failure.
|
||||||
|
*/
|
||||||
override fun shutdown(): Long {
|
override fun shutdown(): Long {
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
val serviceControl = serviceControl?.get() ?: return -1
|
||||||
// called by go
|
|
||||||
return try {
|
return try {
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
0
|
0
|
||||||
@@ -259,46 +303,25 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prepare(): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun protect(l: Long): Boolean {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return true
|
|
||||||
return serviceControl.vpnProtect(l.toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by Go to emit status.
|
* Called when V2Ray core emits status information.
|
||||||
* @param l The status code.
|
* @param l Status code.
|
||||||
* @param s The status message.
|
* @param s Status message.
|
||||||
* @return The status code.
|
* @return Always returns 0.
|
||||||
*/
|
*/
|
||||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
override fun onEmitStatus(l: Long, s: String?): Long {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by Go to set up the service.
|
* Broadcast receiver for handling messages sent to the service.
|
||||||
* @param s The setup string.
|
* Handles registration, service control, and screen events.
|
||||||
* @return The status code.
|
|
||||||
*/
|
*/
|
||||||
override fun setup(s: String): Long {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
|
||||||
return try {
|
|
||||||
serviceControl.startService()
|
|
||||||
NotificationService.startSpeedNotification(currentConfig)
|
|
||||||
0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to setup service in callback", e)
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ReceiveMessageHandler : BroadcastReceiver() {
|
private class ReceiveMessageHandler : BroadcastReceiver() {
|
||||||
/**
|
/**
|
||||||
* Handles received broadcast messages.
|
* Handles received broadcast messages.
|
||||||
|
* Processes service control messages and screen state changes.
|
||||||
* @param ctx The context in which the receiver is running.
|
* @param ctx The context in which the receiver is running.
|
||||||
* @param intent The intent being received.
|
* @param intent The intent being received.
|
||||||
*/
|
*/
|
||||||
@@ -306,7 +329,7 @@ object V2RayServiceManager {
|
|||||||
val serviceControl = serviceControl?.get() ?: return
|
val serviceControl = serviceControl?.get() ?: return
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
when (intent?.getIntExtra("key", 0)) {
|
||||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
AppConfig.MSG_REGISTER_CLIENT -> {
|
||||||
if (v2rayPoint.isRunning) {
|
if (coreController.isRunning) {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
||||||
} else {
|
} else {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class V2RayTestService : Service() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Seq.setContext(this)
|
Seq.setContext(this)
|
||||||
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ 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
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
|
||||||
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.MyContextWrapper
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
@@ -105,7 +104,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
V2RayServiceManager.startV2rayPoint()
|
if (V2RayServiceManager.startCoreLoop()) {
|
||||||
|
startService()
|
||||||
|
}
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
//return super.onStartCommand(intent, flags, startId)
|
//return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
@@ -166,7 +167,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
val bypassLan = SettingsManager.routingRulesetsBypassLan()
|
val bypassLan = SettingsManager.routingRulesetsBypassLan()
|
||||||
if (bypassLan) {
|
if (bypassLan) {
|
||||||
resources.getStringArray(R.array.bypass_private_ip_address).forEach {
|
AppConfig.BYPASS_PRIVATE_IP_LIST.forEach {
|
||||||
val addr = it.split('/')
|
val addr = it.split('/')
|
||||||
builder.addRoute(addr[0], addr[1].toInt())
|
builder.addRoute(addr[0], addr[1].toInt())
|
||||||
}
|
}
|
||||||
@@ -256,6 +257,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
* Starts the tun2socks process with the appropriate parameters.
|
* Starts the tun2socks process with the appropriate parameters.
|
||||||
*/
|
*/
|
||||||
private fun runTun2socks() {
|
private fun runTun2socks() {
|
||||||
|
Log.i(AppConfig.TAG, "Start run $TUN2SOCKS")
|
||||||
val socksPort = SettingsManager.getSocksPort()
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
val cmd = arrayListOf(
|
val cmd = arrayListOf(
|
||||||
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||||
@@ -294,11 +296,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
runTun2socks()
|
runTun2socks()
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
Log.i(AppConfig.TAG, process.toString())
|
Log.i(AppConfig.TAG, "$TUN2SOCKS process info : ${process.toString()}")
|
||||||
|
|
||||||
sendFd()
|
sendFd()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to start tun2socks process", e)
|
Log.e(AppConfig.TAG, "Failed to start $TUN2SOCKS process", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,13 +311,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
private fun sendFd() {
|
private fun sendFd() {
|
||||||
val fd = mInterface.fileDescriptor
|
val fd = mInterface.fileDescriptor
|
||||||
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
||||||
Log.i(AppConfig.TAG, path)
|
Log.i(AppConfig.TAG, "LocalSocket path : $path")
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
var tries = 0
|
var tries = 0
|
||||||
while (true) try {
|
while (true) try {
|
||||||
Thread.sleep(50L shl tries)
|
Thread.sleep(50L shl tries)
|
||||||
Log.i(AppConfig.TAG, "sendFd tries: $tries")
|
Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries")
|
||||||
LocalSocket().use { localSocket ->
|
LocalSocket().use { localSocket ->
|
||||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||||
@@ -349,13 +351,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Log.i(AppConfig.TAG, "tun2socks destroy")
|
Log.i(AppConfig.TAG, "$TUN2SOCKS destroy")
|
||||||
process.destroy()
|
process.destroy()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to destroy tun2socks process", e)
|
Log.e(AppConfig.TAG, "Failed to destroy $TUN2SOCKS process", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
V2RayServiceManager.stopV2rayPoint()
|
V2RayServiceManager.stopCoreLoop()
|
||||||
|
|
||||||
if (isForced) {
|
if (isForced) {
|
||||||
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
|
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
|
||||||
|
|||||||
@@ -5,20 +5,28 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityAboutBinding
|
import com.v2ray.ang.databinding.ActivityAboutBinding
|
||||||
|
import com.v2ray.ang.dto.CheckUpdateResult
|
||||||
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.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SpeedtestManager
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
|
import com.v2ray.ang.handler.UpdateCheckerManager
|
||||||
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.ZipUtil
|
import com.v2ray.ang.util.ZipUtil
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -97,12 +105,29 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//If it is the Google Play version, not be displayed within 1 days after update
|
||||||
|
// if (Utils.isGoogleFlavor()) {
|
||||||
|
// val lastUpdateTime = AppManagerUtil.getLastUpdateTime(this)
|
||||||
|
// val currentTime = System.currentTimeMillis()
|
||||||
|
// if ((currentTime - lastUpdateTime) < 1 * 24 * 60 * 60 * 1000L) {
|
||||||
|
// binding.layoutCheckUpdate.visibility = View.GONE
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
binding.layoutCheckUpdate.setOnClickListener {
|
||||||
|
checkForUpdates(binding.checkPreRelease.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked)
|
||||||
|
}
|
||||||
|
binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false)
|
||||||
|
|
||||||
binding.layoutSoureCcode.setOnClickListener {
|
binding.layoutSoureCcode.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGUrl)
|
Utils.openUri(this, AppConfig.APP_URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutFeedback.setOnClickListener {
|
binding.layoutFeedback.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
Utils.openUri(this, AppConfig.APP_ISSUES_URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutOssLicenses.setOnClickListener {
|
binding.layoutOssLicenses.setOnClickListener {
|
||||||
@@ -116,11 +141,11 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutTgChannel.setOnClickListener {
|
binding.layoutTgChannel.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.TgChannelUrl)
|
Utils.openUri(this, AppConfig.TG_CHANNEL_URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutPrivacyPolicy.setOnClickListener {
|
binding.layoutPrivacyPolicy.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
|
Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY)
|
||||||
}
|
}
|
||||||
|
|
||||||
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
||||||
@@ -197,4 +222,28 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkForUpdates(includePreRelease: Boolean) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
|
||||||
|
if (result.hasUpdate) {
|
||||||
|
showUpdateDialog(result)
|
||||||
|
} else {
|
||||||
|
toast(R.string.update_already_latest_version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUpdateDialog(result: CheckUpdateResult) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.update_new_version_found, result.latestVersion))
|
||||||
|
.setMessage(result.releaseNotes)
|
||||||
|
.setPositiveButton(R.string.update_now) { _, _ ->
|
||||||
|
result.downloadUrl?.let {
|
||||||
|
Utils.openUri(this, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -87,9 +87,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
Action.IMPORT_QR_CODE_CONFIG ->
|
Action.IMPORT_QR_CODE_CONFIG ->
|
||||||
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||||
|
|
||||||
// Action.IMPORT_QR_CODE_URL ->
|
|
||||||
// scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
|
|
||||||
Action.READ_CONTENT_FROM_URI ->
|
Action.READ_CONTENT_FROM_URI ->
|
||||||
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
|
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
type = "*/*"
|
type = "*/*"
|
||||||
@@ -110,8 +107,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
enum class Action {
|
enum class Action {
|
||||||
NONE,
|
NONE,
|
||||||
IMPORT_QR_CODE_CONFIG,
|
IMPORT_QR_CODE_CONFIG,
|
||||||
|
|
||||||
//IMPORT_QR_CODE_URL,
|
|
||||||
READ_CONTENT_FROM_URI,
|
READ_CONTENT_FROM_URI,
|
||||||
POST_NOTIFICATIONS
|
POST_NOTIFICATIONS
|
||||||
}
|
}
|
||||||
@@ -129,12 +124,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
// if (it.resultCode == RESULT_OK) {
|
|
||||||
// importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT"))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
@@ -325,7 +314,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.import_qrcode -> {
|
R.id.import_qrcode -> {
|
||||||
importQRcode(true)
|
importQRcode()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,26 +368,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// R.id.import_config_custom_clipboard -> {
|
|
||||||
// importConfigCustomClipboard()
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// R.id.import_config_custom_local -> {
|
|
||||||
// importConfigCustomLocal()
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// R.id.import_config_custom_url -> {
|
|
||||||
// importConfigCustomUrlClipboard()
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// R.id.import_config_custom_url_scan -> {
|
|
||||||
// importQRcode(false)
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
|
|
||||||
R.id.export_all -> {
|
R.id.export_all -> {
|
||||||
exportAll()
|
exportAll()
|
||||||
true
|
true
|
||||||
@@ -462,16 +431,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
/**
|
/**
|
||||||
* import config from qrcode
|
* import config from qrcode
|
||||||
*/
|
*/
|
||||||
private fun importQRcode(forConfig: Boolean): Boolean {
|
private fun importQRcode(): Boolean {
|
||||||
val permission = Manifest.permission.CAMERA
|
val permission = Manifest.permission.CAMERA
|
||||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
if (forConfig) {
|
|
||||||
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||||
} else {
|
} else {
|
||||||
//scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
pendingAction = Action.IMPORT_QR_CODE_CONFIG
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pendingAction = Action.IMPORT_QR_CODE_CONFIG//if (forConfig) Action.IMPORT_QR_CODE_CONFIG else Action.IMPORT_QR_CODE_URL
|
|
||||||
requestPermissionLauncher.launch(permission)
|
requestPermissionLauncher.launch(permission)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -535,77 +500,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// private fun importConfigCustomClipboard()
|
|
||||||
// : Boolean {
|
|
||||||
// try {
|
|
||||||
// val configText = Utils.getClipboard(this)
|
|
||||||
// if (TextUtils.isEmpty(configText)) {
|
|
||||||
// toast(R.string.toast_none_data_clipboard)
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// importCustomizeConfig(configText)
|
|
||||||
// return true
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* import config from local config file
|
|
||||||
*/
|
|
||||||
// private fun importConfigCustomLocal(): Boolean {
|
|
||||||
// try {
|
|
||||||
// showFileChooser()
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun importConfigCustomUrlClipboard()
|
|
||||||
// : Boolean {
|
|
||||||
// try {
|
|
||||||
// val url = Utils.getClipboard(this)
|
|
||||||
// if (TextUtils.isEmpty(url)) {
|
|
||||||
// toast(R.string.toast_none_data_clipboard)
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// return importConfigCustomUrl(url)
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* import config from url
|
|
||||||
*/
|
|
||||||
// private fun importConfigCustomUrl(url: String?): Boolean {
|
|
||||||
// try {
|
|
||||||
// if (!Utils.isValidUrl(url)) {
|
|
||||||
// toast(R.string.toast_invalid_url)
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
// val configText = try {
|
|
||||||
// HttpUtil.getUrlContentWithUserAgent(url)
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// ""
|
|
||||||
// }
|
|
||||||
// launch(Dispatchers.Main) {
|
|
||||||
// importCustomizeConfig(configText)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* import config from sub
|
* import config from sub
|
||||||
*/
|
*/
|
||||||
@@ -755,29 +649,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * import customize config
|
|
||||||
// */
|
|
||||||
// private fun importCustomizeConfig(server: String?) {
|
|
||||||
// try {
|
|
||||||
// if (server == null || TextUtils.isEmpty(server)) {
|
|
||||||
// toast(R.string.toast_none_data)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if (mainViewModel.appendCustomConfigServer(server)) {
|
|
||||||
// mainViewModel.reloadServerList()
|
|
||||||
// toastSuccess(R.string.toast_success)
|
|
||||||
// } else {
|
|
||||||
// toastError(R.string.toast_failure)
|
|
||||||
// }
|
|
||||||
// //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
|
||||||
// e.printStackTrace()
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
private fun setTestState(content: String?) {
|
private fun setTestState(content: String?) {
|
||||||
binding.tvTestState.text = content
|
binding.tvTestState.text = content
|
||||||
}
|
}
|
||||||
@@ -812,7 +683,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
.putExtra("isRunning", mainViewModel.isRunning.value == true)
|
.putExtra("isRunning", mainViewModel.isRunning.value == true)
|
||||||
)
|
)
|
||||||
|
|
||||||
R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}")
|
||||||
R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
|
R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
|
||||||
R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
|
R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.v2ray.ang.handler.MmkvManager
|
|||||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -220,12 +221,17 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
* @param guid The server unique identifier
|
* @param guid The server unique identifier
|
||||||
*/
|
*/
|
||||||
private fun shareFullContent(guid: String) {
|
private fun shareFullContent(guid: String) {
|
||||||
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
|
mActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
if (result == 0) {
|
||||||
mActivity.toastSuccess(R.string.toast_success)
|
mActivity.toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
mActivity.toastError(R.string.toast_failure)
|
mActivity.toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits server configuration
|
* Edits server configuration
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.text.TextUtils
|
|||||||
import android.util.Log
|
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 androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
@@ -21,16 +22,14 @@ 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.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
import es.dmoral.toasty.Toasty
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
|
||||||
class PerAppProxyActivity : BaseActivity() {
|
class PerAppProxyActivity : BaseActivity() {
|
||||||
private val binding by lazy {
|
private val binding by lazy { ActivityBypassListBinding.inflate(layoutInflater) }
|
||||||
ActivityBypassListBinding.inflate(layoutInflater)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private var adapter: PerAppProxyAdapter? = null
|
private var adapter: PerAppProxyAdapter? = null
|
||||||
private var appsAll: List<AppInfo>? = null
|
private var appsAll: List<AppInfo>? = null
|
||||||
@@ -86,6 +85,10 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
|
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
|
||||||
}
|
}
|
||||||
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
|
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
|
||||||
|
|
||||||
|
binding.layoutSwitchBypassAppsTips.setOnClickListener {
|
||||||
|
Toasty.info(this, R.string.summary_pref_per_app_proxy, Toast.LENGTH_LONG, true).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -157,7 +160,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
binding.pbWaiting.show()
|
binding.pbWaiting.show()
|
||||||
|
|
||||||
val url = AppConfig.androidpackagenamelistUrl
|
val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
var content = HttpUtil.getUrlContent(url, 5000)
|
var content = HttpUtil.getUrlContent(url, 5000)
|
||||||
if (content.isNullOrEmpty()) {
|
if (content.isNullOrEmpty()) {
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
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.widget.AdapterView
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -67,15 +65,9 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
|
|
||||||
val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "")
|
binding.tvDomainStrategySummary.text = getDomainStrategy()
|
||||||
found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) }
|
binding.layoutDomainStrategy.setOnClickListener {
|
||||||
binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
setDomainStrategy()
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, routing_domain_strategy[position])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +90,22 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getDomainStrategy(): String {
|
||||||
|
return MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: routing_domain_strategy.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setDomainStrategy() {
|
||||||
|
android.app.AlertDialog.Builder(this).setItems(routing_domain_strategy.asList().toTypedArray()) { _, i ->
|
||||||
|
try {
|
||||||
|
val value = routing_domain_strategy[i]
|
||||||
|
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value)
|
||||||
|
binding.tvDomainStrategySummary.text = value
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to set domain strategy", e)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun importPredefined() {
|
private fun importPredefined() {
|
||||||
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
||||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||||
|
|||||||
@@ -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 android.view.View
|
import android.view.View
|
||||||
@@ -19,7 +18,6 @@ import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
|
|||||||
import com.v2ray.ang.AppConfig.REALITY
|
import com.v2ray.ang.AppConfig.REALITY
|
||||||
import com.v2ray.ang.AppConfig.TLS
|
import com.v2ray.ang.AppConfig.TLS
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
@@ -328,7 +326,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
|
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
|
||||||
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
|
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
|
||||||
et_local_address?.text = Utils.getEditable(
|
et_local_address?.text = Utils.getEditable(
|
||||||
config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6"
|
config.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
)
|
)
|
||||||
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
|
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
|
||||||
} else if (config.configType == EConfigType.HYSTERIA2) {
|
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
@@ -421,7 +419,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
et_public_key?.text = null
|
et_public_key?.text = null
|
||||||
et_reserved1?.text = Utils.getEditable("0,0,0")
|
et_reserved1?.text = Utils.getEditable("0,0,0")
|
||||||
et_local_address?.text =
|
et_local_address?.text =
|
||||||
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
|
Utils.getEditable(WIREGUARD_LOCAL_ADDRESS_V4)
|
||||||
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
|
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
|
||||||
val nval = any as String
|
val nval = any as String
|
||||||
delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval
|
delayTestUrl?.summary = if (nval == "") AppConfig.DELAY_TEST_URL else nval
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mode?.setOnPreferenceChangeListener { _, newValue ->
|
mode?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
@@ -202,7 +202,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
||||||
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
|
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
|
||||||
dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
||||||
delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
|
delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL)
|
||||||
|
|
||||||
initSharedPreference()
|
initSharedPreference()
|
||||||
}
|
}
|
||||||
@@ -364,6 +364,6 @@ class SettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onModeHelpClicked(view: View) {
|
fun onModeHelpClicked(view: View) {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGWikiMode)
|
Utils.openUri(this, AppConfig.APP_WIKI_MODE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class SubEditActivity : BaseActivity() {
|
|||||||
binding.etFilter.text = Utils.getEditable(subItem.filter)
|
binding.etFilter.text = Utils.getEditable(subItem.filter)
|
||||||
binding.chkEnable.isChecked = subItem.enabled
|
binding.chkEnable.isChecked = subItem.enabled
|
||||||
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
|
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
|
||||||
|
binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl
|
||||||
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
|
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
|
||||||
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
|
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
|
||||||
return true
|
return true
|
||||||
@@ -77,6 +78,7 @@ class SubEditActivity : BaseActivity() {
|
|||||||
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
|
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
|
||||||
subItem.prevProfile = binding.etPreProfile.text.toString()
|
subItem.prevProfile = binding.etPreProfile.text.toString()
|
||||||
subItem.nextProfile = binding.etNextProfile.text.toString()
|
subItem.nextProfile = binding.etNextProfile.text.toString()
|
||||||
|
subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked
|
||||||
|
|
||||||
if (TextUtils.isEmpty(subItem.remarks)) {
|
if (TextUtils.isEmpty(subItem.remarks)) {
|
||||||
toast(R.string.sub_setting_remarks)
|
toast(R.string.sub_setting_remarks)
|
||||||
@@ -90,7 +92,9 @@ class SubEditActivity : BaseActivity() {
|
|||||||
|
|
||||||
if (!Utils.isValidSubUrl(subItem.url)) {
|
if (!Utils.isValidSubUrl(subItem.url)) {
|
||||||
toast(R.string.toast_insecure_url_protocol)
|
toast(R.string.toast_insecure_url_protocol)
|
||||||
//return false
|
if (!subItem.allowInsecureUrl) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
|
import com.v2ray.ang.extension.concatUrl
|
||||||
import com.v2ray.ang.extension.toTrafficString
|
import com.v2ray.ang.extension.toTrafficString
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastError
|
import com.v2ray.ang.extension.toastError
|
||||||
@@ -42,8 +43,7 @@ import java.text.DateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class UserAssetActivity : BaseActivity() {
|
class UserAssetActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
|
|
||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
||||||
@@ -90,6 +90,11 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = UserAssetAdapter()
|
binding.recyclerView.adapter = UserAssetAdapter()
|
||||||
|
|
||||||
|
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
|
||||||
|
binding.layoutGeoFilesSources.setOnClickListener {
|
||||||
|
setGeoFilesSources()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -111,6 +116,22 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getGeoFilesSources(): String {
|
||||||
|
return MmkvManager.decodeSettingsString(AppConfig.PREF_GEO_FILES_SOURCES) ?: AppConfig.GEO_FILES_SOURCES.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setGeoFilesSources() {
|
||||||
|
AlertDialog.Builder(this).setItems(AppConfig.GEO_FILES_SOURCES.toTypedArray()) { _, i ->
|
||||||
|
try {
|
||||||
|
val value = AppConfig.GEO_FILES_SOURCES[i]
|
||||||
|
MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value)
|
||||||
|
binding.tvGeoFilesSourcesSummary.text = value
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to set geo files sources", e)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun showFileChooser() {
|
private fun showFileChooser() {
|
||||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
@@ -264,7 +285,8 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
list.add(
|
list.add(
|
||||||
Utils.getUuid() to AssetUrlItem(
|
Utils.getUuid() to AssetUrlItem(
|
||||||
it,
|
it,
|
||||||
AppConfig.GeoUrl + it
|
String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it),
|
||||||
|
locked = true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -315,7 +337,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
|
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) {
|
if (item.second.locked == true) {
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
||||||
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ object AppManagerUtil {
|
|||||||
|
|
||||||
val appName = applicationInfo.loadLabel(packageManager).toString()
|
val appName = applicationInfo.loadLabel(packageManager).toString()
|
||||||
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
|
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
|
||||||
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
|
val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0
|
||||||
|
|
||||||
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
||||||
apps.add(appInfo)
|
apps.add(appInfo)
|
||||||
@@ -33,4 +33,8 @@ object AppManagerUtil {
|
|||||||
|
|
||||||
return@withContext apps
|
return@withContext apps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLastUpdateTime(context: Context): Long =
|
||||||
|
context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -7,12 +7,16 @@ import com.v2ray.ang.BuildConfig
|
|||||||
import com.v2ray.ang.util.Utils.encode
|
import com.v2ray.ang.util.Utils.encode
|
||||||
import com.v2ray.ang.util.Utils.urlDecode
|
import com.v2ray.ang.util.Utils.urlDecode
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.*
|
import java.net.HttpURLConnection
|
||||||
import java.util.*
|
import java.net.IDN
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
object HttpUtil {
|
object HttpUtil {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a URL string to its ASCII representation.
|
* Converts a URL string to its ASCII representation.
|
||||||
*
|
*
|
||||||
@@ -20,7 +24,7 @@ object HttpUtil {
|
|||||||
* @return The ASCII representation of the URL.
|
* @return The ASCII representation of the URL.
|
||||||
*/
|
*/
|
||||||
fun idnToASCII(str: String): String {
|
fun idnToASCII(str: String): String {
|
||||||
val url = URI(str)
|
val url = URL(str)
|
||||||
val host = url.host
|
val host = url.host
|
||||||
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
|
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
|
||||||
if (host != asciiHost) {
|
if (host != asciiHost) {
|
||||||
@@ -30,6 +34,45 @@ object HttpUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a hostname to an IP address, returns original input if it's already an IP
|
||||||
|
*
|
||||||
|
* @param host The hostname or IP address to resolve
|
||||||
|
* @param ipv6Preferred Whether to prefer IPv6 addresses, defaults to false
|
||||||
|
* @return The resolved IP address or the original input (if it's already an IP or resolution fails)
|
||||||
|
*/
|
||||||
|
fun resolveHostToIP(host: String, ipv6Preferred: Boolean = false): List<String>? {
|
||||||
|
try {
|
||||||
|
// If it's already an IP address, return it as a list
|
||||||
|
if (Utils.isPureIpAddress(host)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all IP addresses
|
||||||
|
val addresses = InetAddress.getAllByName(host)
|
||||||
|
if (addresses.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort addresses based on preference
|
||||||
|
val sortedAddresses = if (ipv6Preferred) {
|
||||||
|
addresses.sortedWith(compareByDescending { it is Inet6Address })
|
||||||
|
} else {
|
||||||
|
addresses.sortedWith(compareBy { it is Inet6Address })
|
||||||
|
}
|
||||||
|
|
||||||
|
val ipList = sortedAddresses.mapNotNull { it.hostAddress }
|
||||||
|
|
||||||
|
Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}")
|
||||||
|
|
||||||
|
return ipList
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to resolve host to IP", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the content of a URL as a string.
|
* Retrieves the content of a URL as a string.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ object PluginUtil {
|
|||||||
*
|
*
|
||||||
* @param context The context to use.
|
* @param context The context to use.
|
||||||
* @param config The profile configuration.
|
* @param config The profile configuration.
|
||||||
* @param domainPort The domain and port information.
|
* @param socksPort The port information.
|
||||||
*/
|
*/
|
||||||
fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) {
|
fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
|
||||||
Log.i(AppConfig.TAG, "Starting plugin execution")
|
Log.i(AppConfig.TAG, "Starting plugin execution")
|
||||||
|
|
||||||
if (config == null) {
|
if (config == null || socksPort == null) {
|
||||||
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
|
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ object PluginUtil {
|
|||||||
try {
|
try {
|
||||||
if (config.configType == EConfigType.HYSTERIA2) {
|
if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
|
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
|
||||||
val configFile = genConfigHy2(context, config, domainPort) ?: return
|
val configFile = genConfigHy2(context, config, socksPort) ?: return
|
||||||
val cmd = genCmdHy2(context, configFile)
|
val cmd = genCmdHy2(context, configFile)
|
||||||
|
|
||||||
procService.runProcess(context, cmd)
|
procService.runProcess(context, cmd)
|
||||||
@@ -66,7 +66,7 @@ object PluginUtil {
|
|||||||
|
|
||||||
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
||||||
val socksPort = Utils.findFreePort(listOf(0))
|
val socksPort = Utils.findFreePort(listOf(0))
|
||||||
val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure
|
val configFile = genConfigHy2(context, config, socksPort) ?: return retFailure
|
||||||
val cmd = genCmdHy2(context, configFile)
|
val cmd = genCmdHy2(context, configFile)
|
||||||
|
|
||||||
val proc = ProcessService()
|
val proc = ProcessService()
|
||||||
@@ -85,14 +85,12 @@ object PluginUtil {
|
|||||||
*
|
*
|
||||||
* @param context The context to use.
|
* @param context The context to use.
|
||||||
* @param config The profile configuration.
|
* @param config The profile configuration.
|
||||||
* @param domainPort The domain and port information.
|
* @param socksPort The port information.
|
||||||
* @return The generated configuration file.
|
* @return The generated configuration file.
|
||||||
*/
|
*/
|
||||||
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
|
private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? {
|
||||||
Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2")
|
Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2")
|
||||||
|
|
||||||
val socksPort = domainPort?.split(":")?.last()
|
|
||||||
.let { if (it.isNullOrEmpty()) return null else it.toInt() }
|
|
||||||
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
|
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
|
||||||
|
|
||||||
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
|
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ import android.webkit.URLUtil
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
|
import com.v2ray.ang.BuildConfig
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.net.InetAddress
|
||||||
import java.net.ServerSocket
|
import java.net.ServerSocket
|
||||||
|
import java.net.URI
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -461,13 +463,23 @@ object Utils {
|
|||||||
fun isValidSubUrl(value: String?): Boolean {
|
fun isValidSubUrl(value: String?): Boolean {
|
||||||
if (value.isNullOrEmpty()) return false
|
if (value.isNullOrEmpty()) return false
|
||||||
|
|
||||||
return try {
|
try {
|
||||||
URLUtil.isHttpsUrl(value) ||
|
if (URLUtil.isHttpsUrl(value)) return true
|
||||||
(URLUtil.isHttpUrl(value) && value.contains(LOOPBACK))
|
if (URLUtil.isHttpUrl(value)) {
|
||||||
|
if (value.contains(LOOPBACK)) return true
|
||||||
|
|
||||||
|
//Check private ip address
|
||||||
|
val uri = URI(fixIllegalUrl(value))
|
||||||
|
if (isIpAddress(uri.host)) {
|
||||||
|
AppConfig.PRIVATE_IP_LIST.forEach {
|
||||||
|
if (isIpInCidr(uri.host, it)) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
|
Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -486,7 +498,58 @@ object Utils {
|
|||||||
*
|
*
|
||||||
* @return True if the package is Xray, false otherwise.
|
* @return True if the package is Xray, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isXray(): Boolean = ANG_PACKAGE.startsWith("com.v2ray.ang")
|
fun isXray(): Boolean = BuildConfig.APPLICATION_ID.startsWith("com.v2ray.ang")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if it is the Google Play version.
|
||||||
|
*
|
||||||
|
* @return True if the package is Google Play, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isGoogleFlavor(): Boolean = BuildConfig.FLAVOR == "playstore"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an InetAddress to its long representation
|
||||||
|
*
|
||||||
|
* @param ip The InetAddress to convert
|
||||||
|
* @return The long representation of the IP address
|
||||||
|
*/
|
||||||
|
private fun inetAddressToLong(ip: InetAddress): Long {
|
||||||
|
val bytes = ip.address
|
||||||
|
var result: Long = 0
|
||||||
|
for (i in bytes.indices) {
|
||||||
|
result = result shl 8 or (bytes[i].toInt() and 0xff).toLong()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP address is within a CIDR range
|
||||||
|
*
|
||||||
|
* @param ip The IP address to check
|
||||||
|
* @param cidr The CIDR notation range (e.g., "192.168.1.0/24")
|
||||||
|
* @return True if the IP is within the CIDR range, false otherwise
|
||||||
|
*/
|
||||||
|
fun isIpInCidr(ip: String, cidr: String): Boolean {
|
||||||
|
try {
|
||||||
|
if (!isIpAddress(ip)) return false
|
||||||
|
|
||||||
|
// Parse CIDR (e.g., "192.168.1.0/24")
|
||||||
|
val (cidrIp, prefixLen) = cidr.split("/")
|
||||||
|
val prefixLength = prefixLen.toInt()
|
||||||
|
|
||||||
|
// Convert IP and CIDR's IP portion to Long
|
||||||
|
val ipLong = inetAddressToLong(InetAddress.getByName(ip))
|
||||||
|
val cidrIpLong = inetAddressToLong(InetAddress.getByName(cidrIp))
|
||||||
|
|
||||||
|
// Calculate subnet mask (e.g., /24 → 0xFFFFFF00)
|
||||||
|
val mask = if (prefixLength == 0) 0L else (-1L shl (32 - prefixLength))
|
||||||
|
|
||||||
|
// Check if they're in the same subnet
|
||||||
|
return (ipLong and mask) == (cidrIpLong and mask)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<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="#FFFFFFFF"
|
||||||
|
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
11
V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml
Normal file
11
V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<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="#FF000000"
|
||||||
|
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
@@ -111,6 +111,49 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingTop="@dimen/padding_spacing_dp16">
|
android:paddingTop="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_check_update"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center|start"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/image_size_dp24"
|
||||||
|
android:layout_height="@dimen/image_size_dp24"
|
||||||
|
app:srcCompat="@drawable/ic_check_update_24dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/update_check_for_update"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/check_pre_release"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/padding_spacing_dp16"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/update_check_pre_release"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
android:textColor="@color/colorAccent"
|
||||||
|
app:theme="@style/BrandedSwitch" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_soure_ccode"
|
android:id="@+id/layout_soure_ccode"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
tools:context=".ui.PerAppProxyActivity">
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:id="@+id/pb_waiting"
|
android:id="@+id/pb_waiting"
|
||||||
@@ -71,6 +72,23 @@
|
|||||||
android:textColor="@color/colorAccent"
|
android:textColor="@color/colorAccent"
|
||||||
app:theme="@style/BrandedSwitch" />
|
app:theme="@style/BrandedSwitch" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_switch_bypass_apps_tips"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="@dimen/padding_spacing_dp8">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/image_size_dp24"
|
||||||
|
android:layout_height="@dimen/image_size_dp24"
|
||||||
|
app:srcCompat="@drawable/ic_about_24dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -83,7 +101,6 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" />
|
||||||
tools:context=".ui.PerAppProxyActivity" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
tools:context=".ui.SubSettingActivity">
|
tools:context=".ui.RoutingEditActivity">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -18,30 +18,35 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_domain_strategy"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/padding_spacing_dp8"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:orientation="vertical">
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center|start"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/routing_settings_domain_strategy" />
|
android:text="@string/routing_settings_domain_strategy"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||||
|
|
||||||
<Spinner
|
<TextView
|
||||||
android:id="@+id/sp_domain_strategy"
|
android:id="@+id/tv_domain_strategy_summary"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
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:maxLines="1"
|
||||||
android:entries="@array/routing_domain_strategy"
|
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||||
android:paddingTop="@dimen/padding_spacing_dp8" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/padding_spacing_dp8"
|
android:layout_margin="@dimen/padding_spacing_dp16"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
tools:context=".ui.SubSettingActivity">
|
tools:context=".ui.SubEditActivity">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/title_server"
|
android:text="@string/title_sub_setting"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -138,6 +138,28 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/padding_spacing_dp16"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:text="@string/sub_allow_insecure_url" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/allow_insecure_url"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||||
|
android:paddingEnd="@dimen/padding_spacing_dp16"
|
||||||
|
app:theme="@style/BrandedSwitch" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
78
V2rayNG/app/src/main/res/layout/activity_user_asset.xml
Normal file
78
V2rayNG/app/src/main/res/layout/activity_user_asset.xml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
tools:context=".ui.UserAssetActivity">
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/pb_waiting"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:indicatorColor="@color/color_fab_active" />
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:id="@+id/main_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_geo_files_sources"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center|start"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/asset_geo_files_sources"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_geo_files_sources_summary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/padding_spacing_dp16"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/title_user_asset_setting"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:nestedScrollingEnabled="false"
|
||||||
|
tools:listitem="@layout/item_recycler_user_asset" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
@@ -56,29 +56,6 @@
|
|||||||
android:id="@+id/import_manually_hysteria2"
|
android:id="@+id/import_manually_hysteria2"
|
||||||
android:title="@string/menu_item_import_config_manually_hysteria2"
|
android:title="@string/menu_item_import_config_manually_hysteria2"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<!-- <item-->
|
|
||||||
<!-- android:title="@string/menu_item_import_config_custom"-->
|
|
||||||
<!-- app:showAsAction="ifRoom">-->
|
|
||||||
<!-- <menu>-->
|
|
||||||
<!-- <item-->
|
|
||||||
<!-- android:id="@+id/import_config_custom_clipboard"-->
|
|
||||||
<!-- android:title="@string/menu_item_import_config_custom_clipboard"-->
|
|
||||||
<!-- app:showAsAction="never" />-->
|
|
||||||
<!-- <item-->
|
|
||||||
<!-- android:id="@+id/import_config_custom_local"-->
|
|
||||||
<!-- android:title="@string/menu_item_import_config_custom_local"-->
|
|
||||||
<!-- app:showAsAction="never" />-->
|
|
||||||
<!-- <item-->
|
|
||||||
<!-- android:id="@+id/import_config_custom_url"-->
|
|
||||||
<!-- android:title="@string/menu_item_import_config_custom_url"-->
|
|
||||||
<!-- app:showAsAction="never" />-->
|
|
||||||
<!-- <item-->
|
|
||||||
<!-- android:id="@+id/import_config_custom_url_scan"-->
|
|
||||||
<!-- android:title="@string/menu_item_import_config_custom_url_scan"-->
|
|
||||||
<!-- app:showAsAction="never" />-->
|
|
||||||
<!-- </menu>-->
|
|
||||||
<!-- </item>-->
|
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
<item
|
<item
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
<string name="menu_item_import_config_manually_trojan">الكتابة يدويًا [Trojan]</string>
|
<string name="menu_item_import_config_manually_trojan">الكتابة يدويًا [Trojan]</string>
|
||||||
<string name="menu_item_import_config_manually_wireguard">الكتابة يدويًا [Wireguard]</string>
|
<string name="menu_item_import_config_manually_wireguard">الكتابة يدويًا [Wireguard]</string>
|
||||||
<string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string>
|
<string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string>
|
||||||
<string name="menu_item_import_config_custom">تكوين مخصص</string>
|
|
||||||
<string name="menu_item_import_config_custom_clipboard">استيراد تكوين مخصص من الحافظة</string>
|
|
||||||
<string name="menu_item_import_config_custom_local">استيراد تكوين مخصص من الجهاز</string>
|
|
||||||
<string name="menu_item_import_config_custom_url">استيراد تكوين مخصص من عنوان URL</string>
|
|
||||||
<string name="menu_item_import_config_custom_url_scan">استيراد تكوين مخصص مسح عنوان URL</string>
|
|
||||||
<string name="del_config_comfirm">تأكيد الحذف؟</string>
|
<string name="del_config_comfirm">تأكيد الحذف؟</string>
|
||||||
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||||
<string name="server_lab_remarks">ملاحظات</string>
|
<string name="server_lab_remarks">ملاحظات</string>
|
||||||
@@ -127,6 +122,7 @@
|
|||||||
<string name="title_user_asset_add_url">إضافة عنوان URL للأصل</string>
|
<string name="title_user_asset_add_url">إضافة عنوان URL للأصل</string>
|
||||||
<string name="msg_file_not_found">الملف غير موجود</string>
|
<string name="msg_file_not_found">الملف غير موجود</string>
|
||||||
<string name="msg_remark_is_duplicate">الملاحظات موجودة بالفعل</string>
|
<string name="msg_remark_is_duplicate">الملاحظات موجودة بالفعل</string>
|
||||||
|
<string name="asset_geo_files_sources">Geo files source (optional)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">جار التحميل</string>
|
<string name="msg_dialog_progress">جار التحميل</string>
|
||||||
@@ -262,9 +258,10 @@
|
|||||||
<string name="sub_setting_filter">Remarks regular filter</string>
|
<string name="sub_setting_filter">Remarks regular filter</string>
|
||||||
<string name="sub_setting_enable">تفعيل التحديث</string>
|
<string name="sub_setting_enable">تفعيل التحديث</string>
|
||||||
<string name="sub_auto_update">تفعيل التحديث التلقائي</string>
|
<string name="sub_auto_update">تفعيل التحديث التلقائي</string>
|
||||||
<string name="sub_setting_pre_profile">Previous proxy remarks</string>
|
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
|
||||||
<string name="sub_setting_next_profile">Next proxy remarks</string>
|
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
|
||||||
<string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string>
|
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
|
||||||
|
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||||
<string name="title_sub_update">تحديث الاشتراك (أول خطوة)</string>
|
<string name="title_sub_update">تحديث الاشتراك (أول خطوة)</string>
|
||||||
<string name="title_ping_all_server">Tcping لجميع الإعدادات</string>
|
<string name="title_ping_all_server">Tcping لجميع الإعدادات</string>
|
||||||
<string name="title_real_ping_all_server"> اختبر جميع الإعدادات (3)</string>
|
<string name="title_real_ping_all_server"> اختبر جميع الإعدادات (3)</string>
|
||||||
@@ -314,6 +311,12 @@
|
|||||||
<string name="title_pref_fragment_interval">فاصل الجزء (الحد الأدنى - الحد الأقصى)</string>
|
<string name="title_pref_fragment_interval">فاصل الجزء (الحد الأدنى - الحد الأقصى)</string>
|
||||||
<string name="title_pref_fragment_enabled">تفعيل الجزء</string>
|
<string name="title_pref_fragment_enabled">تفعيل الجزء</string>
|
||||||
|
|
||||||
|
<string name="update_check_for_update">Check for update</string>
|
||||||
|
<string name="update_already_latest_version">Already on the latest version</string>
|
||||||
|
<string name="update_new_version_found">New version found: %s</string>
|
||||||
|
<string name="update_now">Update now</string>
|
||||||
|
<string name="update_check_pre_release">Check Pre-release</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>رمز استجابة سريعة (QRcode)</item>
|
<item>رمز استجابة سريعة (QRcode)</item>
|
||||||
<item>تصدير إلى الحافظة</item>
|
<item>تصدير إلى الحافظة</item>
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
<string name="menu_item_import_config_manually_trojan">ম্যানুয়ালি টাইপ করুন [Trojan]</string>
|
<string name="menu_item_import_config_manually_trojan">ম্যানুয়ালি টাইপ করুন [Trojan]</string>
|
||||||
<string name="menu_item_import_config_manually_wireguard">ম্যানুয়ালি টাইপ করুন [Wireguard]</string>
|
<string name="menu_item_import_config_manually_wireguard">ম্যানুয়ালি টাইপ করুন [Wireguard]</string>
|
||||||
<string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string>
|
<string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string>
|
||||||
<string name="menu_item_import_config_custom">কাস্টম কনফিগারেশন</string>
|
|
||||||
<string name="menu_item_import_config_custom_clipboard">ক্লিপবোর্ড থেকে কাস্টম কনফিগারেশন আমদানি করুন</string>
|
|
||||||
<string name="menu_item_import_config_custom_local">স্থানীয়ভাবে কাস্টম কনফিগারেশন আমদানি করুন</string>
|
|
||||||
<string name="menu_item_import_config_custom_url">URL থেকে কাস্টম কনফিগারেশন আমদানি করুন</string>
|
|
||||||
<string name="menu_item_import_config_custom_url_scan">কাস্টম কনফিগারেশন স্ক্যান URL আমদানি করুন</string>
|
|
||||||
<string name="del_config_comfirm">মুছে ফেলুন নিশ্চিত করুন?</string>
|
<string name="del_config_comfirm">মুছে ফেলুন নিশ্চিত করুন?</string>
|
||||||
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||||
<string name="server_lab_remarks">মন্তব্য</string>
|
<string name="server_lab_remarks">মন্তব্য</string>
|
||||||
@@ -126,6 +121,7 @@
|
|||||||
<string name="title_user_asset_add_url">অ্যাসেট URL যোগ করুন</string>
|
<string name="title_user_asset_add_url">অ্যাসেট URL যোগ করুন</string>
|
||||||
<string name="msg_file_not_found">ফাইল খুঁজে পাওয়া যায়নি</string>
|
<string name="msg_file_not_found">ফাইল খুঁজে পাওয়া যায়নি</string>
|
||||||
<string name="msg_remark_is_duplicate">মন্তব্য ইতিমধ্যে বিদ্যমান</string>
|
<string name="msg_remark_is_duplicate">মন্তব্য ইতিমধ্যে বিদ্যমান</string>
|
||||||
|
<string name="asset_geo_files_sources">Geo files source (optional)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">লোড হচ্ছে</string>
|
<string name="msg_dialog_progress">লোড হচ্ছে</string>
|
||||||
@@ -262,9 +258,10 @@
|
|||||||
<string name="sub_setting_filter">Remarks regular filter</string>
|
<string name="sub_setting_filter">Remarks regular filter</string>
|
||||||
<string name="sub_setting_enable">আপডেট সক্রিয় করুন</string>
|
<string name="sub_setting_enable">আপডেট সক্রিয় করুন</string>
|
||||||
<string name="sub_auto_update">স্বয়ংক্রিয় আপডেট সক্রিয় করুন</string>
|
<string name="sub_auto_update">স্বয়ংক্রিয় আপডেট সক্রিয় করুন</string>
|
||||||
<string name="sub_setting_pre_profile">Previous proxy remarks</string>
|
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
|
||||||
<string name="sub_setting_next_profile">Next proxy remarks</string>
|
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
|
||||||
<string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string>
|
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
|
||||||
|
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||||
<string name="title_sub_update">সাবস্ক্রিপশন আপডেট</string>
|
<string name="title_sub_update">সাবস্ক্রিপশন আপডেট</string>
|
||||||
<string name="title_ping_all_server">সব কনফিগারেশন TCPing</string>
|
<string name="title_ping_all_server">সব কনফিগারেশন TCPing</string>
|
||||||
<string name="title_real_ping_all_server">সব কনফিগারেশন প্রকৃত বিলম্ব</string>
|
<string name="title_real_ping_all_server">সব কনফিগারেশন প্রকৃত বিলম্ব</string>
|
||||||
@@ -313,6 +310,12 @@
|
|||||||
<string name="title_pref_fragment_interval">ফ্র্যাগমেন্ট ইন্টারভ্যাল (ন্যূনতম-সর্বাধিক)</string>
|
<string name="title_pref_fragment_interval">ফ্র্যাগমেন্ট ইন্টারভ্যাল (ন্যূনতম-সর্বাধিক)</string>
|
||||||
<string name="title_pref_fragment_enabled">ফ্র্যাগমেন্ট সক্রিয় করুন</string>
|
<string name="title_pref_fragment_enabled">ফ্র্যাগমেন্ট সক্রিয় করুন</string>
|
||||||
|
|
||||||
|
<string name="update_check_for_update">Check for update</string>
|
||||||
|
<string name="update_already_latest_version">Already on the latest version</string>
|
||||||
|
<string name="update_new_version_found">New version found: %s</string>
|
||||||
|
<string name="update_now">Update now</string>
|
||||||
|
<string name="update_check_pre_release">Check Pre-release</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QR কোড</item>
|
<item>QR কোড</item>
|
||||||
<item>ক্লিপবোর্ডে রপ্তানি করুন</item>
|
<item>ক্লিপবোর্ডে রপ্তানি করুন</item>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
<string name="navigation_drawer_close">بستن نومگه کشاری</string>
|
<string name="navigation_drawer_close">بستن نومگه کشاری</string>
|
||||||
<string name="migration_success">مووفقیت من جاگورویی داده</string>
|
<string name="migration_success">مووفقیت من جاگورویی داده</string>
|
||||||
<string name="migration_fail">جاگورویی داده ٱنجوم نگرؽڌ</string>
|
<string name="migration_fail">جاگورویی داده ٱنجوم نگرؽڌ</string>
|
||||||
<string name="pull_down_to_refresh">Please pull down to refresh!</string>
|
<string name="pull_down_to_refresh">سی وانۊ کردن، بکشینس بلم!</string>
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->↓
|
||||||
<string name="notification_action_stop_v2ray">واڌاشتن</string>
|
<string name="notification_action_stop_v2ray">واڌاشتن</string>
|
||||||
<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>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<string name="menu_item_del_config">پاک کردن کانفیگ</string>
|
<string name="menu_item_del_config">پاک کردن کانفیگ</string>
|
||||||
<string name="menu_item_import_config_qrcode">و من ٱووردن کانفیگ ز QRcode</string>
|
<string name="menu_item_import_config_qrcode">و من ٱووردن کانفیگ ز QRcode</string>
|
||||||
<string name="menu_item_import_config_clipboard">و من ٱووردن کانفیگ ز کلیپ بورد</string>
|
<string name="menu_item_import_config_clipboard">و من ٱووردن کانفیگ ز کلیپ بورد</string>
|
||||||
<string name="menu_item_import_config_local">Import config from locally</string>
|
<string name="menu_item_import_config_local">و من ٱووردن کانفیگ ز مهلی</string>
|
||||||
<string name="menu_item_import_config_manually_vmess">هؽل دستی[VMess]</string>
|
<string name="menu_item_import_config_manually_vmess">هؽل دستی[VMess]</string>
|
||||||
<string name="menu_item_import_config_manually_vless">هؽل دستی[VLESS]</string>
|
<string name="menu_item_import_config_manually_vless">هؽل دستی[VLESS]</string>
|
||||||
<string name="menu_item_import_config_manually_ss">هؽل دستی[Shadowsocks]</string>
|
<string name="menu_item_import_config_manually_ss">هؽل دستی[Shadowsocks]</string>
|
||||||
@@ -35,17 +35,12 @@
|
|||||||
<string name="menu_item_import_config_manually_trojan">هؽل دستی[Trojan]</string>
|
<string name="menu_item_import_config_manually_trojan">هؽل دستی[Trojan]</string>
|
||||||
<string name="menu_item_import_config_manually_wireguard">هؽل دستی[Wireguard]</string>
|
<string name="menu_item_import_config_manually_wireguard">هؽل دستی[Wireguard]</string>
|
||||||
<string name="menu_item_import_config_manually_hysteria2">هؽل دستی[Hysteria2]</string>
|
<string name="menu_item_import_config_manually_hysteria2">هؽل دستی[Hysteria2]</string>
|
||||||
<string name="menu_item_import_config_custom">کانفیگ سفارشی</string>
|
|
||||||
<string name="menu_item_import_config_custom_clipboard">کانفیگ سفارشین ز کلیپ بورد و من بیار</string>
|
|
||||||
<string name="menu_item_import_config_custom_local">کانفیگ سفارشین ز مهلی و من بیار</string>
|
|
||||||
<string name="menu_item_import_config_custom_url">کانفیگ سفارشین ز آدرس اینترنتی و من بیار</string>
|
|
||||||
<string name="menu_item_import_config_custom_url_scan">نشۊوی اینترنتی اسکن کانفیگ سفارشین بزݩ</string>
|
|
||||||
<string name="del_config_comfirm">پاک بۊ؟</string>
|
<string name="del_config_comfirm">پاک بۊ؟</string>
|
||||||
<string name="del_invalid_config_comfirm">پؽش ز پاک کردن کانفیگ نا موئتبر واجۊری کوݩ! پاک کردن کانفیگن قوۊل اکۊنی؟</string>
|
<string name="del_invalid_config_comfirm">پؽش ز پاک کردن کانفیگ نا موئتبر واجۊری کوݩ! پاک کردن کانفیگن قوۊل اکۊنی؟</string>
|
||||||
<string name="server_lab_remarks">نیشتنا</string>
|
<string name="server_lab_remarks">نیشتنا</string>
|
||||||
<string name="server_lab_address">آدرس</string>
|
<string name="server_lab_address">نشۊوی</string>
|
||||||
<string name="server_lab_port">پورت</string>
|
<string name="server_lab_port">پورت</string>
|
||||||
<string name="server_lab_id">نوم من توری</string>
|
<string name="server_lab_id">نوم منتوری</string>
|
||||||
<string name="server_lab_alterid">شناسه جایگۊزین</string>
|
<string name="server_lab_alterid">شناسه جایگۊزین</string>
|
||||||
<string name="server_lab_security">ٱمنیت</string>
|
<string name="server_lab_security">ٱمنیت</string>
|
||||||
<string name="server_lab_network">شبکه</string>
|
<string name="server_lab_network">شبکه</string>
|
||||||
@@ -73,21 +68,21 @@
|
|||||||
<string name="server_lab_stream_alpn">Alpn</string>
|
<string name="server_lab_stream_alpn">Alpn</string>
|
||||||
<string name="server_lab_allow_insecure">اجازه نا ٱمن</string>
|
<string name="server_lab_allow_insecure">اجازه نا ٱمن</string>
|
||||||
<string name="server_lab_sni">SNI</string>
|
<string name="server_lab_sni">SNI</string>
|
||||||
<string name="server_lab_address3">آدرس</string>
|
<string name="server_lab_address3">نشۊوی</string>
|
||||||
<string name="server_lab_port3">پورت</string>
|
<string name="server_lab_port3">پورت</string>
|
||||||
<string name="server_lab_id3">رزم</string>
|
<string name="server_lab_id3">رزم</string>
|
||||||
<string name="server_lab_security3">ٱمنیت</string>
|
<string name="server_lab_security3">ٱمنیت</string>
|
||||||
<string name="server_lab_id4">رزم (اختیاری)</string>
|
<string name="server_lab_id4">رزم (اختیاری)</string>
|
||||||
<string name="server_lab_security4">نوم من توری (اختیاری)</string>
|
<string name="server_lab_security4">نوم منتوری (اختیاری)</string>
|
||||||
<string name="server_lab_encryption">رزم نگاری</string>
|
<string name="server_lab_encryption">رزم نگاری</string>
|
||||||
<string name="server_lab_flow">جریان</string>
|
<string name="server_lab_flow">جریان</string>
|
||||||
<string name="server_lab_public_key">کیلیت پوی وولاتی</string>
|
<string name="server_lab_public_key">کیلیت پوی وولاتی</string>
|
||||||
<string name="server_lab_preshared_key">کیلیت رمز ناهاڌن ازاف (اختیاری)</string>
|
<string name="server_lab_preshared_key">کیلیت رزم ناهاڌن ازاف (اختیاری)</string>
|
||||||
<string name="server_lab_short_id">ShortID</string>
|
<string name="server_lab_short_id">ShortID</string>
|
||||||
<string name="server_lab_spider_x">SpiderX</string>
|
<string name="server_lab_spider_x">SpiderX</string>
|
||||||
<string name="server_lab_secret_key">کیلیت سیخومی</string>
|
<string name="server_lab_secret_key">کیلیت سیخومی</string>
|
||||||
<string name="server_lab_reserved">Reserved(اختیاری، وا کاما ز یک جوڌا ابۊن)</string>
|
<string name="server_lab_reserved">Reserved(اختیاری، وا کاما ز یک جوڌا ابۊن)</string>
|
||||||
<string name="server_lab_local_address">آدرس مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن)</string>
|
<string name="server_lab_local_address">نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن)</string>
|
||||||
<string name="server_lab_local_mtu">Mtu(اختیاری، پؽش فرز 1420)</string>
|
<string name="server_lab_local_mtu">Mtu(اختیاری، پؽش فرز 1420)</string>
|
||||||
<string name="toast_success">وا مووفقیت ٱنجوم وابی</string>
|
<string name="toast_success">وا مووفقیت ٱنجوم وابی</string>
|
||||||
<string name="toast_failure">شکست خرد</string>
|
<string name="toast_failure">شکست خرد</string>
|
||||||
@@ -101,11 +96,11 @@
|
|||||||
<string name="server_lab_content">موئتوا</string>
|
<string name="server_lab_content">موئتوا</string>
|
||||||
<string name="toast_none_data_clipboard">هیچ داده ای من کلیپ بورد وۊجۊڌ نڌاره</string>
|
<string name="toast_none_data_clipboard">هیچ داده ای من کلیپ بورد وۊجۊڌ نڌاره</string>
|
||||||
<string name="toast_invalid_url">نشۊوی اینترنتی نا موئتبر هڌ</string>
|
<string name="toast_invalid_url">نشۊوی اینترنتی نا موئتبر هڌ</string>
|
||||||
<string name="toast_insecure_url_protocol">آدرس اشتراک پوروتوکول نا ٱمن HTTP ن و کار مبرین</string>
|
<string name="toast_insecure_url_protocol">نشۊوی اشتراک پوروتوکول نا ٱمن HTTP ن و کار مبرین</string>
|
||||||
<string name="server_lab_need_inbound">موتمعن بۊین ک پورت وۊرۊڌی وا سامووا ی جۊر هڌ</string>
|
<string name="server_lab_need_inbound">موتمعن بۊین ک پورت وۊرۊڌی وا سامووا ی جۊر هڌ</string>
|
||||||
<string name="toast_malformed_josn">کانفیگ زبال نؽڌ</string>
|
<string name="toast_malformed_josn">کانفیگ زبال نؽڌ</string>
|
||||||
<string name="server_lab_request_host6">هاست(SNI)(اختیاری)</string>
|
<string name="server_lab_request_host6">هاست(SNI)(اختیاری)</string>
|
||||||
<string name="toast_action_not_allowed">ای کار ممنۊ هڌ</string>
|
<string name="toast_action_not_allowed">ای کار ممنۊع هڌ</string>
|
||||||
<string name="server_obfs_password">رزم obfs</string>
|
<string name="server_obfs_password">رزم obfs</string>
|
||||||
<string name="server_lab_port_hop">پورت گوم (درگا سرورن ز نۊ هؽل اکونه)</string>
|
<string name="server_lab_port_hop">پورت گوم (درگا سرورن ز نۊ هؽل اکونه)</string>
|
||||||
<string name="server_lab_port_hop_interval">فاسله پورت گوم (سانیه)</string>
|
<string name="server_lab_port_hop_interval">فاسله پورت گوم (سانیه)</string>
|
||||||
@@ -121,24 +116,25 @@
|
|||||||
<string name="menu_item_add_file">ازاف کردن فایل</string>
|
<string name="menu_item_add_file">ازاف کردن فایل</string>
|
||||||
<string name="menu_item_add_url">ازاف کردن لینگ</string>
|
<string name="menu_item_add_url">ازاف کردن لینگ</string>
|
||||||
<string name="menu_item_scan_qrcode">اسکن QRcode</string>
|
<string name="menu_item_scan_qrcode">اسکن QRcode</string>
|
||||||
<string name="title_url">آدرس اینترنتی</string>
|
<string name="title_url">نشۊوی اینترنتی</string>
|
||||||
<string name="menu_item_download_file">دانلود فایلا</string>
|
<string name="menu_item_download_file">دانلود فایلا</string>
|
||||||
<string name="title_user_asset_add_url">آدرس اینترنتی دارایین ازاف کۊنین</string>
|
<string name="title_user_asset_add_url">نشۊوی اینترنتی دارایین ازاف کۊنین</string>
|
||||||
<string name="msg_file_not_found">فایلن نجوست</string>
|
<string name="msg_file_not_found">فایلن نجوست</string>
|
||||||
<string name="msg_remark_is_duplicate">ائزارات ز زیتر بیڌسۉݩ</string>
|
<string name="msg_remark_is_duplicate">نوم ز زیتر بیڌس</string>
|
||||||
|
<string name="asset_geo_files_sources">بونچک فایلا جوقرافیایی (اختیاری)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">هون بار ونی بۊ</string>
|
<string name="msg_dialog_progress">هونی بار ونی بۊ</string>
|
||||||
<string name="menu_item_search">پیتینیڌن</string>
|
<string name="menu_item_search">پیتینیڌن</string>
|
||||||
<string name="menu_item_select_all">پسند پوی</string>
|
<string name="menu_item_select_all">پسند پوی</string>
|
||||||
<string name="msg_enter_keywords">رزمان بزنین</string>
|
<string name="msg_enter_keywords">رزما ن بزنین</string>
|
||||||
<string name="switch_bypass_apps_mode">هالت Bypass</string>
|
<string name="switch_bypass_apps_mode">هالت Bypass</string>
|
||||||
<string name="menu_item_select_proxy_app">پسند خوتکار پروکسی برنومه</string>
|
<string name="menu_item_select_proxy_app">پسند خوتکار پروکسی برنومه</string>
|
||||||
<string name="msg_downloading_content">موئتوا هونی دانلود ابۊن</string>
|
<string name="msg_downloading_content">موئتوا هونی دانلود ابۊن</string>
|
||||||
<string name="menu_item_export_proxy_app">و در کشیڌن من کلیپ بورد</string>
|
<string name="menu_item_export_proxy_app">و در کشیڌن من کلیپ بورد</string>
|
||||||
<string name="menu_item_import_proxy_app">و من ٱووردن ز کلیپ بورد</string>
|
<string name="menu_item_import_proxy_app">و من ٱووردن ز کلیپ بورد</string>
|
||||||
<string name="per_app_proxy_settings">Per-app settings</string>
|
<string name="per_app_proxy_settings">سامووا ب تفکیک برنومه</string>
|
||||||
<string name="per_app_proxy_settings_enable">Enable per-app</string>
|
<string name="per_app_proxy_settings_enable">ر وندن ب تفکیک برنومه</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Preferences -->
|
<!-- Preferences -->
|
||||||
@@ -155,7 +151,7 @@
|
|||||||
<string name="summary_pref_mux_enabled">زل تر، ٱما گاشڌ منپیز زی قت بۊ بارت دؽوۉداری، TCP، UDP و QUIC ن ای لم سفارشی کۊنین.</string>
|
<string name="summary_pref_mux_enabled">زل تر، ٱما گاشڌ منپیز زی قت بۊ بارت دؽوۉداری، TCP، UDP و QUIC ن ای لم سفارشی کۊنین.</string>
|
||||||
<string name="title_pref_mux_concurency">منپیزا TCP (تلایه منجا 1-1024)</string>
|
<string name="title_pref_mux_concurency">منپیزا TCP (تلایه منجا 1-1024)</string>
|
||||||
<string name="title_pref_mux_xudp_concurency">منپیزا XUDP (تلایه منجا 1-1024)</string>
|
<string name="title_pref_mux_xudp_concurency">منپیزا XUDP (تلایه منجا 1-1024)</string>
|
||||||
<string name="title_pref_mux_xudp_quic">دؽوۉداری QUIC من تونل mux</string>
|
<string name="title_pref_mux_xudp_quic">دؽوۉداری QUIC من تۊنل mux</string>
|
||||||
<string-array name="mux_xudp_quic_entries">
|
<string-array name="mux_xudp_quic_entries">
|
||||||
<item>رڌ کردن</item>
|
<item>رڌ کردن</item>
|
||||||
<item>موجاز</item>
|
<item>موجاز</item>
|
||||||
@@ -168,16 +164,16 @@
|
|||||||
<string name="title_pref_sniffing_enabled">ر وندن Sniffing</string>
|
<string name="title_pref_sniffing_enabled">ر وندن Sniffing</string>
|
||||||
<string name="summary_pref_sniffing_enabled">دامنه sniff ن ز کتن امتهۉݩ کۊنین (پؽش فرز رۊشن)</string>
|
<string name="summary_pref_sniffing_enabled">دامنه sniff ن ز کتن امتهۉݩ کۊنین (پؽش فرز رۊشن)</string>
|
||||||
<string name="title_pref_route_only_enabled">ر وندن routeOnly</string>
|
<string name="title_pref_route_only_enabled">ر وندن routeOnly</string>
|
||||||
<string name="summary_pref_route_only_enabled">ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو آدرس مورد نزرن و عونوان آدرس IP ووردارین.</string>
|
<string name="summary_pref_route_only_enabled">ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو نشۊوی مۉرد نزرن و عونوان نشۊوی IP ووردارین.</string>
|
||||||
|
|
||||||
<string name="title_pref_local_dns_enabled">ر وندن DNS مهلی</string>
|
<string name="title_pref_local_dns_enabled">ر وندن DNS مهلی</string>
|
||||||
<string name="summary_pref_local_dns_enabled">DNS پردازشت وابیڌه و دس هسته ماژول DNS (پؽشنهاڌ ابۊ، ٱر نیاز هڌ ک جوستن تور وو ولات ٱسلین دور زنی)</string>
|
<string name="summary_pref_local_dns_enabled">DNS پردازشت وابیڌه و دس هسته ماژول DNS (پؽشنهاڌ ابۊ، ٱر نیاز هڌ ک جوستن تور وو ولات ٱسلین دور زنی)</string>
|
||||||
|
|
||||||
<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_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 ن فعال کۊنین وو نشۊویا 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>
|
||||||
@@ -188,14 +184,14 @@
|
|||||||
<string name="title_pref_domestic_dns">DNS منی (اختیاری)</string>
|
<string name="title_pref_domestic_dns">DNS منی (اختیاری)</string>
|
||||||
<string name="summary_pref_domestic_dns">DNS</string>
|
<string name="summary_pref_domestic_dns">DNS</string>
|
||||||
|
|
||||||
<string name="title_pref_dns_hosts">DNS هاست موستقیم (قالوو: دامنه: آدرس،...)</string>
|
<string name="title_pref_dns_hosts">DNS هاست موستقیم (قالوو: دامنه: نشۊوی،...)</string>
|
||||||
<string name="summary_pref_dns_hosts">دامنه:آدرس،...</string>
|
<string name="summary_pref_dns_hosts">دامنه:نشۊوی،...</string>
|
||||||
|
|
||||||
<string name="title_pref_delay_test_url">آدرس اینترنتی آزمایش تئخیر واقعی (http/https)</string>
|
<string name="title_pref_delay_test_url">نشۊوی اینترنتی آزمایش تئخیر واقعی (http/https)</string>
|
||||||
<string name="summary_pref_delay_test_url">نشۊوی اینترنتی</string>
|
<string name="summary_pref_delay_test_url">نشۊوی اینترنتی</string>
|
||||||
|
|
||||||
<string name="title_pref_proxy_sharing_enabled">هشتن منپیزا ز شبکه مهلی</string>
|
<string name="title_pref_proxy_sharing_enabled">هشتن منپیزا ز شبکه مهلی</string>
|
||||||
<string name="summary_pref_proxy_sharing_enabled">پوی دسگایل ترن وا آدرس IP ایسا، ز ر socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل اعتماد فعال بۊ تا ز منپیز غیر موجاز جلو گری بۊ.</string>
|
<string name="summary_pref_proxy_sharing_enabled">پوی دسگایل ترن وا نشۊوی IP ایسا، ز ر socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل اعتماد فعال بۊ تا ز منپیز غیر موجاز جلو گری بۊ.</string>
|
||||||
<string name="toast_warning_pref_proxysharing_short">منپیزا ز شبکه مهلی ن موجار کۊنین، موتمعن بۊین ک من ی شبکه قابل ائتماڌ هڌین.</string>
|
<string name="toast_warning_pref_proxysharing_short">منپیزا ز شبکه مهلی ن موجار کۊنین، موتمعن بۊین ک من ی شبکه قابل ائتماڌ هڌین.</string>
|
||||||
|
|
||||||
<string name="title_pref_allow_insecure">اجازه نا ٱمن</string>
|
<string name="title_pref_allow_insecure">اجازه نا ٱمن</string>
|
||||||
@@ -214,10 +210,10 @@
|
|||||||
<string name="summary_pref_start_scan_immediate">شؽواتگرن سی اسکن، زی مجال ر وندن بۊگۊشین، اندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین.</string>
|
<string name="summary_pref_start_scan_immediate">شؽواتگرن سی اسکن، زی مجال ر وندن بۊگۊشین، اندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین.</string>
|
||||||
|
|
||||||
<string name="title_pref_append_http_proxy">پروکسی HTTP ن و VPN ازاف کۊنین</string>
|
<string name="title_pref_append_http_proxy">پروکسی HTTP ن و VPN ازاف کۊنین</string>
|
||||||
<string name="summary_pref_append_http_proxy">پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومیل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ.</string>
|
<string name="summary_pref_append_http_proxy">پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومه یل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ.</string>
|
||||||
|
|
||||||
<string name="title_pref_double_column_display">Enable double column display</string>
|
<string name="title_pref_double_column_display">ر وندن نشۉݩ داڌن دو سۊتۊنی</string>
|
||||||
<string name="summary_pref_double_column_display">The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect.</string>
|
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشۉݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ر وستن وا برنومه ن ز نۊ ر ونین.</string>
|
||||||
|
|
||||||
<!-- AboutActivity -->
|
<!-- AboutActivity -->
|
||||||
<string name="title_pref_feedback">فشناڌن منشڌ</string>
|
<string name="title_pref_feedback">فشناڌن منشڌ</string>
|
||||||
@@ -262,16 +258,17 @@
|
|||||||
<string name="sub_setting_filter">نوم موستعار فیلتر</string>
|
<string name="sub_setting_filter">نوم موستعار فیلتر</string>
|
||||||
<string name="sub_setting_enable">فعال بیڌن ورۊ کردن</string>
|
<string name="sub_setting_enable">فعال بیڌن ورۊ کردن</string>
|
||||||
<string name="sub_auto_update">فعال بیڌن ورۊ کردن خوتکار</string>
|
<string name="sub_auto_update">فعال بیڌن ورۊ کردن خوتکار</string>
|
||||||
|
<string name="sub_allow_insecure_url">موجاز کردن نشۊوی HTTP نا ٱمن</string>
|
||||||
<string name="sub_setting_pre_profile">نوم موستعار پروکسی دیندایی</string>
|
<string name="sub_setting_pre_profile">نوم موستعار پروکسی دیندایی</string>
|
||||||
<string name="sub_setting_next_profile">نوم موستعار پروکسی نیایی</string>
|
<string name="sub_setting_next_profile">نوم موستعار پروکسی نیایی</string>
|
||||||
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</string>
|
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</string>
|
||||||
<string name="title_sub_update">ورۊ کردن اشتراک جرگه سکویی</string>
|
<string name="title_sub_update">ورۊ کردن اشتراک جرگه سکویی</string>
|
||||||
<string name="title_ping_all_server">Tcping کانفیگا جرگه سکویی</string>
|
<string name="title_ping_all_server">Tcping کانفیگا جرگه سکویی</string>
|
||||||
<string name="title_real_ping_all_server">تئخیر واقعی کانفیگا جرگه سکویی</string>
|
<string name="title_real_ping_all_server">تئخیر واقعی کانفیگا جرگه سکویی</string>
|
||||||
<string name="title_user_asset_setting">Asset files</string>
|
<string name="title_user_asset_setting">فایلا بونچک جوقرافیایی</string>
|
||||||
<string name="title_sort_by_test_results">ترتیب و ری نتیجیل آزمایش</string>
|
<string name="title_sort_by_test_results">ترتیب و ری نتیجه یل آزمایش</string>
|
||||||
<string name="title_filter_config">فیلتر کردن کانفیگا</string>
|
<string name="title_filter_config">فیلتر کردن کانفیگا</string>
|
||||||
<string name="filter_config_all">پوی جرگیل</string>
|
<string name="filter_config_all">پوی جرگه یل کانفیگ</string>
|
||||||
<string name="title_del_duplicate_config_count">پاک کردن %d کانفیگ تکراری</string>
|
<string name="title_del_duplicate_config_count">پاک کردن %d کانفیگ تکراری</string>
|
||||||
|
|
||||||
<string name="title_del_config_count">پاک کردن %d کانفیگ</string>
|
<string name="title_del_config_count">پاک کردن %d کانفیگ</string>
|
||||||
@@ -307,7 +304,7 @@
|
|||||||
<string name="connection_test_pending">منپیزن واجۊری کوݩ</string>
|
<string name="connection_test_pending">منپیزن واجۊری کوݩ</string>
|
||||||
<string name="connection_test_testing">هونی آزمایش ابۊ…</string>
|
<string name="connection_test_testing">هونی آزمایش ابۊ…</string>
|
||||||
<string name="connection_test_testing_count">%d کانفیگ هونی آزمایش ابۊ...</string>
|
<string name="connection_test_testing_count">%d کانفیگ هونی آزمایش ابۊ...</string>
|
||||||
<string name="connection_test_available">مووفق بی: منپیز HTTP %dms تۊل کشی</string>
|
<string name="connection_test_available">مووفق بی: منپیز %dms تۊل کشی</string>
|
||||||
<string name="connection_test_error">منپیز و اینترنتن نجوست: %s</string>
|
<string name="connection_test_error">منپیز و اینترنتن نجوست: %s</string>
|
||||||
<string name="connection_test_fail">اینترنت من دسرس نؽ</string>
|
<string name="connection_test_fail">اینترنت من دسرس نؽ</string>
|
||||||
<string name="connection_test_error_status_code">کود ختا: #%d</string>
|
<string name="connection_test_error_status_code">کود ختا: #%d</string>
|
||||||
@@ -316,11 +313,17 @@
|
|||||||
|
|
||||||
<string name="import_subscription_success">اشتراک وا مووفقیت زفت زابی</string>
|
<string name="import_subscription_success">اشتراک وا مووفقیت زفت زابی</string>
|
||||||
<string name="import_subscription_failure">اشتراک زفت نوابی</string>
|
<string name="import_subscription_failure">اشتراک زفت نوابی</string>
|
||||||
<string name="title_fragment_settings">سامووا Fragment</string>
|
<string name="title_fragment_settings">سامووا فرگمنت</string>
|
||||||
<string name="title_pref_fragment_packets">Fragment Packets</string>
|
<string name="title_pref_fragment_packets">کتنا فرگمنت</string>
|
||||||
<string name="title_pref_fragment_length">Fragment Length (min-max)</string>
|
<string name="title_pref_fragment_length">تۊل کتنا فرگمنت (هدقل-هدکسر)</string>
|
||||||
<string name="title_pref_fragment_interval">Fragment Interval (min-max)</string>
|
<string name="title_pref_fragment_interval">فاسله منجا کتنا فرگمنت (هدقل-هدکسر)</string>
|
||||||
<string name="title_pref_fragment_enabled">ر وندن Fragment</string>
|
<string name="title_pref_fragment_enabled">ر وندن فرگمنت</string>
|
||||||
|
|
||||||
|
<string name="update_check_for_update">واجۊری سی ورۊ رسۊوی</string>
|
||||||
|
<string name="update_already_latest_version">سکو نوسخه دیندایی پۊرنیڌه هڌ</string>
|
||||||
|
<string name="update_new_version_found">نوسخه نۊ ن جوست: %s</string>
|
||||||
|
<string name="update_now">سکو ورۊ رسۊوی کۊنین</string>
|
||||||
|
<string name="update_check_pre_release">واجۊری نوسخه یل پؽش ز تیجنیڌن</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QRcode</item>
|
<item>QRcode</item>
|
||||||
@@ -330,10 +333,10 @@
|
|||||||
|
|
||||||
<string-array name="share_method_more">
|
<string-array name="share_method_more">
|
||||||
<item>QRcode</item>
|
<item>QRcode</item>
|
||||||
<item>Export to clipboard</item>
|
<item>و در کشیڌن من کلیپ بورد</item>
|
||||||
<item>Export full configuration to clipboard</item>
|
<item>و در کشیڌن پوی کانفیگ من کلیپ بورد</item>
|
||||||
<item>Edit</item>
|
<item>آلشت</item>
|
||||||
<item>Delete</item>
|
<item>پاک کردن</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="share_sub_method">
|
<string-array name="share_sub_method">
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
<string name="menu_item_import_config_manually_trojan">تایپ دستی[TROJAN]</string>
|
<string name="menu_item_import_config_manually_trojan">تایپ دستی[TROJAN]</string>
|
||||||
<string name="menu_item_import_config_manually_wireguard">تایپ دستی[WIREGUARD]</string>
|
<string name="menu_item_import_config_manually_wireguard">تایپ دستی[WIREGUARD]</string>
|
||||||
<string name="menu_item_import_config_manually_hysteria2">تایپ دستی[HYSTERIA2]</string>
|
<string name="menu_item_import_config_manually_hysteria2">تایپ دستی[HYSTERIA2]</string>
|
||||||
<string name="menu_item_import_config_custom">کانفیگ سفارشی</string>
|
|
||||||
<string name="menu_item_import_config_custom_clipboard">کانفیگ سفارشی را از کلیپ بورد وارد کنید</string>
|
|
||||||
<string name="menu_item_import_config_custom_local">کانفیگ سفارشی را به صورت محلی وارد کنید</string>
|
|
||||||
<string name="menu_item_import_config_custom_url">کانفیگ سفارشی را از طریق نشانی اینترنتی وارد کنید</string>
|
|
||||||
<string name="menu_item_import_config_custom_url_scan">نشانی اینترنتی اسکن کانفیگ سفارشی را وارد کنید</string>
|
|
||||||
<string name="del_config_comfirm">حذف شود؟</string>
|
<string name="del_config_comfirm">حذف شود؟</string>
|
||||||
<string name="del_invalid_config_comfirm">لطفا قبل از حذف کانفیگ نامعتبر بررسی کنید! حذف کانفیگ را تایید می کنید؟</string>
|
<string name="del_invalid_config_comfirm">لطفا قبل از حذف کانفیگ نامعتبر بررسی کنید! حذف کانفیگ را تایید می کنید؟</string>
|
||||||
<string name="server_lab_remarks">ملاحظات</string>
|
<string name="server_lab_remarks">ملاحظات</string>
|
||||||
@@ -124,6 +119,7 @@
|
|||||||
<string name="title_user_asset_add_url">آدرس اینترنتی را اضافه کنید</string>
|
<string name="title_user_asset_add_url">آدرس اینترنتی را اضافه کنید</string>
|
||||||
<string name="msg_file_not_found">فایل پیدا نشد</string>
|
<string name="msg_file_not_found">فایل پیدا نشد</string>
|
||||||
<string name="msg_remark_is_duplicate">نام قبلاً وجود دارد</string>
|
<string name="msg_remark_is_duplicate">نام قبلاً وجود دارد</string>
|
||||||
|
<string name="asset_geo_files_sources">منبع فایل های جغرافیایی (اختیاری)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">بارگذاری</string>
|
<string name="msg_dialog_progress">بارگذاری</string>
|
||||||
@@ -175,7 +171,7 @@
|
|||||||
<string name="summary_pref_fake_dns_enabled">دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند)</string>
|
<string name="summary_pref_fake_dns_enabled">دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند)</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 را فعال کنید و آدرسهای 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>
|
||||||
@@ -259,6 +255,7 @@
|
|||||||
<string name="sub_setting_filter">نام مستعار فیلتر</string>
|
<string name="sub_setting_filter">نام مستعار فیلتر</string>
|
||||||
<string name="sub_setting_enable">فعال کردن بهروزرسانی</string>
|
<string name="sub_setting_enable">فعال کردن بهروزرسانی</string>
|
||||||
<string name="sub_auto_update">فعال سازی بهروزرسانی خودکار</string>
|
<string name="sub_auto_update">فعال سازی بهروزرسانی خودکار</string>
|
||||||
|
<string name="sub_allow_insecure_url">مجاز کردن آدرس HTTP ناامن</string>
|
||||||
<string name="sub_setting_pre_profile">نام مستعار پروکسی قبلی</string>
|
<string name="sub_setting_pre_profile">نام مستعار پروکسی قبلی</string>
|
||||||
<string name="sub_setting_next_profile">نام مستعار پروکسی بعدی</string>
|
<string name="sub_setting_next_profile">نام مستعار پروکسی بعدی</string>
|
||||||
<string name="sub_setting_pre_profile_tip">لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است</string>
|
<string name="sub_setting_pre_profile_tip">لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است</string>
|
||||||
@@ -304,7 +301,7 @@
|
|||||||
<string name="connection_test_pending">اتصال را بررسی کنید</string>
|
<string name="connection_test_pending">اتصال را بررسی کنید</string>
|
||||||
<string name="connection_test_testing">در حال آزمایش...</string>
|
<string name="connection_test_testing">در حال آزمایش...</string>
|
||||||
<string name="connection_test_testing_count">تست کردن %d کانفیگ…</string>
|
<string name="connection_test_testing_count">تست کردن %d کانفیگ…</string>
|
||||||
<string name="connection_test_available">موفقیت: اتصال HTTP %dms طول کشید</string>
|
<string name="connection_test_available">موفقیت: اتصال %dms طول کشید</string>
|
||||||
<string name="connection_test_error">اتصال به اینترنت شناسایی نشد: %s</string>
|
<string name="connection_test_error">اتصال به اینترنت شناسایی نشد: %s</string>
|
||||||
<string name="connection_test_fail">اینترنت در دسترس نیست</string>
|
<string name="connection_test_fail">اینترنت در دسترس نیست</string>
|
||||||
<string name="connection_test_error_status_code">کد خطا: #%d</string>
|
<string name="connection_test_error_status_code">کد خطا: #%d</string>
|
||||||
@@ -319,6 +316,12 @@
|
|||||||
<string name="title_pref_fragment_interval">فاصله بین بسته های فرگمنت (حداقل-حداکثر)</string>
|
<string name="title_pref_fragment_interval">فاصله بین بسته های فرگمنت (حداقل-حداکثر)</string>
|
||||||
<string name="title_pref_fragment_enabled">فعال کردن فرگمنت</string>
|
<string name="title_pref_fragment_enabled">فعال کردن فرگمنت</string>
|
||||||
|
|
||||||
|
<string name="update_check_for_update">بررسی به روز رسانی</string>
|
||||||
|
<string name="update_already_latest_version">در حال حاضر آخرین نسخه نصب شده است</string>
|
||||||
|
<string name="update_new_version_found">نسخه جدید پیدا شد: %s</string>
|
||||||
|
<string name="update_now">اکنون به روز رسانی کنید</string>
|
||||||
|
<string name="update_check_pre_release">بررسی نسخه پیش از انتشار</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QRcode</item>
|
<item>QRcode</item>
|
||||||
<item>خروجی گرفتن در کلیپ بورد</item>
|
<item>خروجی گرفتن در کلیپ بورد</item>
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
<string name="menu_item_import_config_manually_trojan">Ручной ввод Trojan</string>
|
<string name="menu_item_import_config_manually_trojan">Ручной ввод Trojan</string>
|
||||||
<string name="menu_item_import_config_manually_wireguard">Ручной ввод WireGuard</string>
|
<string name="menu_item_import_config_manually_wireguard">Ручной ввод WireGuard</string>
|
||||||
<string name="menu_item_import_config_manually_hysteria2">Ручной ввод Hysteria2</string>
|
<string name="menu_item_import_config_manually_hysteria2">Ручной ввод Hysteria2</string>
|
||||||
<string name="menu_item_import_config_custom">Другой профиль</string>
|
|
||||||
<string name="menu_item_import_config_custom_clipboard">Импорт из буфера обмена</string>
|
|
||||||
<string name="menu_item_import_config_custom_local">Импорт из файла</string>
|
|
||||||
<string name="menu_item_import_config_custom_url">Импорт из URL</string>
|
|
||||||
<string name="menu_item_import_config_custom_url_scan">Импорт сканированием URL</string>
|
|
||||||
<string name="del_config_comfirm">Подтверждаете удаление?</string>
|
<string name="del_config_comfirm">Подтверждаете удаление?</string>
|
||||||
<string name="del_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string>
|
<string name="del_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string>
|
||||||
<string name="server_lab_remarks">Название</string>
|
<string name="server_lab_remarks">Название</string>
|
||||||
@@ -126,6 +121,7 @@
|
|||||||
<string name="title_user_asset_add_url">Добавить URL ресурса</string>
|
<string name="title_user_asset_add_url">Добавить URL ресурса</string>
|
||||||
<string name="msg_file_not_found">Файл не найден</string>
|
<string name="msg_file_not_found">Файл не найден</string>
|
||||||
<string name="msg_remark_is_duplicate">Название уже существует</string>
|
<string name="msg_remark_is_duplicate">Название уже существует</string>
|
||||||
|
<string name="asset_geo_files_sources">Источник геофайлов (необязательно)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">Загрузка…</string>
|
<string name="msg_dialog_progress">Загрузка…</string>
|
||||||
@@ -176,7 +172,7 @@
|
|||||||
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
|
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</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 предпочитать 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>
|
||||||
@@ -261,9 +257,10 @@
|
|||||||
<string name="sub_setting_filter">Название фильтра</string>
|
<string name="sub_setting_filter">Название фильтра</string>
|
||||||
<string name="sub_setting_enable">Использовать обновление</string>
|
<string name="sub_setting_enable">Использовать обновление</string>
|
||||||
<string name="sub_auto_update">Использовать автообновление</string>
|
<string name="sub_auto_update">Использовать автообновление</string>
|
||||||
<string name="sub_setting_pre_profile">Название предыдущего прокси</string>
|
<string name="sub_allow_insecure_url">Разрешать незащищённые HTTP-адреса</string>
|
||||||
<string name="sub_setting_next_profile">Название следующего прокси</string>
|
<string name="sub_setting_pre_profile">Предыдущая конфигурация прокси</string>
|
||||||
<string name="sub_setting_pre_profile_tip">Название должно существовать и быть уникальным</string>
|
<string name="sub_setting_next_profile">Следующая конфигурация прокси</string>
|
||||||
|
<string name="sub_setting_pre_profile_tip">Конфигурация должна быть уникальной</string>
|
||||||
<string name="title_sub_update">Обновить подписку группы</string>
|
<string name="title_sub_update">Обновить подписку группы</string>
|
||||||
<string name="title_ping_all_server">Проверка профилей группы</string>
|
<string name="title_ping_all_server">Проверка профилей группы</string>
|
||||||
<string name="title_real_ping_all_server">Время отклика профилей группы</string>
|
<string name="title_real_ping_all_server">Время отклика профилей группы</string>
|
||||||
@@ -306,7 +303,7 @@
|
|||||||
<string name="connection_test_pending">Проверить подключение</string>
|
<string name="connection_test_pending">Проверить подключение</string>
|
||||||
<string name="connection_test_testing">Проверка…</string>
|
<string name="connection_test_testing">Проверка…</string>
|
||||||
<string name="connection_test_testing_count">Проверка профилей (%d)</string>
|
<string name="connection_test_testing_count">Проверка профилей (%d)</string>
|
||||||
<string name="connection_test_available">Успешно: HTTP-соединение заняло %d мс</string>
|
<string name="connection_test_available">Успешно: соединение заняло %d мс</string>
|
||||||
<string name="connection_test_error">Сбой проверки интернет-соединения: %s</string>
|
<string name="connection_test_error">Сбой проверки интернет-соединения: %s</string>
|
||||||
<string name="connection_test_fail">Интернет недоступен</string>
|
<string name="connection_test_fail">Интернет недоступен</string>
|
||||||
<string name="connection_test_error_status_code">Код ошибки: #%d</string>
|
<string name="connection_test_error_status_code">Код ошибки: #%d</string>
|
||||||
@@ -321,6 +318,12 @@
|
|||||||
<string name="title_pref_fragment_interval">Интервал фрагментов (от - до)</string>
|
<string name="title_pref_fragment_interval">Интервал фрагментов (от - до)</string>
|
||||||
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string>
|
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string>
|
||||||
|
|
||||||
|
<string name="update_check_for_update">Проверить обновление</string>
|
||||||
|
<string name="update_already_latest_version">Установлена последняя версия</string>
|
||||||
|
<string name="update_new_version_found">Найдена новая версия: %s</string>
|
||||||
|
<string name="update_now">Обновить</string>
|
||||||
|
<string name="update_check_pre_release">Искать предварительный выпуск</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QR-код</item>
|
<item>QR-код</item>
|
||||||
<item>Экспорт в буфер обмена</item>
|
<item>Экспорт в буфер обмена</item>
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
<string name="menu_item_import_config_manually_trojan">Nhập thủ công [Trojan]</string>
|
<string name="menu_item_import_config_manually_trojan">Nhập thủ công [Trojan]</string>
|
||||||
<string name="menu_item_import_config_manually_wireguard">Nhập thủ công [WireGuard]</string>
|
<string name="menu_item_import_config_manually_wireguard">Nhập thủ công [WireGuard]</string>
|
||||||
<string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string>
|
<string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string>
|
||||||
<string name="menu_item_import_config_custom">Nâng cao / Cấu hình tùy chỉnh</string>
|
|
||||||
<string name="menu_item_import_config_custom_clipboard">Nhập cấu hình tùy chỉnh từ Clipboard</string>
|
|
||||||
<string name="menu_item_import_config_custom_local">Nhập cấu hình tùy chỉnh từ Tệp</string>
|
|
||||||
<string name="menu_item_import_config_custom_url">Nhập cấu hình tùy chỉnh từ URL</string>
|
|
||||||
<string name="menu_item_import_config_custom_url_scan">Nhập cấu hình tùy chỉnh quét URL</string>
|
|
||||||
<string name="del_config_comfirm">Xác nhận xóa?</string>
|
<string name="del_config_comfirm">Xác nhận xóa?</string>
|
||||||
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||||
<string name="server_lab_remarks">Tên cấu hình</string>
|
<string name="server_lab_remarks">Tên cấu hình</string>
|
||||||
@@ -124,6 +119,7 @@
|
|||||||
<string name="title_user_asset_add_url">Thêm URL nội dung</string>
|
<string name="title_user_asset_add_url">Thêm URL nội dung</string>
|
||||||
<string name="msg_file_not_found">Không tìm thấy tập tin!</string>
|
<string name="msg_file_not_found">Không tìm thấy tập tin!</string>
|
||||||
<string name="msg_remark_is_duplicate">Nhận xét đã tồn tại!</string>
|
<string name="msg_remark_is_duplicate">Nhận xét đã tồn tại!</string>
|
||||||
|
<string name="asset_geo_files_sources">Geo files source (optional)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">Đang tải...</string>
|
<string name="msg_dialog_progress">Đang tải...</string>
|
||||||
@@ -262,9 +258,10 @@
|
|||||||
<string name="sub_setting_filter">Remarks regular filter</string>
|
<string name="sub_setting_filter">Remarks regular filter</string>
|
||||||
<string name="sub_setting_enable">Sử dụng gói đăng ký này</string>
|
<string name="sub_setting_enable">Sử dụng gói đăng ký này</string>
|
||||||
<string name="sub_auto_update">Bật tự động cập nhật</string>
|
<string name="sub_auto_update">Bật tự động cập nhật</string>
|
||||||
<string name="sub_setting_pre_profile">Previous proxy remarks</string>
|
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
|
||||||
<string name="sub_setting_next_profile">Next proxy remarks</string>
|
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
|
||||||
<string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string>
|
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
|
||||||
|
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||||
<string name="title_sub_update">Cập nhật các gói đăng ký</string>
|
<string name="title_sub_update">Cập nhật các gói đăng ký</string>
|
||||||
<string name="title_ping_all_server">Ping tất cả máy chủ</string>
|
<string name="title_ping_all_server">Ping tất cả máy chủ</string>
|
||||||
<string name="title_real_ping_all_server">Kiểm tra HTTP tất cả máy chủ</string>
|
<string name="title_real_ping_all_server">Kiểm tra HTTP tất cả máy chủ</string>
|
||||||
@@ -309,6 +306,18 @@
|
|||||||
<string name="import_subscription_success">Nhập gói đăng ký thành công!</string>
|
<string name="import_subscription_success">Nhập gói đăng ký thành công!</string>
|
||||||
<string name="import_subscription_failure">Nhập gói đăng ký không thành công!</string>
|
<string name="import_subscription_failure">Nhập gói đăng ký không thành công!</string>
|
||||||
|
|
||||||
|
<string name="title_fragment_settings">Fragment Settings</string>
|
||||||
|
<string name="title_pref_fragment_packets">Fragment Packets</string>
|
||||||
|
<string name="title_pref_fragment_length">Fragment Length (min-max)</string>
|
||||||
|
<string name="title_pref_fragment_interval">Fragment Interval (min-max)</string>
|
||||||
|
<string name="title_pref_fragment_enabled">Enable Fragment</string>
|
||||||
|
|
||||||
|
<string name="update_check_for_update">Check for update</string>
|
||||||
|
<string name="update_already_latest_version">Already on the latest version</string>
|
||||||
|
<string name="update_new_version_found">New version found: %s</string>
|
||||||
|
<string name="update_now">Update now</string>
|
||||||
|
<string name="update_check_pre_release">Check Pre-release</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>Xuất ra mã QR (Chụp màn hình để lưu)</item>
|
<item>Xuất ra mã QR (Chụp màn hình để lưu)</item>
|
||||||
<item>Sao chép vào Clipboard</item>
|
<item>Sao chép vào Clipboard</item>
|
||||||
@@ -340,12 +349,6 @@
|
|||||||
<item>Sáng</item>
|
<item>Sáng</item>
|
||||||
<item>Tối</item>
|
<item>Tối</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string name="title_fragment_settings">Fragment Settings</string>
|
|
||||||
<string name="title_pref_fragment_packets">Fragment Packets</string>
|
|
||||||
<string name="title_pref_fragment_length">Fragment Length (min-max)</string>
|
|
||||||
<string name="title_pref_fragment_interval">Fragment Interval (min-max)</string>
|
|
||||||
<string name="title_pref_fragment_enabled">Enable Fragment</string>
|
|
||||||
|
|
||||||
<string-array name="vpn_bypass_lan">
|
<string-array name="vpn_bypass_lan">
|
||||||
<item>Follow config</item>
|
<item>Follow config</item>
|
||||||
<item>Bypass</item>
|
<item>Bypass</item>
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
<string name="menu_item_import_config_manually_trojan">手动输入 [Trojan]</string>
|
<string name="menu_item_import_config_manually_trojan">手动输入 [Trojan]</string>
|
||||||
<string name="menu_item_import_config_manually_wireguard">手动输入 [Wireguard]</string>
|
<string name="menu_item_import_config_manually_wireguard">手动输入 [Wireguard]</string>
|
||||||
<string name="menu_item_import_config_manually_hysteria2">手动输入 [Hysteria2]</string>
|
<string name="menu_item_import_config_manually_hysteria2">手动输入 [Hysteria2]</string>
|
||||||
<string name="menu_item_import_config_custom">自定义配置</string>
|
|
||||||
<string name="menu_item_import_config_custom_clipboard">从剪贴板导入自定义配置</string>
|
|
||||||
<string name="menu_item_import_config_custom_local">从本地导入自定义配置</string>
|
|
||||||
<string name="menu_item_import_config_custom_url">剪贴板 URL 导入自定义配置</string>
|
|
||||||
<string name="menu_item_import_config_custom_url_scan">扫描 URL 导入自定义配置</string>
|
|
||||||
<string name="del_config_comfirm">确认删除?</string>
|
<string name="del_config_comfirm">确认删除?</string>
|
||||||
<string name="del_invalid_config_comfirm">删除前请先测试!确认删除?</string>
|
<string name="del_invalid_config_comfirm">删除前请先测试!确认删除?</string>
|
||||||
<string name="server_lab_remarks">别名 (remarks)</string>
|
<string name="server_lab_remarks">别名 (remarks)</string>
|
||||||
@@ -124,6 +119,7 @@
|
|||||||
<string name="title_user_asset_add_url">添加资产网址</string>
|
<string name="title_user_asset_add_url">添加资产网址</string>
|
||||||
<string name="msg_file_not_found">文件未找到</string>
|
<string name="msg_file_not_found">文件未找到</string>
|
||||||
<string name="msg_remark_is_duplicate">备注已经存在</string>
|
<string name="msg_remark_is_duplicate">备注已经存在</string>
|
||||||
|
<string name="asset_geo_files_sources">Geo 文件来源 (可选)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">正在加载</string>
|
<string name="msg_dialog_progress">正在加载</string>
|
||||||
@@ -259,9 +255,10 @@
|
|||||||
<string name="sub_setting_filter">别名正则过滤</string>
|
<string name="sub_setting_filter">别名正则过滤</string>
|
||||||
<string name="sub_setting_enable">启用更新</string>
|
<string name="sub_setting_enable">启用更新</string>
|
||||||
<string name="sub_auto_update">启用自动更新</string>
|
<string name="sub_auto_update">启用自动更新</string>
|
||||||
<string name="sub_setting_pre_profile">前置代理别名</string>
|
<string name="sub_allow_insecure_url">允许不安全的 HTTP 地址</string>
|
||||||
<string name="sub_setting_next_profile">落地代理別名</string>
|
<string name="sub_setting_pre_profile">前置代理配置文件别名</string>
|
||||||
<string name="sub_setting_pre_profile_tip">请确保别名存在并唯一</string>
|
<string name="sub_setting_next_profile">落地代理配置文件別名</string>
|
||||||
|
<string name="sub_setting_pre_profile_tip">请确保配置文件别名存在并唯一</string>
|
||||||
<string name="title_sub_update">更新当前组订阅</string>
|
<string name="title_sub_update">更新当前组订阅</string>
|
||||||
<string name="title_ping_all_server">测试当前组配置 Tcping</string>
|
<string name="title_ping_all_server">测试当前组配置 Tcping</string>
|
||||||
<string name="title_real_ping_all_server">测试当前组配置真连接</string>
|
<string name="title_real_ping_all_server">测试当前组配置真连接</string>
|
||||||
@@ -313,6 +310,12 @@
|
|||||||
<string name="title_pref_fragment_interval">分片间隔(最小 - 最大)</string>
|
<string name="title_pref_fragment_interval">分片间隔(最小 - 最大)</string>
|
||||||
<string name="title_pref_fragment_enabled">启用分片(Fragment)</string>
|
<string name="title_pref_fragment_enabled">启用分片(Fragment)</string>
|
||||||
|
|
||||||
|
<string name="update_check_for_update">检查更新</string>
|
||||||
|
<string name="update_already_latest_version">目前已是最新版本</string>
|
||||||
|
<string name="update_new_version_found">发现新版本: %s</string>
|
||||||
|
<string name="update_now">立即更新</string>
|
||||||
|
<string name="update_check_pre_release">检查 Pre-release</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>二维码</item>
|
<item>二维码</item>
|
||||||
<item>导出至剪贴板</item>
|
<item>导出至剪贴板</item>
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
<string name="menu_item_import_config_manually_trojan">手動鍵入 [Trojan]</string>
|
<string name="menu_item_import_config_manually_trojan">手動鍵入 [Trojan]</string>
|
||||||
<string name="menu_item_import_config_manually_wireguard">手動鍵入 [Wireguard]</string>
|
<string name="menu_item_import_config_manually_wireguard">手動鍵入 [Wireguard]</string>
|
||||||
<string name="menu_item_import_config_manually_hysteria2">手動鍵入 [Hysteria2]</string>
|
<string name="menu_item_import_config_manually_hysteria2">手動鍵入 [Hysteria2]</string>
|
||||||
<string name="menu_item_import_config_custom">自訂設定</string>
|
|
||||||
<string name="menu_item_import_config_custom_clipboard">從剪貼簿匯入自訂設定</string>
|
|
||||||
<string name="menu_item_import_config_custom_local">從本地匯入自訂設定</string>
|
|
||||||
<string name="menu_item_import_config_custom_url">從 URL 匯入自訂設定</string>
|
|
||||||
<string name="menu_item_import_config_custom_url_scan">掃描 URL 匯入自訂設定</string>
|
|
||||||
<string name="del_config_comfirm">確定刪除?</string>
|
<string name="del_config_comfirm">確定刪除?</string>
|
||||||
<string name="del_invalid_config_comfirm">刪除前請先測試!確認刪除?</string>
|
<string name="del_invalid_config_comfirm">刪除前請先測試!確認刪除?</string>
|
||||||
<string name="server_lab_remarks">備註</string>
|
<string name="server_lab_remarks">備註</string>
|
||||||
@@ -124,6 +119,7 @@
|
|||||||
<string name="title_user_asset_add_url">新增資產網址</string>
|
<string name="title_user_asset_add_url">新增資產網址</string>
|
||||||
<string name="msg_file_not_found">文件未找到</string>
|
<string name="msg_file_not_found">文件未找到</string>
|
||||||
<string name="msg_remark_is_duplicate">備註已經存在</string>
|
<string name="msg_remark_is_duplicate">備註已經存在</string>
|
||||||
|
<string name="asset_geo_files_sources">Geo 檔案來源 (可選)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">載入</string>
|
<string name="msg_dialog_progress">載入</string>
|
||||||
@@ -260,9 +256,10 @@
|
|||||||
<string name="sub_setting_filter">別名正規過濾</string>
|
<string name="sub_setting_filter">別名正規過濾</string>
|
||||||
<string name="sub_setting_enable">啟用更新</string>
|
<string name="sub_setting_enable">啟用更新</string>
|
||||||
<string name="sub_auto_update">啟用自動更新</string>
|
<string name="sub_auto_update">啟用自動更新</string>
|
||||||
<string name="sub_setting_pre_profile">前置代理别名</string>
|
<string name="sub_allow_insecure_url">允許不安全的 HTTP 位址</string>
|
||||||
<string name="sub_setting_next_profile">落地代理別名</string>
|
<string name="sub_setting_pre_profile">前置代理設定檔别名</string>
|
||||||
<string name="sub_setting_pre_profile_tip">请确保别名存在并唯一</string>
|
<string name="sub_setting_next_profile">落地代理設定檔別名</string>
|
||||||
|
<string name="sub_setting_pre_profile_tip">请确保設定檔别名存在并唯一</string>
|
||||||
<string name="title_sub_update">更新目前群組訂閱</string>
|
<string name="title_sub_update">更新目前群組訂閱</string>
|
||||||
<string name="title_ping_all_server">偵測目前群組設定 Tcping</string>
|
<string name="title_ping_all_server">偵測目前群組設定 Tcping</string>
|
||||||
<string name="title_real_ping_all_server">偵測目前群組設定真延遲</string>
|
<string name="title_real_ping_all_server">偵測目前群組設定真延遲</string>
|
||||||
@@ -313,6 +310,12 @@
|
|||||||
<string name="title_pref_fragment_interval">分片間隔(最小-最大)</string>
|
<string name="title_pref_fragment_interval">分片間隔(最小-最大)</string>
|
||||||
<string name="title_pref_fragment_enabled">啟用分片(Fragment)</string>
|
<string name="title_pref_fragment_enabled">啟用分片(Fragment)</string>
|
||||||
|
|
||||||
|
<string name="update_check_for_update">檢查更新</string>
|
||||||
|
<string name="update_already_latest_version">当前已是最新版本</string>
|
||||||
|
<string name="update_new_version_found">發現新版本: %s</string>
|
||||||
|
<string name="update_now">立即更新</string>
|
||||||
|
<string name="update_check_pre_release">檢查 Pre-release</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QR Code</item>
|
<item>QR Code</item>
|
||||||
<item>匯出至剪貼簿</item>
|
<item>匯出至剪貼簿</item>
|
||||||
|
|||||||
@@ -124,43 +124,6 @@
|
|||||||
<item>xtls-rprx-vision-udp443</item>
|
<item>xtls-rprx-vision-udp443</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
|
||||||
<!-- minimum list https://serverfault.com/a/304791 -->
|
|
||||||
<string-array name="bypass_private_ip_address" translatable="false">
|
|
||||||
<item>0.0.0.0/5</item>
|
|
||||||
<item>8.0.0.0/7</item>
|
|
||||||
<item>11.0.0.0/8</item>
|
|
||||||
<item>12.0.0.0/6</item>
|
|
||||||
<item>16.0.0.0/4</item>
|
|
||||||
<item>32.0.0.0/3</item>
|
|
||||||
<item>64.0.0.0/2</item>
|
|
||||||
<item>128.0.0.0/3</item>
|
|
||||||
<item>160.0.0.0/5</item>
|
|
||||||
<item>168.0.0.0/6</item>
|
|
||||||
<item>172.0.0.0/12</item>
|
|
||||||
<item>172.32.0.0/11</item>
|
|
||||||
<item>172.64.0.0/10</item>
|
|
||||||
<item>172.128.0.0/9</item>
|
|
||||||
<item>173.0.0.0/8</item>
|
|
||||||
<item>174.0.0.0/7</item>
|
|
||||||
<item>176.0.0.0/4</item>
|
|
||||||
<item>192.0.0.0/9</item>
|
|
||||||
<item>192.128.0.0/11</item>
|
|
||||||
<item>192.160.0.0/13</item>
|
|
||||||
<item>192.169.0.0/16</item>
|
|
||||||
<item>192.170.0.0/15</item>
|
|
||||||
<item>192.172.0.0/14</item>
|
|
||||||
<item>192.176.0.0/12</item>
|
|
||||||
<item>192.192.0.0/10</item>
|
|
||||||
<item>193.0.0.0/8</item>
|
|
||||||
<item>194.0.0.0/7</item>
|
|
||||||
<item>196.0.0.0/6</item>
|
|
||||||
<item>200.0.0.0/5</item>
|
|
||||||
<item>208.0.0.0/4</item>
|
|
||||||
<item>240.0.0.0/4</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
|
|
||||||
<string-array name="language_select" translatable="false">
|
<string-array name="language_select" translatable="false">
|
||||||
<item>auto</item>
|
<item>auto</item>
|
||||||
<item>English</item>
|
<item>English</item>
|
||||||
|
|||||||
@@ -36,11 +36,6 @@
|
|||||||
<string name="menu_item_import_config_manually_trojan">Type manually[Trojan]</string>
|
<string name="menu_item_import_config_manually_trojan">Type manually[Trojan]</string>
|
||||||
<string name="menu_item_import_config_manually_wireguard">Type manually[Wireguard]</string>
|
<string name="menu_item_import_config_manually_wireguard">Type manually[Wireguard]</string>
|
||||||
<string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string>
|
<string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string>
|
||||||
<string name="menu_item_import_config_custom">Custom config</string>
|
|
||||||
<string name="menu_item_import_config_custom_clipboard">Import custom config from Clipboard</string>
|
|
||||||
<string name="menu_item_import_config_custom_local">Import custom config from locally</string>
|
|
||||||
<string name="menu_item_import_config_custom_url">Import custom config from URL</string>
|
|
||||||
<string name="menu_item_import_config_custom_url_scan">Import custom config scan URL</string>
|
|
||||||
<string name="del_config_comfirm">Confirm delete ?</string>
|
<string name="del_config_comfirm">Confirm delete ?</string>
|
||||||
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||||
<string name="server_lab_remarks">remarks</string>
|
<string name="server_lab_remarks">remarks</string>
|
||||||
@@ -127,6 +122,7 @@
|
|||||||
<string name="title_user_asset_add_url">Add asset URL</string>
|
<string name="title_user_asset_add_url">Add asset URL</string>
|
||||||
<string name="msg_file_not_found">File not found</string>
|
<string name="msg_file_not_found">File not found</string>
|
||||||
<string name="msg_remark_is_duplicate">The remarks already exists</string>
|
<string name="msg_remark_is_duplicate">The remarks already exists</string>
|
||||||
|
<string name="asset_geo_files_sources">Geo files source (optional)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">Loading</string>
|
<string name="msg_dialog_progress">Loading</string>
|
||||||
@@ -178,7 +174,7 @@
|
|||||||
<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_prefer_ipv6">Prefer IPv6</string>
|
<string name="title_pref_prefer_ipv6">Prefer IPv6</string>
|
||||||
<string name="summary_pref_prefer_ipv6">Prefer IPv6 addresses and routes</string>
|
<string name="summary_pref_prefer_ipv6">Enable IPv6 routes and Prefer IPv6 addresses</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>
|
||||||
@@ -263,9 +259,10 @@
|
|||||||
<string name="sub_setting_filter">Remarks regular filter</string>
|
<string name="sub_setting_filter">Remarks regular filter</string>
|
||||||
<string name="sub_setting_enable">Enable update</string>
|
<string name="sub_setting_enable">Enable update</string>
|
||||||
<string name="sub_auto_update">Enable automatic update</string>
|
<string name="sub_auto_update">Enable automatic update</string>
|
||||||
<string name="sub_setting_pre_profile">Previous proxy remarks</string>
|
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
|
||||||
<string name="sub_setting_next_profile">Next proxy remarks</string>
|
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
|
||||||
<string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string>
|
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
|
||||||
|
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||||
<string name="title_sub_update">Update current group subscription</string>
|
<string name="title_sub_update">Update current group subscription</string>
|
||||||
<string name="title_ping_all_server">Tcping current group configuration</string>
|
<string name="title_ping_all_server">Tcping current group configuration</string>
|
||||||
<string name="title_real_ping_all_server">Real delay current group configuration</string>
|
<string name="title_real_ping_all_server">Real delay current group configuration</string>
|
||||||
@@ -308,7 +305,7 @@
|
|||||||
<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>
|
||||||
<string name="connection_test_testing_count">Testing %d configurations…</string>
|
<string name="connection_test_testing_count">Testing %d configurations…</string>
|
||||||
<string name="connection_test_available">Success: HTTP connection took %dms</string>
|
<string name="connection_test_available">Success: Connection took %dms</string>
|
||||||
<string name="connection_test_error">Fail to detect internet connection: %s</string>
|
<string name="connection_test_error">Fail to detect internet connection: %s</string>
|
||||||
<string name="connection_test_fail">Internet Unavailable</string>
|
<string name="connection_test_fail">Internet Unavailable</string>
|
||||||
<string name="connection_test_error_status_code">Error code: #%d</string>
|
<string name="connection_test_error_status_code">Error code: #%d</string>
|
||||||
@@ -323,6 +320,12 @@
|
|||||||
<string name="title_pref_fragment_interval">Fragment Interval (min-max)</string>
|
<string name="title_pref_fragment_interval">Fragment Interval (min-max)</string>
|
||||||
<string name="title_pref_fragment_enabled">Enable Fragment</string>
|
<string name="title_pref_fragment_enabled">Enable Fragment</string>
|
||||||
|
|
||||||
|
<string name="update_check_for_update">Check for update</string>
|
||||||
|
<string name="update_already_latest_version">Already on the latest version</string>
|
||||||
|
<string name="update_new_version_found">New version found: %s</string>
|
||||||
|
<string name="update_now">Update now</string>
|
||||||
|
<string name="update_check_pre_release">Check Pre-release</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QRcode</item>
|
<item>QRcode</item>
|
||||||
<item>Export to clipboard</item>
|
<item>Export to clipboard</item>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||||
<base-config>
|
<base-config cleartextTrafficPermitted="true">
|
||||||
<trust-anchors>
|
<trust-anchors>
|
||||||
<certificates src="system" />
|
<certificates src="system" />
|
||||||
<certificates
|
<certificates
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
android:title="@string/title_pref_is_booted" />
|
android:title="@string/title_pref_is_booted" />
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/title_vpn_settings">
|
<PreferenceCategory android:title="@string/title_vpn_settings">
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:key="pref_prefer_ipv6"
|
||||||
|
android:summary="@string/summary_pref_prefer_ipv6"
|
||||||
|
android:title="@string/title_pref_prefer_ipv6" />
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:key="pref_per_app_proxy"
|
android:key="pref_per_app_proxy"
|
||||||
android:summary="@string/summary_pref_per_app_proxy"
|
android:summary="@string/summary_pref_per_app_proxy"
|
||||||
@@ -170,11 +175,6 @@
|
|||||||
android:title="@string/title_advanced"
|
android:title="@string/title_advanced"
|
||||||
app:initialExpandedChildrenCount="0">
|
app:initialExpandedChildrenCount="0">
|
||||||
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:key="pref_prefer_ipv6"
|
|
||||||
android:summary="@string/summary_pref_prefer_ipv6"
|
|
||||||
android:title="@string/title_pref_prefer_ipv6" />
|
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:key="pref_proxy_sharing_enabled"
|
android:key="pref_proxy_sharing_enabled"
|
||||||
|
|||||||
41
V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt
Normal file
41
V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package com.v2ray.ang
|
||||||
|
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class HttpUtilTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIdnToASCII() {
|
||||||
|
// Regular URL remains unchanged
|
||||||
|
val regularUrl = "https://example.com/path"
|
||||||
|
assertEquals(regularUrl, HttpUtil.idnToASCII(regularUrl))
|
||||||
|
|
||||||
|
// Non-ASCII URL converts to ASCII (Punycode)
|
||||||
|
val nonAsciiUrl = "https://例子.测试/path"
|
||||||
|
val expectedNonAscii = "https://xn--fsqu00a.xn--0zwm56d/path"
|
||||||
|
assertEquals(expectedNonAscii, HttpUtil.idnToASCII(nonAsciiUrl))
|
||||||
|
|
||||||
|
// Mixed URL only converts the host part
|
||||||
|
val mixedUrl = "https://例子.com/测试"
|
||||||
|
val expectedMixed = "https://xn--fsqu00a.com/测试"
|
||||||
|
assertEquals(expectedMixed, HttpUtil.idnToASCII(mixedUrl))
|
||||||
|
|
||||||
|
// URL with Basic Authentication using regular domain
|
||||||
|
val basicAuthUrl = "https://user:password@example.com/path"
|
||||||
|
assertEquals(basicAuthUrl, HttpUtil.idnToASCII(basicAuthUrl))
|
||||||
|
|
||||||
|
// URL with Basic Authentication using non-ASCII domain
|
||||||
|
val basicAuthNonAscii = "https://user:password@例子.测试/path"
|
||||||
|
val expectedBasicAuthNonAscii = "https://user:password@xn--fsqu00a.xn--0zwm56d/path"
|
||||||
|
assertEquals(expectedBasicAuthNonAscii, HttpUtil.idnToASCII(basicAuthNonAscii))
|
||||||
|
|
||||||
|
// URL with non-ASCII username and password
|
||||||
|
val nonAsciiAuth = "https://用户:密码@example.com/path"
|
||||||
|
// Basic auth credentials should remain unchanged as they're percent-encoded separately
|
||||||
|
assertEquals(nonAsciiAuth, HttpUtil.idnToASCII(nonAsciiAuth))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import org.junit.Test
|
|||||||
*
|
*
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
class AngUnitTest {
|
class UtilsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_parseInt() {
|
fun test_parseInt() {
|
||||||
@@ -45,4 +45,18 @@ class AngUnitTest {
|
|||||||
assertTrue(Utils.isIpAddress("240e:1234:abcd:12::/64"))
|
assertTrue(Utils.isIpAddress("240e:1234:abcd:12::/64"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_IsIpInCidr() {
|
||||||
|
assertTrue(Utils.isIpInCidr("192.168.1.1", "192.168.1.0/24"))
|
||||||
|
assertTrue(Utils.isIpInCidr("192.168.1.254", "192.168.1.0/24"))
|
||||||
|
assertFalse(Utils.isIpInCidr("192.168.2.1", "192.168.1.0/24"))
|
||||||
|
|
||||||
|
assertTrue(Utils.isIpInCidr("10.0.0.0", "10.0.0.0/8"))
|
||||||
|
assertTrue(Utils.isIpInCidr("10.255.255.255", "10.0.0.0/8"))
|
||||||
|
assertFalse(Utils.isIpInCidr("11.0.0.0", "10.0.0.0/8"))
|
||||||
|
|
||||||
|
assertFalse(Utils.isIpInCidr("invalid-ip", "192.168.1.0/24"))
|
||||||
|
assertFalse(Utils.isIpInCidr("192.168.1.1", "invalid-cidr"))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.9.1"
|
agp = "8.9.3"
|
||||||
desugar_jdk_libs = "2.1.5"
|
desugarJdkLibs = "2.1.5"
|
||||||
gradleLicensePlugin = "0.9.8"
|
gradleLicensePlugin = "0.9.8"
|
||||||
kotlin = "2.1.20"
|
kotlin = "2.1.20"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.16.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
@@ -14,14 +14,14 @@ constraintlayout = "2.2.1"
|
|||||||
mmkvStatic = "1.3.12"
|
mmkvStatic = "1.3.12"
|
||||||
gson = "2.12.1"
|
gson = "2.12.1"
|
||||||
quickieFoss = "1.14.0"
|
quickieFoss = "1.14.0"
|
||||||
kotlinx-coroutines-android = "1.10.1"
|
kotlinxCoroutinesAndroid = "1.10.2"
|
||||||
kotlinx-coroutines-core = "1.10.1"
|
kotlinxCoroutinesCore = "1.10.2"
|
||||||
swiperefreshlayout = "1.1.0"
|
swiperefreshlayout = "1.1.0"
|
||||||
toasty = "1.5.2"
|
toasty = "1.5.2"
|
||||||
editorkit = "2.9.0"
|
editorkit = "2.9.0"
|
||||||
core = "3.5.3"
|
core = "3.5.3"
|
||||||
workRuntimeKtx = "2.10.0"
|
workRuntimeKtx = "2.10.1"
|
||||||
lifecycleViewmodelKtx = "2.8.7"
|
lifecycleViewmodelKtx = "2.9.0"
|
||||||
multidex = "2.0.1"
|
multidex = "2.0.1"
|
||||||
mockitoMockitoInline = "5.2.0"
|
mockitoMockitoInline = "5.2.0"
|
||||||
flexbox = "3.0.0"
|
flexbox = "3.0.0"
|
||||||
@@ -30,7 +30,7 @@ recyclerview = "1.4.0"
|
|||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
|
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
|
||||||
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" }
|
||||||
gradle-license-plugin = { module = "com.jaredsburrows:gradle-license-plugin", version.ref = "gradleLicensePlugin" }
|
gradle-license-plugin = { module = "com.jaredsburrows:gradle-license-plugin", version.ref = "gradleLicensePlugin" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
@@ -42,8 +42,8 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
|
|||||||
mmkv-static = { module = "com.tencent:mmkv-static", version.ref = "mmkvStatic" }
|
mmkv-static = { module = "com.tencent:mmkv-static", version.ref = "mmkvStatic" }
|
||||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||||
quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" }
|
quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" }
|
||||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version = "kotlinx-coroutines-android" }
|
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||||
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "kotlinx-coroutines-core" }
|
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
|
||||||
toasty = { module = "com.github.GrenderG:Toasty", version.ref = "toasty" }
|
toasty = { module = "com.github.GrenderG:Toasty", version.ref = "toasty" }
|
||||||
editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" }
|
editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" }
|
||||||
language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" }
|
language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" }
|
||||||
|
|||||||
2
hysteria
2
hysteria
Submodule hysteria updated: 245c6e9bd1...2adeec2900
Reference in New Issue
Block a user