Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86e38c6963 | ||
|
|
9ba4d7e691 | ||
|
|
16e72787c9 | ||
|
|
3ffac8b29f | ||
|
|
10df1b44ea | ||
|
|
fa12878258 | ||
|
|
0f1ea1e119 | ||
|
|
1959608f24 | ||
|
|
0e041a6e9a | ||
|
|
c78ef380cc | ||
|
|
57362a4bde | ||
|
|
7f24ad534f | ||
|
|
680832614b | ||
|
|
4357abbff4 | ||
|
|
905be66c3f | ||
|
|
318a7b54a5 | ||
|
|
5db2df77a0 | ||
|
|
d039cb9edf | ||
|
|
9a1654bae9 | ||
|
|
3bf911da9c | ||
|
|
3f778a1ea2 | ||
|
|
8e03de8055 | ||
|
|
1f42d7fc07 | ||
|
|
0700e834f1 | ||
|
|
777190e861 | ||
|
|
33572477fc | ||
|
|
2fb6e62e13 | ||
|
|
94cc72d2b9 | ||
|
|
f68c353715 | ||
|
|
e077c18108 | ||
|
|
1a5e105212 | ||
|
|
e0881caab4 | ||
|
|
7219425258 | ||
|
|
51eabe5440 | ||
|
|
6f0b3ce990 | ||
|
|
69e27ed3bb | ||
|
|
fff6ab30e6 | ||
|
|
fdb67a86f4 | ||
|
|
ea088376ac | ||
|
|
52332d960e | ||
|
|
3ead542e2b | ||
|
|
9d1f98ff34 | ||
|
|
f305e26a39 | ||
|
|
aa47fba20d | ||
|
|
69c5bbfd3d | ||
|
|
90ed02804c | ||
|
|
822c1de79c | ||
|
|
d910b93525 | ||
|
|
7e6b1c247b | ||
|
|
f3f2b7fab5 | ||
|
|
e6f260da76 | ||
|
|
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 | ||
|
|
1e7f49b756 | ||
|
|
ac4c0f7ee1 | ||
|
|
6cc91b1a89 | ||
|
|
45facff41d | ||
|
|
ee703e6c95 | ||
|
|
87213c34a6 | ||
|
|
73a7c76183 | ||
|
|
ed5282f2b3 | ||
|
|
390c657047 | ||
|
|
7071072862 | ||
|
|
d111328541 | ||
|
|
76cb2aaf46 | ||
|
|
7ff1397163 |
2
.github/workflows/fastlane.yml
vendored
2
.github/workflows/fastlane.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Validate Fastlane Supply Metadata
|
||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0
|
||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0
|
||||
|
||||
Submodule AndroidLibXrayLite updated: c8a6ca7c5e...68d3a74511
@@ -3,16 +3,12 @@
|
||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||
|
||||
[](https://developer.android.com/about/versions/lollipop)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||
[](https://github.com/2dust/v2rayNG/releases)
|
||||
[](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
|
||||
[github_2dust](https://t.me/github_2dust)
|
||||
|
||||
@@ -21,7 +17,7 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
|
||||
#### Geoip and Geosite
|
||||
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
||||
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
||||
- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually
|
||||
- latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually
|
||||
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
|
||||
|
||||
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 643
|
||||
versionName = "1.9.43"
|
||||
versionCode = 662
|
||||
versionName = "1.10.12"
|
||||
multiDexEnabled = true
|
||||
|
||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
<data android:host="install-sub" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.CheckUpdateActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:exported="false" />
|
||||
|
||||
@@ -20,13 +20,6 @@
|
||||
"port": "443",
|
||||
"network": "udp"
|
||||
},
|
||||
{
|
||||
"remarks": "阻断广告",
|
||||
"outboundTag": "block",
|
||||
"domain": [
|
||||
"geosite:category-ads-all"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "绕过局域网IP",
|
||||
"outboundTag": "direct",
|
||||
|
||||
@@ -5,13 +5,6 @@
|
||||
"port": "443",
|
||||
"network": "udp"
|
||||
},
|
||||
{
|
||||
"remarks": "阻断广告",
|
||||
"outboundTag": "block",
|
||||
"domain": [
|
||||
"geosite:category-ads-all"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "绕过局域网IP",
|
||||
"outboundTag": "direct",
|
||||
|
||||
@@ -13,13 +13,6 @@
|
||||
"port": "443",
|
||||
"network": "udp"
|
||||
},
|
||||
{
|
||||
"remarks": "阻断广告",
|
||||
"outboundTag": "block",
|
||||
"domain": [
|
||||
"geosite:category-ads-all"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "绕过局域网IP",
|
||||
"outboundTag": "direct",
|
||||
|
||||
@@ -5,13 +5,6 @@
|
||||
"port": "443",
|
||||
"network": "udp"
|
||||
},
|
||||
{
|
||||
"remarks": "Block ads and trackers",
|
||||
"outboundTag": "block",
|
||||
"domain": [
|
||||
"geosite:category-ads-all"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "Direct LAN IP",
|
||||
"outboundTag": "direct",
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": []
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@@ -39,5 +39,9 @@ class AngApplication : MultiDexApplication() {
|
||||
WorkManager.initialize(this, workManagerConfiguration)
|
||||
|
||||
SettingsManager.initRoutingRulesets(this)
|
||||
|
||||
es.dmoral.toasty.Toasty.Config.getInstance()
|
||||
.setGravity(android.view.Gravity.BOTTOM, 0, 200)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ object AppConfig {
|
||||
|
||||
/** The application's package name. */
|
||||
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
|
||||
const val TAG = BuildConfig.APPLICATION_ID
|
||||
|
||||
/** Directory names used in the app's file system. */
|
||||
const val DIR_ASSETS = "assets"
|
||||
@@ -25,6 +26,7 @@ object AppConfig {
|
||||
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
||||
const val PREF_VPN_DNS = "pref_vpn_dns"
|
||||
const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
|
||||
const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index"
|
||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
||||
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
|
||||
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
||||
@@ -54,15 +56,19 @@ object AppConfig {
|
||||
const val PREF_DNS_HOSTS = "pref_dns_hosts"
|
||||
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
|
||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||
const val PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method"
|
||||
const val PREF_INTELLIGENT_SELECTION_METHOD = "pref_intelligent_selection_method"
|
||||
const val PREF_MODE = "pref_mode"
|
||||
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. */
|
||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
||||
|
||||
/** Protocol identifiers. */
|
||||
const val PROTOCOL_FREEDOM: String = "freedom"
|
||||
const val PROTOCOL_FREEDOM = "freedom"
|
||||
|
||||
/** Broadcast actions. */
|
||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
||||
@@ -87,19 +93,20 @@ object AppConfig {
|
||||
const val DOWNLINK = "downlink"
|
||||
|
||||
/** URLs for various resources. */
|
||||
const val androidpackagenamelistUrl =
|
||||
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
||||
const val v2rayCustomRoutingListUrl =
|
||||
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
||||
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
|
||||
const val v2rayNGIssues = "$v2rayNGUrl/issues"
|
||||
const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode"
|
||||
const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md"
|
||||
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
||||
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
|
||||
const val TgChannelUrl = "https://t.me/github_2dust"
|
||||
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
|
||||
const val DelayTestUrl2 = "https://www.google.com/generate_204"
|
||||
const val GITHUB_URL = "https://github.com"
|
||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
|
||||
const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
|
||||
const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
|
||||
const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
|
||||
const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
|
||||
const val APP_ISSUES_URL = "$APP_URL/issues"
|
||||
const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
|
||||
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
||||
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
||||
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
||||
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
||||
const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
||||
|
||||
/** DNS server addresses. */
|
||||
const val DNS_PROXY = "1.1.1.1"
|
||||
@@ -156,6 +163,7 @@ object AppConfig {
|
||||
|
||||
/** Give a good name to this, IDK*/
|
||||
const val VPN = "VPN"
|
||||
const val VPN_MTU = 1500
|
||||
|
||||
// Google API rule constants
|
||||
const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn"
|
||||
@@ -164,19 +172,13 @@ object AppConfig {
|
||||
// Android Private DNS constants
|
||||
const val DNS_DNSPOD_DOMAIN = "dot.pub"
|
||||
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
||||
const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one"
|
||||
const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one"
|
||||
const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com"
|
||||
const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com"
|
||||
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
||||
const val DNS_QUAD9_DOMAIN = "dns.quad9.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_SECURITY = "auto"
|
||||
const val DEFAULT_LEVEL = 8
|
||||
@@ -185,4 +187,64 @@ object AppConfig {
|
||||
const val REALITY = "reality"
|
||||
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_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||
val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5")
|
||||
val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9")
|
||||
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 ROUTED_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 url: String = "",
|
||||
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 guid: String? = null,
|
||||
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
|
||||
)
|
||||
@@ -8,6 +8,7 @@ enum class Language(val code: String) {
|
||||
VIETNAMESE("vi"),
|
||||
RUSSIAN("ru"),
|
||||
PERSIAN("fa"),
|
||||
ARABIC("ar"),
|
||||
BANGLA("bn"),
|
||||
BAKHTIARI("bqi-rIR");
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ data class ProfileItem(
|
||||
var publicKey: String? = null,
|
||||
var shortId: String? = null,
|
||||
var spiderX: String? = null,
|
||||
var mldsa65Verify: String? = null,
|
||||
|
||||
var secretKey: String? = null,
|
||||
var preSharedKey: 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,7 @@ data class SubscriptionItem(
|
||||
var prevProfile: String? = null,
|
||||
var nextProfile: String? = null,
|
||||
var filter: String? = null,
|
||||
var intelligentSelectionFilter: String? = null,
|
||||
var allowInsecureUrl: Boolean = false,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
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.reflect.TypeToken
|
||||
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 java.lang.reflect.Type
|
||||
|
||||
data class V2rayConfig(
|
||||
var remarks: String? = null,
|
||||
var stats: Any? = null,
|
||||
val log: LogBean,
|
||||
var policy: PolicyBean?,
|
||||
var policy: PolicyBean? = null,
|
||||
val inbounds: ArrayList<InboundBean>,
|
||||
var outbounds: ArrayList<OutboundBean>,
|
||||
var dns: DnsBean,
|
||||
var dns: DnsBean? = null,
|
||||
val routing: RoutingBean,
|
||||
val api: Any? = null,
|
||||
val transport: Any? = null,
|
||||
@@ -34,9 +23,9 @@ data class V2rayConfig(
|
||||
) {
|
||||
|
||||
data class LogBean(
|
||||
val access: String,
|
||||
val error: String,
|
||||
var loglevel: String?,
|
||||
val access: String? = null,
|
||||
val error: String? = null,
|
||||
var loglevel: String? = null,
|
||||
val dnsLog: Boolean? = null
|
||||
)
|
||||
|
||||
@@ -46,7 +35,7 @@ data class V2rayConfig(
|
||||
var protocol: String,
|
||||
var listen: String? = null,
|
||||
val settings: Any? = null,
|
||||
val sniffing: SniffingBean?,
|
||||
val sniffing: SniffingBean? = null,
|
||||
val streamSettings: Any? = null,
|
||||
val allocate: Any? = null
|
||||
) {
|
||||
@@ -77,50 +66,6 @@ data class V2rayConfig(
|
||||
val sendThrough: String? = null,
|
||||
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(
|
||||
var vnext: List<VnextBean>? = null,
|
||||
var fragment: FragmentBean? = null,
|
||||
@@ -197,7 +142,7 @@ data class V2rayConfig(
|
||||
|
||||
data class WireGuardBean(
|
||||
var publicKey: String = "",
|
||||
var preSharedKey: String = "",
|
||||
var preSharedKey: String? = null,
|
||||
var endpoint: String = ""
|
||||
)
|
||||
}
|
||||
@@ -299,7 +244,8 @@ data class V2rayConfig(
|
||||
var tcpFastOpen: Boolean? = null,
|
||||
var tproxy: String? = null,
|
||||
var mark: Int? = null,
|
||||
var dialerProxy: String? = null
|
||||
var dialerProxy: String? = null,
|
||||
var domainStrategy: String? = null
|
||||
)
|
||||
|
||||
data class TlsSettingsBean(
|
||||
@@ -318,7 +264,8 @@ data class V2rayConfig(
|
||||
val show: Boolean = false,
|
||||
var publicKey: String? = null,
|
||||
var shortId: String? = null,
|
||||
var spiderX: String? = null
|
||||
var spiderX: String? = null,
|
||||
var mldsa65Verify: String? = null
|
||||
)
|
||||
|
||||
data class QuicSettingBean(
|
||||
@@ -349,139 +296,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(
|
||||
@@ -647,6 +461,18 @@ data class V2rayConfig(
|
||||
}
|
||||
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(
|
||||
@@ -671,14 +497,14 @@ data class V2rayConfig(
|
||||
var domainStrategy: String,
|
||||
var domainMatcher: String? = null,
|
||||
var rules: ArrayList<RulesBean>,
|
||||
val balancers: List<Any>? = null
|
||||
var balancers: List<BalancerBean>? = null
|
||||
) {
|
||||
|
||||
data class RulesBean(
|
||||
var type: String = "field",
|
||||
var ip: ArrayList<String>? = null,
|
||||
var domain: ArrayList<String>? = null,
|
||||
var outboundTag: String = "",
|
||||
var outboundTag: String? = null,
|
||||
var balancerTag: String? = null,
|
||||
var port: String? = null,
|
||||
val sourcePort: String? = null,
|
||||
@@ -690,6 +516,32 @@ data class V2rayConfig(
|
||||
val attrs: String? = null,
|
||||
val domainMatcher: String? = null
|
||||
)
|
||||
|
||||
data class BalancerBean(
|
||||
val tag: String,
|
||||
val selector: List<String>,
|
||||
val fallbackTag: String? = null,
|
||||
val strategy: StrategyObject? = null
|
||||
)
|
||||
|
||||
data class StrategyObject(
|
||||
val type: String = "random", // "random" | "roundRobin" | "leastPing" | "leastLoad"
|
||||
val settings: StrategySettingsObject? = null
|
||||
)
|
||||
|
||||
data class StrategySettingsObject(
|
||||
val expected: Int? = null,
|
||||
val maxRTT: String? = null,
|
||||
val tolerance: Double? = null,
|
||||
val baselines: List<String>? = null,
|
||||
val costs: List<CostObject>? = null
|
||||
)
|
||||
|
||||
data class CostObject(
|
||||
val regexp: Boolean = false,
|
||||
val match: String,
|
||||
val value: Double
|
||||
)
|
||||
}
|
||||
|
||||
data class PolicyBean(
|
||||
@@ -707,6 +559,26 @@ data class V2rayConfig(
|
||||
)
|
||||
}
|
||||
|
||||
data class ObservatoryObject(
|
||||
val subjectSelector: List<String>,
|
||||
val probeUrl: String,
|
||||
val probeInterval: String,
|
||||
val enableConcurrency: Boolean = false
|
||||
)
|
||||
|
||||
data class BurstObservatoryObject(
|
||||
val subjectSelector: List<String>,
|
||||
val pingConfig: PingConfigObject
|
||||
) {
|
||||
data class PingConfigObject(
|
||||
val destination: String,
|
||||
val connectivity: String? = null,
|
||||
val interval: String,
|
||||
val sampling: Int,
|
||||
val timeout: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
data class FakednsBean(
|
||||
var ipPool: String = "198.18.0.0/15",
|
||||
var poolSize: Int = 10000
|
||||
@@ -723,15 +595,9 @@ data class V2rayConfig(
|
||||
return null
|
||||
}
|
||||
|
||||
fun toPrettyPrinting(): String {
|
||||
return GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.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)
|
||||
fun getAllProxyOutbound(): List<OutboundBean> {
|
||||
return outbounds.filter { outbound ->
|
||||
EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
/**
|
||||
* VPN interface address configuration enum class
|
||||
* Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration.
|
||||
* Each option provides client and router addresses to establish point-to-point VPN tunnels.
|
||||
*/
|
||||
enum class VpnInterfaceAddressConfig(
|
||||
val displayName: String,
|
||||
val ipv4Client: String,
|
||||
val ipv4Router: String,
|
||||
val ipv6Client: String,
|
||||
val ipv6Router: String
|
||||
) {
|
||||
OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"),
|
||||
OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"),
|
||||
OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"),
|
||||
OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"),
|
||||
OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"),
|
||||
OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"),
|
||||
OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2");
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Retrieves the VPN interface address configuration based on the specified index.
|
||||
*
|
||||
* @param index The configuration index (0-based) corresponding to user selection
|
||||
* @return The VpnInterfaceAddressConfig instance at the specified index,
|
||||
* or OPTION_1 (default) if the index is out of bounds
|
||||
*/
|
||||
fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig {
|
||||
return if (index in values().indices) {
|
||||
values()[index]
|
||||
} else {
|
||||
OPTION_1 // Default to the first configuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
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()
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.NetworkType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -18,15 +20,15 @@ open class FmtBase {
|
||||
*/
|
||||
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
|
||||
val query = if (dicQuery != null)
|
||||
("?" + dicQuery.toList().joinToString(
|
||||
"?" + dicQuery.toList().joinToString(
|
||||
separator = "&",
|
||||
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
|
||||
transform = { it.first + "=" + Utils.urlEncode(it.second) })
|
||||
else ""
|
||||
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
Utils.urlEncode(userInfo ?: ""),
|
||||
Utils.getIpv6Address(config.server),
|
||||
Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
|
||||
config.serverPort
|
||||
)
|
||||
|
||||
@@ -81,6 +83,7 @@ open class FmtBase {
|
||||
config.publicKey = queryParam["pbk"]
|
||||
config.shortId = queryParam["sid"]
|
||||
config.spiderX = queryParam["spx"]
|
||||
config.mldsa65Verify = queryParam["pqv"]
|
||||
config.flow = queryParam["flow"]
|
||||
}
|
||||
|
||||
@@ -99,6 +102,7 @@ open class FmtBase {
|
||||
config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() }
|
||||
config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() }
|
||||
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
|
||||
config.mldsa65Verify.let { if (it.isNotNullEmpty()) dicQuery["pqv"] = it.orEmpty() }
|
||||
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
|
||||
|
||||
val networkType = NetworkType.fromString(config.network)
|
||||
@@ -148,4 +152,21 @@ open class FmtBase {
|
||||
|
||||
return dicQuery
|
||||
}
|
||||
}
|
||||
|
||||
fun getServerAddress(profileItem: ProfileItem): String {
|
||||
if (Utils.isPureIpAddress(profileItem.server.orEmpty())) {
|
||||
return profileItem.server.orEmpty()
|
||||
}
|
||||
|
||||
val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty())
|
||||
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") {
|
||||
return domain
|
||||
}
|
||||
//Resolve and replace domain
|
||||
val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
||||
if (resolvedIps.isNullOrEmpty()) {
|
||||
return domain
|
||||
}
|
||||
return resolvedIps.first()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
|
||||
object HttpFmt : FmtBase() {
|
||||
/**
|
||||
@@ -13,10 +14,10 @@ object HttpFmt : FmtBase() {
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = profileItem.server.orEmpty()
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
if (profileItem.username.isNotNullEmpty()) {
|
||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -24,7 +25,7 @@ object Hysteria2Fmt : FmtBase() {
|
||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
@@ -144,7 +145,7 @@ object Hysteria2Fmt : FmtBase() {
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2)
|
||||
return outboundBean
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.NetworkType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -33,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
|
||||
if (uri.port <= 0) 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.serverPort = uri.port.toString()
|
||||
|
||||
@@ -82,7 +85,7 @@ object ShadowsocksFmt : FmtBase() {
|
||||
config.remarks =
|
||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
|
||||
}
|
||||
|
||||
result = result.substring(0, indexSplit)
|
||||
@@ -129,38 +132,22 @@ object ShadowsocksFmt : FmtBase() {
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = profileItem.server.orEmpty()
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.password = profileItem.password
|
||||
server.method = profileItem.method
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
||||
profileItem.network.orEmpty(),
|
||||
profileItem.headerType,
|
||||
profileItem.host,
|
||||
profileItem.path,
|
||||
profileItem.seed,
|
||||
profileItem.quicSecurity,
|
||||
profileItem.quicKey,
|
||||
profileItem.mode,
|
||||
profileItem.serviceName,
|
||||
profileItem.authority,
|
||||
)
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.populateTlsSettings(
|
||||
profileItem.security.orEmpty(),
|
||||
profileItem.insecure == true,
|
||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
||||
profileItem.fingerPrint,
|
||||
profileItem.alpn,
|
||||
profileItem.publicKey,
|
||||
profileItem.shortId,
|
||||
profileItem.spiderX,
|
||||
)
|
||||
outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -22,7 +23,7 @@ object SocksFmt : FmtBase() {
|
||||
if (uri.idnHost.isEmpty()) 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.serverPort = uri.port.toString()
|
||||
|
||||
@@ -60,10 +61,10 @@ object SocksFmt : FmtBase() {
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = profileItem.server.orEmpty()
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
if (profileItem.username.isNotNullEmpty()) {
|
||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -22,7 +23,7 @@ object TrojanFmt : FmtBase() {
|
||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
@@ -60,38 +61,22 @@ object TrojanFmt : FmtBase() {
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = profileItem.server.orEmpty()
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.password = profileItem.password
|
||||
server.flow = profileItem.flow
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
||||
profileItem.network.orEmpty(),
|
||||
profileItem.headerType,
|
||||
profileItem.host,
|
||||
profileItem.path,
|
||||
profileItem.seed,
|
||||
profileItem.quicSecurity,
|
||||
profileItem.quicKey,
|
||||
profileItem.mode,
|
||||
profileItem.serviceName,
|
||||
profileItem.authority,
|
||||
)
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.populateTlsSettings(
|
||||
profileItem.security.orEmpty(),
|
||||
profileItem.insecure == true,
|
||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
||||
profileItem.fingerPrint,
|
||||
profileItem.alpn,
|
||||
profileItem.publicKey,
|
||||
profileItem.shortId,
|
||||
profileItem.spiderX,
|
||||
)
|
||||
outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
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 java.net.URI
|
||||
|
||||
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
@@ -57,41 +57,23 @@ object VlessFmt : FmtBase() {
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
|
||||
|
||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||
vnext.address = profileItem.server.orEmpty()
|
||||
vnext.address = getServerAddress(profileItem)
|
||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||
vnext.users[0].id = profileItem.password.orEmpty()
|
||||
vnext.users[0].encryption = profileItem.method
|
||||
vnext.users[0].flow = profileItem.flow
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
||||
profileItem.network.orEmpty(),
|
||||
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)
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.populateTlsSettings(
|
||||
profileItem.security.orEmpty(),
|
||||
profileItem.insecure == true,
|
||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
||||
profileItem.fingerPrint,
|
||||
profileItem.alpn,
|
||||
profileItem.publicKey,
|
||||
profileItem.shortId,
|
||||
profileItem.spiderX,
|
||||
)
|
||||
outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.v2ray.ang.dto.VmessQRCode
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
@@ -33,7 +34,7 @@ object VmessFmt : FmtBase() {
|
||||
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
||||
result = Utils.decode(result)
|
||||
if (TextUtils.isEmpty(result)) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
|
||||
Log.w(AppConfig.TAG, "Toast decoding failed")
|
||||
return null
|
||||
}
|
||||
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
|
||||
@@ -43,7 +44,7 @@ object VmessFmt : FmtBase() {
|
||||
|| TextUtils.isEmpty(vmessQRCode.id)
|
||||
|| TextUtils.isEmpty(vmessQRCode.net)
|
||||
) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
|
||||
Log.w(AppConfig.TAG, "Toast incorrect protocol")
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ object VmessFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
@@ -168,38 +169,22 @@ object VmessFmt : FmtBase() {
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
|
||||
|
||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||
vnext.address = profileItem.server.orEmpty()
|
||||
vnext.address = getServerAddress(profileItem)
|
||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||
vnext.users[0].id = profileItem.password.orEmpty()
|
||||
vnext.users[0].security = profileItem.method
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
||||
profileItem.network.orEmpty(),
|
||||
profileItem.headerType,
|
||||
profileItem.host,
|
||||
profileItem.path,
|
||||
profileItem.seed,
|
||||
profileItem.quicSecurity,
|
||||
profileItem.quicKey,
|
||||
profileItem.mode,
|
||||
profileItem.serviceName,
|
||||
profileItem.authority,
|
||||
)
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.populateTlsSettings(
|
||||
profileItem.security.orEmpty(),
|
||||
profileItem.insecure == true,
|
||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
||||
profileItem.fingerPrint,
|
||||
profileItem.alpn,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.removeWhiteSpace
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -24,16 +25,16 @@ object WireguardFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
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.serverPort = uri.port.toString()
|
||||
|
||||
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.preSharedKey = queryParam["presharedkey"].orEmpty()
|
||||
config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() }
|
||||
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
|
||||
}
|
||||
@@ -83,7 +84,7 @@ object WireguardFmt : FmtBase() {
|
||||
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
config.publicKey = peerParams["publickey"].orEmpty()
|
||||
config.preSharedKey = peerParams["presharedkey"].orEmpty()
|
||||
config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() }
|
||||
val endpoint = peerParams["endpoint"].orEmpty()
|
||||
val endpointParts = endpoint.split(":", limit = 2)
|
||||
if (endpointParts.size == 2) {
|
||||
@@ -105,18 +106,18 @@ object WireguardFmt : FmtBase() {
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
|
||||
|
||||
outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = profileItem.secretKey
|
||||
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||
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}"
|
||||
}
|
||||
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
|
||||
|
||||
@@ -7,7 +7,9 @@ import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.HY2
|
||||
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.Hysteria2Fmt
|
||||
import com.v2ray.ang.fmt.ShadowsocksFmt
|
||||
@@ -42,7 +44,7 @@ object AngConfigManager {
|
||||
Utils.setClipboard(context, conf)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to share config to clipboard", e)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
@@ -69,9 +71,9 @@ object AngConfigManager {
|
||||
if (sb.count() > 0) {
|
||||
Utils.setClipboard(context, sb.toString())
|
||||
}
|
||||
return sb.lines().count()
|
||||
return sb.lines().count() - 1
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
|
||||
return -1
|
||||
}
|
||||
}
|
||||
@@ -91,7 +93,7 @@ object AngConfigManager {
|
||||
return QRCodeDecoder.createQRCode(conf)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to share config as QR code", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -120,7 +122,7 @@ object AngConfigManager {
|
||||
return -1
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
@@ -148,7 +150,7 @@ object AngConfigManager {
|
||||
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -203,7 +205,7 @@ object AngConfigManager {
|
||||
}
|
||||
return count
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to parse batch subscription", e)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -251,7 +253,7 @@ object AngConfigManager {
|
||||
}
|
||||
return count
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -287,7 +289,7 @@ object AngConfigManager {
|
||||
return count
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -298,7 +300,7 @@ object AngConfigManager {
|
||||
MmkvManager.encodeServerRaw(key, server)
|
||||
return 1
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
||||
}
|
||||
return 0
|
||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
||||
@@ -308,7 +310,7 @@ object AngConfigManager {
|
||||
MmkvManager.encodeServerRaw(key, server)
|
||||
return 1
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
||||
}
|
||||
return 0
|
||||
} else {
|
||||
@@ -372,7 +374,7 @@ object AngConfigManager {
|
||||
MmkvManager.setSelectServer(guid)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to parse config", e)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
@@ -390,7 +392,7 @@ object AngConfigManager {
|
||||
count += updateConfigViaSub(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
|
||||
return 0
|
||||
}
|
||||
return count
|
||||
@@ -413,25 +415,29 @@ object AngConfigManager {
|
||||
if (!it.second.enabled) {
|
||||
return 0
|
||||
}
|
||||
val url = HttpUtil.idnToASCII(it.second.url)
|
||||
val url = HttpUtil.toIdnUrl(it.second.url)
|
||||
if (!Utils.isValidUrl(url)) {
|
||||
return 0
|
||||
}
|
||||
Log.d(AppConfig.ANG_PACKAGE, url)
|
||||
if (!it.second.allowInsecureUrl) {
|
||||
if (!Utils.isValidSubUrl(url)) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Log.i(AppConfig.TAG, url)
|
||||
|
||||
var configText = try {
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
HttpUtil.getUrlContentWithUserAgent(url, 15000, httpPort)
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……")
|
||||
//e.printStackTrace()
|
||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
||||
""
|
||||
}
|
||||
if (configText.isEmpty()) {
|
||||
configText = try {
|
||||
HttpUtil.getUrlContentWithUserAgent(url)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||
""
|
||||
}
|
||||
}
|
||||
@@ -440,7 +446,7 @@ object AngConfigManager {
|
||||
}
|
||||
return parseConfigViaSub(configText, it.first, false)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -484,4 +490,31 @@ object AngConfigManager {
|
||||
MmkvManager.encodeSubscription("", subItem)
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an intelligent selection configuration based on multiple server configurations.
|
||||
*
|
||||
* @param context The application context used for configuration generation.
|
||||
* @param guidList The list of server GUIDs to be included in the intelligent selection.
|
||||
* Each GUID represents a server configuration that will be combined.
|
||||
* @param subid The subscription ID to associate with the generated configuration.
|
||||
* This helps organize the configuration under a specific subscription.
|
||||
* @return The GUID key of the newly created intelligent selection configuration,
|
||||
* or null if the operation fails (e.g., empty guidList or configuration parsing error).
|
||||
*/
|
||||
fun createIntelligentSelection(
|
||||
context: Context,
|
||||
guidList: List<String>,
|
||||
subid: String
|
||||
): String? {
|
||||
if (guidList.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val result = V2rayConfigManager.genV2rayConfig(context, guidList) ?: return null
|
||||
val config = CustomFmt.parse(JsonUtil.toJson(result)) ?: return null
|
||||
config.subscriptionId = subid
|
||||
val key = MmkvManager.encodeServerConfig("", config)
|
||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(result) ?: "")
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.handler
|
||||
import android.util.Log
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.NetworkType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
@@ -26,7 +25,7 @@ object MigrateManager {
|
||||
return false
|
||||
}
|
||||
val serverList = serverStorage.allKeys() ?: return false
|
||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
|
||||
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + serverList.count())
|
||||
|
||||
for (guid in serverList) {
|
||||
var configOld = decodeServerConfigOld(guid) ?: continue
|
||||
@@ -43,9 +42,9 @@ object MigrateManager {
|
||||
//check and remove old
|
||||
decodeServerConfig(guid) ?: continue
|
||||
serverStorage.remove(guid)
|
||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
|
||||
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + config.remarks)
|
||||
}
|
||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
|
||||
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-end")
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -571,7 +571,7 @@ object MmkvManager {
|
||||
* @param startOnBoot Whether to start on boot.
|
||||
*/
|
||||
fun encodeStartOnBoot(startOnBoot: Boolean) {
|
||||
MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot)
|
||||
encodeSettings(PREF_IS_BOOTED, startOnBoot)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.service
|
||||
package com.v2ray.ang.handler
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -12,12 +12,10 @@ import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.extension.toSpeedString
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
import com.v2ray.ang.ui.MainActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,7 +25,7 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.min
|
||||
|
||||
object NotificationService {
|
||||
object NotificationManager {
|
||||
private const val NOTIFICATION_ID = 1
|
||||
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
||||
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
||||
@@ -50,7 +48,7 @@ object NotificationService {
|
||||
lastQueryTime = System.currentTimeMillis()
|
||||
var lastZeroSpeed = false
|
||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||
outboundTags?.remove(TAG_DIRECT)
|
||||
outboundTags?.remove(AppConfig.TAG_DIRECT)
|
||||
|
||||
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
while (isActive) {
|
||||
@@ -66,15 +64,15 @@ object NotificationService {
|
||||
proxyTotal += up + down
|
||||
}
|
||||
}
|
||||
val directUplink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.UPLINK)
|
||||
val directDownlink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
|
||||
val directUplink = V2RayServiceManager.queryStats(AppConfig.TAG_DIRECT, AppConfig.UPLINK)
|
||||
val directDownlink = V2RayServiceManager.queryStats(AppConfig.TAG_DIRECT, AppConfig.DOWNLINK)
|
||||
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
||||
if (!zeroSpeed || !lastZeroSpeed) {
|
||||
if (proxyTotal == 0L) {
|
||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
||||
}
|
||||
appendSpeedString(
|
||||
text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
||||
text, AppConfig.TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
||||
directDownlink / sinceLastQueryInSeconds
|
||||
)
|
||||
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
||||
@@ -102,12 +100,12 @@ object NotificationService {
|
||||
val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
|
||||
|
||||
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||
stopV2RayIntent.`package` = ANG_PACKAGE
|
||||
stopV2RayIntent.`package` = AppConfig.ANG_PACKAGE
|
||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
|
||||
|
||||
val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||
restartV2RayIntent.`package` = ANG_PACKAGE
|
||||
restartV2RayIntent.`package` = AppConfig.ANG_PACKAGE
|
||||
restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
|
||||
val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
|
||||
|
||||
@@ -158,6 +156,7 @@ object NotificationService {
|
||||
mBuilder = null
|
||||
speedNotificationJob?.cancel()
|
||||
speedNotificationJob = null
|
||||
mNotificationManager = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,19 +1,20 @@
|
||||
package com.v2ray.ang.util
|
||||
package com.v2ray.ang.handler
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||
import com.v2ray.ang.handler.SpeedtestManager
|
||||
import com.v2ray.ang.service.ProcessService
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.io.File
|
||||
|
||||
object PluginUtil {
|
||||
object PluginServiceManager {
|
||||
private const val HYSTERIA2 = "libhysteria2.so"
|
||||
private const val TAG = ANG_PACKAGE
|
||||
|
||||
private val procService: ProcessService by lazy {
|
||||
ProcessService()
|
||||
}
|
||||
@@ -23,16 +24,30 @@ object PluginUtil {
|
||||
*
|
||||
* @param context The context to use.
|
||||
* @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?) {
|
||||
Log.d(TAG, "runPlugin")
|
||||
fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
|
||||
Log.i(AppConfig.TAG, "Starting plugin execution")
|
||||
|
||||
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
||||
val configFile = genConfigHy2(context, config, domainPort) ?: return
|
||||
val cmd = genCmdHy2(context, configFile)
|
||||
if (config == null) {
|
||||
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
|
||||
return
|
||||
}
|
||||
|
||||
procService.runProcess(context, cmd)
|
||||
try {
|
||||
if (config.configType == EConfigType.HYSTERIA2) {
|
||||
if (socksPort == null) {
|
||||
Log.w(AppConfig.TAG, "Cannot run plugin: socksPort is null")
|
||||
return
|
||||
}
|
||||
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
|
||||
val configFile = genConfigHy2(context, config, socksPort) ?: return
|
||||
val cmd = genCmdHy2(context, configFile)
|
||||
|
||||
procService.runProcess(context, cmd)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Error running plugin", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +66,12 @@ object PluginUtil {
|
||||
* @return The ping delay in milliseconds, or -1 if it fails.
|
||||
*/
|
||||
fun realPingHy2(context: Context, config: ProfileItem?): Long {
|
||||
Log.d(TAG, "realPingHy2")
|
||||
Log.i(AppConfig.TAG, "realPingHy2")
|
||||
val retFailure = -1L
|
||||
|
||||
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
||||
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 proc = ProcessService()
|
||||
@@ -75,22 +90,20 @@ object PluginUtil {
|
||||
*
|
||||
* @param context The context to use.
|
||||
* @param config The profile configuration.
|
||||
* @param domainPort The domain and port information.
|
||||
* @param socksPort The port information.
|
||||
* @return The generated configuration file.
|
||||
*/
|
||||
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
|
||||
Log.d(TAG, "runPlugin $HYSTERIA2")
|
||||
private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? {
|
||||
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 configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
|
||||
Log.d(TAG, "runPlugin ${configFile.absolutePath}")
|
||||
Log.i(AppConfig.TAG, "runPlugin ${configFile.absolutePath}")
|
||||
|
||||
configFile.parentFile?.mkdirs()
|
||||
configFile.writeText(JsonUtil.toJson(hy2Config))
|
||||
Log.d(TAG, JsonUtil.toJson(hy2Config))
|
||||
Log.i(AppConfig.TAG, JsonUtil.toJson(hy2Config))
|
||||
|
||||
return configFile
|
||||
}
|
||||
@@ -119,10 +132,10 @@ object PluginUtil {
|
||||
*/
|
||||
private fun stopHy2() {
|
||||
try {
|
||||
Log.d(TAG, "$HYSTERIA2 destroy")
|
||||
Log.i(AppConfig.TAG, "$HYSTERIA2 destroy")
|
||||
procService?.stopProcess()
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, e.toString())
|
||||
Log.e(AppConfig.TAG, "Failed to stop Hysteria2 process", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.RoutingType
|
||||
import com.v2ray.ang.dto.RulesetItem
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.VpnInterfaceAddressConfig
|
||||
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
||||
import com.v2ray.ang.handler.MmkvManager.decodeServerList
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
@@ -84,7 +85,7 @@ object SettingsManager {
|
||||
resetRoutingRulesetsCommon(rulesetList)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -159,7 +160,7 @@ object SettingsManager {
|
||||
* @return True if bypassing LAN, false otherwise.
|
||||
*/
|
||||
fun routingRulesetsBypassLan(): Boolean {
|
||||
val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "0"
|
||||
val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "1"
|
||||
if (vpnBypassLan == "1") {
|
||||
return true
|
||||
} else if (vpnBypassLan == "2") {
|
||||
@@ -167,11 +168,11 @@ object SettingsManager {
|
||||
}
|
||||
|
||||
val guid = MmkvManager.getSelectServer() ?: return false
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return false
|
||||
val config = decodeServerConfig(guid) ?: return false
|
||||
if (config.configType == EConfigType.CUSTOM) {
|
||||
val raw = MmkvManager.decodeServerRaw(guid) ?: return false
|
||||
val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java)
|
||||
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }?.any {
|
||||
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }.any {
|
||||
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
|
||||
}
|
||||
return exist == true
|
||||
@@ -216,7 +217,7 @@ object SettingsManager {
|
||||
* @return The ProfileItem.
|
||||
*/
|
||||
fun getServerViaRemarks(remarks: String?): ProfileItem? {
|
||||
if (remarks == null) {
|
||||
if (remarks.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
val serverList = decodeServerList()
|
||||
@@ -242,7 +243,7 @@ object SettingsManager {
|
||||
* @return The HTTP port.
|
||||
*/
|
||||
fun getHttpPort(): Int {
|
||||
return getSocksPort() + (if (Utils.isXray()) 0 else 1)
|
||||
return getSocksPort() + if (Utils.isXray()) 0 else 1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,10 +266,7 @@ object SettingsManager {
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
Log.i(
|
||||
ANG_PACKAGE,
|
||||
"Copied from apk assets folder to ${target.absolutePath}"
|
||||
)
|
||||
Log.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
||||
@@ -319,10 +317,10 @@ object SettingsManager {
|
||||
*/
|
||||
fun getDelayTestUrl(second: Boolean = false): String {
|
||||
return if (second) {
|
||||
AppConfig.DelayTestUrl2
|
||||
AppConfig.DELAY_TEST_URL2
|
||||
} else {
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
||||
?: AppConfig.DelayTestUrl
|
||||
?: AppConfig.DELAY_TEST_URL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +341,7 @@ object SettingsManager {
|
||||
Language.VIETNAMESE -> Locale("vi")
|
||||
Language.RUSSIAN -> Locale("ru")
|
||||
Language.PERSIAN -> Locale("fa")
|
||||
Language.ARABIC -> Locale("ar")
|
||||
Language.BANGLA -> Locale("bn")
|
||||
Language.BAKHTIARI -> Locale("bqi", "IR")
|
||||
}
|
||||
@@ -358,4 +357,17 @@ object SettingsManager {
|
||||
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currently selected VPN interface address configuration.
|
||||
* This method reads the user's preference for VPN interface addressing and returns
|
||||
* the corresponding configuration containing IPv4 and IPv6 addresses.
|
||||
*
|
||||
* @return The selected VpnInterfaceAddressConfig instance, or the default configuration
|
||||
* if no valid selection is found or if the stored index is invalid.
|
||||
*/
|
||||
fun getCurrentVpnInterfaceAddressConfig(): VpnInterfaceAddressConfig {
|
||||
val selectedIndex = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, "0")?.toInt()
|
||||
return VpnInterfaceAddressConfig.getConfigByIndex(selectedIndex ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.IPAPIInfo
|
||||
import com.v2ray.ang.extension.responseLength
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import kotlinx.coroutines.isActive
|
||||
import libv2ray.Libv2ray
|
||||
import java.io.IOException
|
||||
@@ -51,7 +53,7 @@ object SpeedtestManager {
|
||||
return try {
|
||||
Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl())
|
||||
} catch (e: Exception) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
|
||||
Log.e(AppConfig.TAG, "Failed to measure outbound delay", e)
|
||||
-1L
|
||||
}
|
||||
}
|
||||
@@ -76,7 +78,7 @@ object SpeedtestManager {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to ping URL: $url", e)
|
||||
}
|
||||
return "-1ms"
|
||||
}
|
||||
@@ -103,11 +105,11 @@ object SpeedtestManager {
|
||||
socket.close()
|
||||
return time
|
||||
} catch (e: UnknownHostException) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Unknown host: $url", e)
|
||||
} catch (e: IOException) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "socketConnectTime IOException: $e")
|
||||
Log.e(AppConfig.TAG, "socketConnectTime IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -152,18 +154,29 @@ object SpeedtestManager {
|
||||
)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
|
||||
Log.e(AppConfig.TAG, "Connection test IOException", e)
|
||||
result = context.getString(R.string.connection_test_error, e.message)
|
||||
} catch (e: Exception) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e))
|
||||
Log.e(AppConfig.TAG, "Connection test Exception", e)
|
||||
result = context.getString(R.string.connection_test_error, e.message)
|
||||
} finally {
|
||||
conn?.disconnect()
|
||||
conn.disconnect()
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.service
|
||||
package com.v2ray.ang.handler
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
@@ -11,11 +11,7 @@ import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
|
||||
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.handler.AngConfigManager.updateConfigViaSub
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
|
||||
object SubscriptionUpdater {
|
||||
|
||||
@@ -24,7 +20,7 @@ object SubscriptionUpdater {
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||
private val notification =
|
||||
NotificationCompat.Builder(applicationContext, SUBSCRIPTION_UPDATE_CHANNEL)
|
||||
NotificationCompat.Builder(applicationContext, AppConfig.SUBSCRIPTION_UPDATE_CHANNEL)
|
||||
.setWhen(0)
|
||||
.setTicker("Update")
|
||||
.setContentTitle(context.getString(R.string.title_pref_auto_update_subscription))
|
||||
@@ -38,7 +34,7 @@ object SubscriptionUpdater {
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
override suspend fun doWork(): Result {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "subscription automatic update starting")
|
||||
Log.i(AppConfig.TAG, "subscription automatic update starting")
|
||||
|
||||
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
|
||||
|
||||
@@ -46,21 +42,18 @@ object SubscriptionUpdater {
|
||||
val subItem = sub.second
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL)
|
||||
notification.setChannelId(AppConfig.SUBSCRIPTION_UPDATE_CHANNEL)
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
SUBSCRIPTION_UPDATE_CHANNEL,
|
||||
SUBSCRIPTION_UPDATE_CHANNEL_NAME,
|
||||
AppConfig.SUBSCRIPTION_UPDATE_CHANNEL,
|
||||
AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_MIN
|
||||
)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
notificationManager.notify(3, notification.build())
|
||||
Log.d(
|
||||
AppConfig.ANG_PACKAGE,
|
||||
"subscription automatic update: ---${subItem.remarks}"
|
||||
)
|
||||
updateConfigViaSub(Pair(sub.first, subItem))
|
||||
Log.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
|
||||
AngConfigManager.updateConfigViaSub(Pair(sub.first, subItem))
|
||||
notification.setContentText("Updating ${subItem.remarks}")
|
||||
}
|
||||
notificationManager.cancel(3)
|
||||
@@ -0,0 +1,107 @@
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.service
|
||||
package com.v2ray.ang.handler
|
||||
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
@@ -9,29 +9,28 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.service.ServiceControl
|
||||
import com.v2ray.ang.service.V2RayProxyOnlyService
|
||||
import com.v2ray.ang.service.V2RayVpnService
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.PluginUtil
|
||||
import com.v2ray.ang.handler.PluginServiceManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import go.Seq
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import libv2ray.CoreCallbackHandler
|
||||
import libv2ray.CoreController
|
||||
import libv2ray.Libv2ray
|
||||
import libv2ray.V2RayPoint
|
||||
import libv2ray.V2RayVPNServiceSupportsSet
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
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 var currentConfig: ProfileItem? = null
|
||||
|
||||
@@ -39,7 +38,7 @@ object V2RayServiceManager {
|
||||
set(value) {
|
||||
field = value
|
||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
||||
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
||||
Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +80,7 @@ object V2RayServiceManager {
|
||||
* Checks if the V2Ray service is running.
|
||||
* @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.
|
||||
@@ -91,15 +90,18 @@ object V2RayServiceManager {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private fun startContextService(context: Context) {
|
||||
if (v2rayPoint.isRunning) return
|
||||
if (coreController.isRunning) {
|
||||
return
|
||||
}
|
||||
val guid = MmkvManager.getSelectServer() ?: return
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||
if (config.configType != EConfigType.CUSTOM
|
||||
&& !Utils.isValidUrl(config.server)
|
||||
&& !Utils.isIpAddress(config.server)
|
||||
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
||||
) return
|
||||
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
||||
// if (!result.status) return
|
||||
@@ -124,18 +126,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):
|
||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||
* Starts the V2Ray point.
|
||||
* Starts the V2Ray core service.
|
||||
*/
|
||||
fun startV2rayPoint() {
|
||||
val service = getService() ?: return
|
||||
val guid = MmkvManager.getSelectServer() ?: return
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||
if (v2rayPoint.isRunning) {
|
||||
return
|
||||
fun startCoreLoop(): Boolean {
|
||||
if (coreController.isRunning) {
|
||||
return false
|
||||
}
|
||||
|
||||
val service = getService() ?: return false
|
||||
val guid = MmkvManager.getSelectServer() ?: return false
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return false
|
||||
val result = V2rayConfigManager.getV2rayConfig(service, guid)
|
||||
if (!result.status)
|
||||
return
|
||||
return false
|
||||
|
||||
try {
|
||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||
@@ -144,55 +147,67 @@ object V2RayServiceManager {
|
||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
|
||||
return false
|
||||
}
|
||||
|
||||
v2rayPoint.configureFileContent = result.content
|
||||
v2rayPoint.domainName = result.domainPort
|
||||
currentConfig = config
|
||||
|
||||
try {
|
||||
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
||||
coreController.startLoop(result.content)
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
Log.e(AppConfig.TAG, "Failed to start Core loop", e)
|
||||
return false
|
||||
}
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||
NotificationService.showNotification(currentConfig)
|
||||
|
||||
PluginUtil.runPlugin(service, config, result.domainPort)
|
||||
} else {
|
||||
if (coreController.isRunning == false) {
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
||||
NotificationService.cancelNotification()
|
||||
NotificationManager.cancelNotification()
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||
NotificationManager.showNotification(currentConfig)
|
||||
NotificationManager.startSpeedNotification(currentConfig)
|
||||
|
||||
PluginServiceManager.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() {
|
||||
val service = getService() ?: return
|
||||
fun stopCoreLoop(): Boolean {
|
||||
val service = getService() ?: return false
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
if (coreController.isRunning) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
v2rayPoint.stopLoop()
|
||||
coreController.stopLoop()
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||
NotificationService.cancelNotification()
|
||||
NotificationManager.cancelNotification()
|
||||
|
||||
try {
|
||||
service.unregisterReceiver(mMsgReceive)
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
|
||||
}
|
||||
PluginUtil.stopPlugin()
|
||||
PluginServiceManager.stopPlugin()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,40 +217,52 @@ object V2RayServiceManager {
|
||||
* @return The statistics value.
|
||||
*/
|
||||
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() {
|
||||
if (coreController.isRunning == false) {
|
||||
return
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val service = getService() ?: return@launch
|
||||
var time = -1L
|
||||
var errstr = ""
|
||||
if (v2rayPoint.isRunning) {
|
||||
try {
|
||||
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl())
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
||||
}
|
||||
if (time == -1L) {
|
||||
try {
|
||||
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl(true))
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
||||
}
|
||||
}
|
||||
var errorStr = ""
|
||||
|
||||
try {
|
||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
|
||||
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)
|
||||
if (time == -1L) {
|
||||
try {
|
||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
|
||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,59 +274,53 @@ object V2RayServiceManager {
|
||||
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 {
|
||||
val serviceControl = serviceControl?.get() ?: return -1
|
||||
// called by go
|
||||
return try {
|
||||
serviceControl.stopService()
|
||||
0
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
* @param l The status code.
|
||||
* @param s The status message.
|
||||
* @return The status code.
|
||||
* Called when V2Ray core emits status information.
|
||||
* @param l Status code.
|
||||
* @param s Status message.
|
||||
* @return Always returns 0.
|
||||
*/
|
||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by Go to set up the service.
|
||||
* @param s The setup string.
|
||||
* @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.d(ANG_PACKAGE, e.toString())
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast receiver for handling messages sent to the service.
|
||||
* Handles registration, service control, and screen events.
|
||||
*/
|
||||
private class ReceiveMessageHandler : BroadcastReceiver() {
|
||||
/**
|
||||
* Handles received broadcast messages.
|
||||
* Processes service control messages and screen state changes.
|
||||
* @param ctx The context in which the receiver is running.
|
||||
* @param intent The intent being received.
|
||||
*/
|
||||
@@ -307,7 +328,7 @@ object V2RayServiceManager {
|
||||
val serviceControl = serviceControl?.get() ?: return
|
||||
when (intent?.getIntExtra("key", 0)) {
|
||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
||||
if (v2rayPoint.isRunning) {
|
||||
if (coreController.isRunning) {
|
||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
||||
} else {
|
||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||
@@ -323,12 +344,12 @@ object V2RayServiceManager {
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_STOP -> {
|
||||
Log.d(ANG_PACKAGE, "Stop Service")
|
||||
Log.i(AppConfig.TAG, "Stop Service")
|
||||
serviceControl.stopService()
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_RESTART -> {
|
||||
Log.d(ANG_PACKAGE, "Restart Service")
|
||||
Log.i(AppConfig.TAG, "Restart Service")
|
||||
serviceControl.stopService()
|
||||
Thread.sleep(500L)
|
||||
startVService(serviceControl.getService())
|
||||
@@ -341,15 +362,15 @@ object V2RayServiceManager {
|
||||
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
||||
NotificationService.stopSpeedNotification(currentConfig)
|
||||
Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats")
|
||||
NotificationManager.stopSpeedNotification(currentConfig)
|
||||
}
|
||||
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
||||
NotificationService.startSpeedNotification(currentConfig)
|
||||
Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats")
|
||||
NotificationManager.startSpeedNotification(currentConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -107,7 +107,7 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
|
||||
addUpdateListener { animation ->
|
||||
val value = animation.animatedValue as Float
|
||||
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()
|
||||
duration = ANIMATION_DURATION
|
||||
@@ -144,4 +144,4 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
|
||||
private const val SWIPE_THRESHOLD = 0.25f
|
||||
private const val ANIMATION_DURATION: Long = 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.TextUtils
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
|
||||
class TaskerReceiver : BroadcastReceiver() {
|
||||
|
||||
@@ -35,7 +35,7 @@ class TaskerReceiver : BroadcastReceiver() {
|
||||
V2RayServiceManager.stopVService(context)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
|
||||
class WidgetProvider : AppWidgetProvider() {
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.v2ray.ang.service
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -16,7 +16,7 @@ class ProcessService {
|
||||
* @param cmd The command to run.
|
||||
*/
|
||||
fun runProcess(context: Context, cmd: MutableList<String>) {
|
||||
Log.d(ANG_PACKAGE, cmd.toString())
|
||||
Log.i(AppConfig.TAG, cmd.toString())
|
||||
|
||||
try {
|
||||
val proBuilder = ProcessBuilder(cmd)
|
||||
@@ -27,14 +27,14 @@ class ProcessService {
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
Thread.sleep(50L)
|
||||
Log.d(ANG_PACKAGE, "runProcess check")
|
||||
Log.i(AppConfig.TAG, "runProcess check")
|
||||
process?.waitFor()
|
||||
Log.d(ANG_PACKAGE, "runProcess exited")
|
||||
Log.i(AppConfig.TAG, "runProcess exited")
|
||||
}
|
||||
Log.d(ANG_PACKAGE, process.toString())
|
||||
Log.i(AppConfig.TAG, process.toString())
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
Log.e(AppConfig.TAG, e.toString(), e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ class ProcessService {
|
||||
*/
|
||||
fun stopProcess() {
|
||||
try {
|
||||
Log.d(ANG_PACKAGE, "runProcess destroy")
|
||||
Log.i(AppConfig.TAG, "runProcess destroy")
|
||||
process?.destroy()
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
Log.e(AppConfig.TAG, "Failed to destroy process", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.lang.ref.SoftReference
|
||||
@@ -24,14 +26,13 @@ class QSTileService : TileService() {
|
||||
* @param state The state to set.
|
||||
*/
|
||||
fun setState(state: Int) {
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||
if (state == Tile.STATE_INACTIVE) {
|
||||
qsTile?.state = Tile.STATE_INACTIVE
|
||||
qsTile?.label = getString(R.string.app_name)
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||
} else if (state == Tile.STATE_ACTIVE) {
|
||||
qsTile?.state = Tile.STATE_ACTIVE
|
||||
qsTile?.label = V2RayServiceManager.getRunningServerName()
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||
}
|
||||
|
||||
qsTile?.updateTile()
|
||||
@@ -44,7 +45,11 @@ class QSTileService : TileService() {
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
|
||||
setState(Tile.STATE_INACTIVE)
|
||||
if (V2RayServiceManager.isRunning()) {
|
||||
setState(Tile.STATE_ACTIVE)
|
||||
} else {
|
||||
setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
mMsgReceive = ReceiveMessageHandler(this)
|
||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||
ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||
@@ -61,7 +66,7 @@ class QSTileService : TileService() {
|
||||
applicationContext.unregisterReceiver(mMsgReceive)
|
||||
mMsgReceive = null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to unregister receiver", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.content.Context
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.VPN_MTU
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Manages the tun2socks process that handles VPN traffic
|
||||
*/
|
||||
class Tun2SocksService(
|
||||
private val context: Context,
|
||||
private val vpnInterface: ParcelFileDescriptor,
|
||||
private val isRunningProvider: () -> Boolean,
|
||||
private val restartCallback: () -> Unit
|
||||
) {
|
||||
companion object {
|
||||
private const val TUN2SOCKS = "libtun2socks.so"
|
||||
}
|
||||
|
||||
private lateinit var process: Process
|
||||
|
||||
/**
|
||||
* Starts the tun2socks process with the appropriate parameters.
|
||||
*/
|
||||
fun startTun2Socks() {
|
||||
Log.i(AppConfig.TAG, "Start run $TUN2SOCKS")
|
||||
val socksPort = SettingsManager.getSocksPort()
|
||||
val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
|
||||
val cmd = arrayListOf(
|
||||
File(context.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", vpnConfig.ipv4Router,
|
||||
"--netif-netmask", "255.255.255.252",
|
||||
"--socks-server-addr", "${AppConfig.LOOPBACK}:${socksPort}",
|
||||
"--tunmtu", VPN_MTU.toString(),
|
||||
"--sock-path", "sock_path",
|
||||
"--enable-udprelay",
|
||||
"--loglevel", "notice"
|
||||
)
|
||||
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
|
||||
cmd.add("--netif-ip6addr")
|
||||
cmd.add(vpnConfig.ipv6Router)
|
||||
}
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
|
||||
val localDnsPort = Utils.parseInt(
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT),
|
||||
AppConfig.PORT_LOCAL_DNS.toInt()
|
||||
)
|
||||
cmd.add("--dnsgw")
|
||||
cmd.add("${AppConfig.LOOPBACK}:${localDnsPort}")
|
||||
}
|
||||
Log.i(AppConfig.TAG, cmd.toString())
|
||||
|
||||
try {
|
||||
val proBuilder = ProcessBuilder(cmd)
|
||||
proBuilder.redirectErrorStream(true)
|
||||
process = proBuilder
|
||||
.directory(context.filesDir)
|
||||
.start()
|
||||
Thread {
|
||||
Log.i(AppConfig.TAG, "$TUN2SOCKS check")
|
||||
process.waitFor()
|
||||
Log.i(AppConfig.TAG, "$TUN2SOCKS exited")
|
||||
if (isRunningProvider()) {
|
||||
Log.i(AppConfig.TAG, "$TUN2SOCKS restart")
|
||||
restartCallback()
|
||||
}
|
||||
}.start()
|
||||
Log.i(AppConfig.TAG, "$TUN2SOCKS process info: $process")
|
||||
|
||||
sendFd()
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to start $TUN2SOCKS process", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the file descriptor to the tun2socks process.
|
||||
* Attempts to send the file descriptor multiple times if necessary.
|
||||
*/
|
||||
private fun sendFd() {
|
||||
val fd = vpnInterface.fileDescriptor
|
||||
val path = File(context.filesDir, "sock_path").absolutePath
|
||||
Log.i(AppConfig.TAG, "LocalSocket path: $path")
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var tries = 0
|
||||
while (true) try {
|
||||
Thread.sleep(50L shl tries)
|
||||
Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries")
|
||||
LocalSocket().use { localSocket ->
|
||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||
localSocket.outputStream.write(42)
|
||||
}
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to send file descriptor, try: $tries", e)
|
||||
if (tries > 5) break
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the tun2socks process
|
||||
*/
|
||||
fun stopTun2Socks() {
|
||||
try {
|
||||
Log.i(AppConfig.TAG, "$TUN2SOCKS destroy")
|
||||
if (::process.isInitialized) {
|
||||
process.destroy()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to destroy $TUN2SOCKS process", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
@@ -27,7 +28,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
* @return The start mode.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
V2RayServiceManager.startV2rayPoint()
|
||||
V2RayServiceManager.startCoreLoop()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
V2RayServiceManager.stopV2rayPoint()
|
||||
V2RayServiceManager.stopCoreLoop()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,10 +9,10 @@ import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.extension.serializable
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.PluginServiceManager
|
||||
import com.v2ray.ang.handler.SpeedtestManager
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.PluginUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import go.Seq
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -32,7 +32,7 @@ class V2RayTestService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Seq.setContext(this)
|
||||
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
||||
Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,14 +78,14 @@ class V2RayTestService : Service() {
|
||||
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure
|
||||
if (config.configType == EConfigType.HYSTERIA2) {
|
||||
val delay = PluginUtil.realPingHy2(this, config)
|
||||
val delay = PluginServiceManager.realPingHy2(this, config)
|
||||
return delay
|
||||
} else {
|
||||
val config = V2rayConfigManager.getV2rayConfig(this, guid)
|
||||
if (!config.status) {
|
||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
|
||||
if (!configResult.status) {
|
||||
return retFailure
|
||||
}
|
||||
return SpeedtestManager.realPing(config.content)
|
||||
return SpeedtestManager.realPing(configResult.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
@@ -18,33 +16,21 @@ import android.os.StrictMode
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||
import com.v2ray.ang.AppConfig.VPN_MTU
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.NotificationManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
class V2RayVpnService : VpnService(), ServiceControl {
|
||||
companion object {
|
||||
private const val VPN_MTU = 1500
|
||||
private const val PRIVATE_VLAN4_CLIENT = "10.10.14.1"
|
||||
private const val PRIVATE_VLAN4_ROUTER = "10.10.14.2"
|
||||
private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:14:1"
|
||||
private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:14:2"
|
||||
private const val TUN2SOCKS = "libtun2socks.so"
|
||||
}
|
||||
|
||||
private lateinit var mInterface: ParcelFileDescriptor
|
||||
private var isRunning = false
|
||||
private lateinit var process: Process
|
||||
private var tun2SocksService: Tun2SocksService? = null
|
||||
|
||||
/**destroy
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||
@@ -101,11 +87,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
NotificationService.cancelNotification()
|
||||
NotificationManager.cancelNotification()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
V2RayServiceManager.startV2rayPoint()
|
||||
if (V2RayServiceManager.startCoreLoop()) {
|
||||
startService()
|
||||
}
|
||||
return START_STICKY
|
||||
//return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
@@ -115,7 +103,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
}
|
||||
|
||||
override fun startService() {
|
||||
setup()
|
||||
setupService()
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
@@ -138,13 +126,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
* Sets up the VPN service.
|
||||
* Prepares the VPN and configures it if preparation is successful.
|
||||
*/
|
||||
private fun setup() {
|
||||
private fun setupService() {
|
||||
val prepare = prepare(this)
|
||||
if (prepare != null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (setupVpnService() != true) {
|
||||
if (configureVpnService() != true) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -155,100 +143,152 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
* Configures the VPN service.
|
||||
* @return True if the VPN service was configured successfully, false otherwise.
|
||||
*/
|
||||
private fun setupVpnService(): Boolean {
|
||||
// If the old interface has exactly the same parameters, use it!
|
||||
// Configure a builder while parsing the parameters.
|
||||
private fun configureVpnService(): Boolean {
|
||||
val builder = Builder()
|
||||
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||
|
||||
// Configure network settings (addresses, routing and DNS)
|
||||
configureNetworkSettings(builder)
|
||||
|
||||
builder.setMtu(VPN_MTU)
|
||||
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
// Configure app-specific settings (session name and per-app proxy)
|
||||
configurePerAppProxy(builder)
|
||||
|
||||
// Close the old interface since the parameters have been changed
|
||||
try {
|
||||
mInterface.close()
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
// Configure platform-specific features
|
||||
configurePlatformFeatures(builder)
|
||||
|
||||
// Create a new interface using the builder and save the parameters
|
||||
try {
|
||||
mInterface = builder.establish()!!
|
||||
isRunning = true
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to establish VPN interface", e)
|
||||
stopV2Ray()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the basic network settings for the VPN.
|
||||
* This includes IP addresses, routing rules, and DNS servers.
|
||||
*
|
||||
* @param builder The VPN Builder to configure
|
||||
*/
|
||||
private fun configureNetworkSettings(builder: Builder) {
|
||||
val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
|
||||
val bypassLan = SettingsManager.routingRulesetsBypassLan()
|
||||
|
||||
// Configure IPv4 settings
|
||||
builder.setMtu(VPN_MTU)
|
||||
builder.addAddress(vpnConfig.ipv4Client, 30)
|
||||
|
||||
// Configure routing rules
|
||||
if (bypassLan) {
|
||||
resources.getStringArray(R.array.bypass_private_ip_address).forEach {
|
||||
AppConfig.ROUTED_IP_LIST.forEach {
|
||||
val addr = it.split('/')
|
||||
builder.addRoute(addr[0], addr[1].toInt())
|
||||
}
|
||||
} else {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
}
|
||||
|
||||
|
||||
// Configure IPv6 if enabled
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||
builder.addAddress(vpnConfig.ipv6Client, 126)
|
||||
if (bypassLan) {
|
||||
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
||||
builder.addRoute("2000::", 3) // Currently only 1/8 of total IPv6 is in use
|
||||
builder.addRoute("fc00::", 18) // Xray-core default FakeIPv6 Pool
|
||||
} else {
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
}
|
||||
|
||||
// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
// } else {
|
||||
SettingsManager.getVpnDnsServers()
|
||||
.forEach {
|
||||
if (Utils.isPureIpAddress(it)) {
|
||||
builder.addDnsServer(it)
|
||||
}
|
||||
|
||||
// Configure DNS servers
|
||||
//if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
//} else {
|
||||
SettingsManager.getVpnDnsServers().forEach {
|
||||
if (Utils.isPureIpAddress(it)) {
|
||||
builder.addDnsServer(it)
|
||||
}
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
builder.setSession(V2RayServiceManager.getRunningServerName())
|
||||
|
||||
val selfPackageName = BuildConfig.APPLICATION_ID
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
|
||||
val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
|
||||
//process self package
|
||||
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
|
||||
apps?.forEach {
|
||||
try {
|
||||
if (bypassApps)
|
||||
builder.addDisallowedApplication(it)
|
||||
else
|
||||
builder.addAllowedApplication(it)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
builder.addDisallowedApplication(selfPackageName)
|
||||
}
|
||||
|
||||
// Close the old interface since the parameters have been changed.
|
||||
try {
|
||||
mInterface.close()
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures platform-specific VPN features for different Android versions.
|
||||
*
|
||||
* @param builder The VPN Builder to configure
|
||||
*/
|
||||
private fun configurePlatformFeatures(builder: Builder) {
|
||||
// Android P (API 28) and above: Configure network callbacks
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to request default network", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Android Q (API 29) and above: Configure metering and HTTP proxy
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
builder.setMetered(false)
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY)) {
|
||||
builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOOPBACK, SettingsManager.getHttpPort()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new interface using the builder and save the parameters.
|
||||
try {
|
||||
mInterface = builder.establish()!!
|
||||
isRunning = true
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
// non-nullable lateinit var
|
||||
e.printStackTrace()
|
||||
stopV2Ray()
|
||||
/**
|
||||
* Configures per-app proxy rules for the VPN builder.
|
||||
*
|
||||
* - If per-app proxy is not enabled, disallow the VPN service's own package.
|
||||
* - If no apps are selected, disallow the VPN service's own package.
|
||||
* - If bypass mode is enabled, disallow all selected apps (including self).
|
||||
* - If proxy mode is enabled, only allow the selected apps (excluding self).
|
||||
*
|
||||
* @param builder The VPN Builder to configure.
|
||||
*/
|
||||
private fun configurePerAppProxy(builder: Builder) {
|
||||
val selfPackageName = BuildConfig.APPLICATION_ID
|
||||
|
||||
// If per-app proxy is not enabled, disallow the VPN service's own package and return
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY) == false) {
|
||||
builder.addDisallowedApplication(selfPackageName)
|
||||
return
|
||||
}
|
||||
|
||||
// If no apps are selected, disallow the VPN service's own package and return
|
||||
val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||
if (apps.isNullOrEmpty()) {
|
||||
builder.addDisallowedApplication(selfPackageName)
|
||||
return
|
||||
}
|
||||
|
||||
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
|
||||
// Handle the VPN service's own package according to the mode
|
||||
if (bypassApps) apps.add(selfPackageName) else apps.remove(selfPackageName)
|
||||
|
||||
apps.forEach {
|
||||
try {
|
||||
if (bypassApps) {
|
||||
// In bypass mode, disallow the selected apps
|
||||
builder.addDisallowedApplication(it)
|
||||
} else {
|
||||
// In proxy mode, only allow the selected apps
|
||||
builder.addAllowedApplication(it)
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Log.e(AppConfig.TAG, "Failed to configure app in VPN: ${e.localizedMessage}", e)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,77 +296,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
* Starts the tun2socks process with the appropriate parameters.
|
||||
*/
|
||||
private fun runTun2socks() {
|
||||
val socksPort = SettingsManager.getSocksPort()
|
||||
val cmd = arrayListOf(
|
||||
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
||||
"--netif-netmask", "255.255.255.252",
|
||||
"--socks-server-addr", "$LOOPBACK:${socksPort}",
|
||||
"--tunmtu", VPN_MTU.toString(),
|
||||
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
||||
"--enable-udprelay",
|
||||
"--loglevel", "notice"
|
||||
)
|
||||
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
|
||||
cmd.add("--netif-ip6addr")
|
||||
cmd.add(PRIVATE_VLAN6_ROUTER)
|
||||
}
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
|
||||
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
||||
cmd.add("--dnsgw")
|
||||
cmd.add("$LOOPBACK:${localDnsPort}")
|
||||
}
|
||||
Log.d(packageName, cmd.toString())
|
||||
|
||||
try {
|
||||
val proBuilder = ProcessBuilder(cmd)
|
||||
proBuilder.redirectErrorStream(true)
|
||||
process = proBuilder
|
||||
.directory(applicationContext.filesDir)
|
||||
.start()
|
||||
Thread {
|
||||
Log.d(packageName, "$TUN2SOCKS check")
|
||||
process.waitFor()
|
||||
Log.d(packageName, "$TUN2SOCKS exited")
|
||||
if (isRunning) {
|
||||
Log.d(packageName, "$TUN2SOCKS restart")
|
||||
runTun2socks()
|
||||
}
|
||||
}.start()
|
||||
Log.d(packageName, process.toString())
|
||||
|
||||
sendFd()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the file descriptor to the tun2socks process.
|
||||
* Attempts to send the file descriptor multiple times if necessary.
|
||||
*/
|
||||
private fun sendFd() {
|
||||
val fd = mInterface.fileDescriptor
|
||||
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
||||
Log.d(packageName, path)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var tries = 0
|
||||
while (true) try {
|
||||
Thread.sleep(50L shl tries)
|
||||
Log.d(packageName, "sendFd tries: $tries")
|
||||
LocalSocket().use { localSocket ->
|
||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||
localSocket.outputStream.write(42)
|
||||
}
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
if (tries > 5) break
|
||||
tries += 1
|
||||
}
|
||||
tun2SocksService = Tun2SocksService(
|
||||
context = applicationContext,
|
||||
vpnInterface = mInterface,
|
||||
isRunningProvider = { isRunning },
|
||||
restartCallback = { runTun2socks() }
|
||||
).also {
|
||||
it.startTun2Socks()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,14 +324,10 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d(packageName, "tun2socks destroy")
|
||||
process.destroy()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
tun2SocksService?.stopTun2Socks()
|
||||
tun2SocksService = null
|
||||
|
||||
V2RayServiceManager.stopV2rayPoint()
|
||||
V2RayServiceManager.stopCoreLoop()
|
||||
|
||||
if (isForced) {
|
||||
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
|
||||
@@ -367,9 +339,10 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
|
||||
try {
|
||||
mInterface.close()
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to close VPN interface", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.v2ray.ang.databinding.ActivityAboutBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SpeedtestManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.util.ZipUtil
|
||||
@@ -34,7 +35,7 @@ class AboutActivity : BaseActivity() {
|
||||
try {
|
||||
showFileChooser()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
|
||||
}
|
||||
} else {
|
||||
toast(R.string.toast_permission_denied)
|
||||
@@ -90,7 +91,7 @@ class AboutActivity : BaseActivity() {
|
||||
try {
|
||||
showFileChooser()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
|
||||
}
|
||||
} else {
|
||||
requestPermissionLauncher.launch(permission)
|
||||
@@ -98,15 +99,15 @@ class AboutActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
binding.layoutSoureCcode.setOnClickListener {
|
||||
Utils.openUri(this, AppConfig.v2rayNGUrl)
|
||||
Utils.openUri(this, AppConfig.APP_URL)
|
||||
}
|
||||
|
||||
binding.layoutFeedback.setOnClickListener {
|
||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
||||
Utils.openUri(this, AppConfig.APP_ISSUES_URL)
|
||||
}
|
||||
|
||||
binding.layoutOssLicenses.setOnClickListener {
|
||||
val webView = android.webkit.WebView(this);
|
||||
val webView = android.webkit.WebView(this)
|
||||
webView.loadUrl("file:///android_asset/open_source_licenses.html")
|
||||
android.app.AlertDialog.Builder(this)
|
||||
.setTitle("Open source licenses")
|
||||
@@ -116,11 +117,11 @@ class AboutActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
binding.layoutTgChannel.setOnClickListener {
|
||||
Utils.openUri(this, AppConfig.TgChannelUrl)
|
||||
Utils.openUri(this, AppConfig.TG_CHANNEL_URL)
|
||||
}
|
||||
|
||||
binding.layoutPrivacyPolicy.setOnClickListener {
|
||||
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
|
||||
Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY)
|
||||
}
|
||||
|
||||
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
||||
@@ -169,7 +170,7 @@ class AboutActivity : BaseActivity() {
|
||||
try {
|
||||
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||
} catch (ex: android.content.ActivityNotFoundException) {
|
||||
Log.e(AppConfig.ANG_PACKAGE, "File chooser activity not found: ${ex.message}", ex)
|
||||
Log.e(AppConfig.TAG, "File chooser activity not found", ex)
|
||||
toast(R.string.toast_require_file_manager)
|
||||
}
|
||||
}
|
||||
@@ -192,7 +193,7 @@ class AboutActivity : BaseActivity() {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
|
||||
Log.e(AppConfig.TAG, "Error during file restore", e)
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityCheckUpdateBinding
|
||||
import com.v2ray.ang.dto.CheckUpdateResult
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SpeedtestManager
|
||||
import com.v2ray.ang.handler.UpdateCheckerManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CheckUpdateActivity : BaseActivity() {
|
||||
|
||||
private val binding by lazy { ActivityCheckUpdateBinding.inflate(layoutInflater) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(binding.root)
|
||||
|
||||
title = getString(R.string.update_check_for_update)
|
||||
|
||||
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)
|
||||
|
||||
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
||||
binding.tvVersion.text = it
|
||||
}
|
||||
|
||||
checkForUpdates(binding.checkPreRelease.isChecked)
|
||||
}
|
||||
|
||||
private fun checkForUpdates(includePreRelease: Boolean) {
|
||||
toast(R.string.update_checking_for_update)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
|
||||
if (result.hasUpdate) {
|
||||
showUpdateDialog(result)
|
||||
} else {
|
||||
toastSuccess(R.string.update_already_latest_version)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
|
||||
toastError(e.message ?: getString(R.string.toast_failure))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||
@@ -68,7 +70,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to get logcat", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +91,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to clear logcat", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
||||
|
||||
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
||||
private var mActivity: LogcatActivity = activity
|
||||
|
||||
|
||||
override fun getItemCount() = mActivity.logsets.size
|
||||
|
||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||
@@ -22,7 +25,7 @@ class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter
|
||||
holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Error binding log view data", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -37,7 +38,7 @@ import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MigrateManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.MainViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -86,9 +87,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
Action.IMPORT_QR_CODE_CONFIG ->
|
||||
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||
|
||||
// Action.IMPORT_QR_CODE_URL ->
|
||||
// scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||
|
||||
Action.READ_CONTENT_FROM_URI ->
|
||||
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "*/*"
|
||||
@@ -109,8 +107,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
enum class Action {
|
||||
NONE,
|
||||
IMPORT_QR_CODE_CONFIG,
|
||||
|
||||
//IMPORT_QR_CODE_URL,
|
||||
READ_CONTENT_FROM_URI,
|
||||
POST_NOTIFICATIONS
|
||||
}
|
||||
@@ -128,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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(binding.root)
|
||||
@@ -324,7 +314,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.import_qrcode -> {
|
||||
importQRcode(true)
|
||||
importQRcode()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -378,26 +368,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
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 -> {
|
||||
exportAll()
|
||||
true
|
||||
@@ -415,6 +385,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
true
|
||||
}
|
||||
|
||||
R.id.intelligent_selection_all -> {
|
||||
mainViewModel.createIntelligentSelectionAll()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.service_restart -> {
|
||||
restartV2Ray()
|
||||
true
|
||||
@@ -461,16 +436,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
/**
|
||||
* import config from qrcode
|
||||
*/
|
||||
private fun importQRcode(forConfig: Boolean): Boolean {
|
||||
private fun importQRcode(): Boolean {
|
||||
val permission = Manifest.permission.CAMERA
|
||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||
if (forConfig) {
|
||||
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||
} else {
|
||||
//scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||
}
|
||||
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||
} else {
|
||||
pendingAction = Action.IMPORT_QR_CODE_CONFIG//if (forConfig) Action.IMPORT_QR_CODE_CONFIG else Action.IMPORT_QR_CODE_URL
|
||||
pendingAction = Action.IMPORT_QR_CODE_CONFIG
|
||||
requestPermissionLauncher.launch(permission)
|
||||
}
|
||||
return true
|
||||
@@ -485,7 +456,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
val clipboard = Utils.getClipboard(this)
|
||||
importBatchConfig(clipboard)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to import config from clipboard", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -515,93 +486,25 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
toastError(R.string.toast_failure)
|
||||
binding.pbWaiting.hide()
|
||||
}
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to import batch config", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* import config from local config file
|
||||
*/
|
||||
private fun importConfigLocal(): Boolean {
|
||||
try {
|
||||
showFileChooser()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to import config from local file", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
*/
|
||||
@@ -744,36 +647,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
importBatchConfig(input?.bufferedReader()?.readText())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to read content from URI", e)
|
||||
}
|
||||
} else {
|
||||
requestPermissionLauncher.launch(permission)
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 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?) {
|
||||
binding.tvTestState.text = content
|
||||
}
|
||||
@@ -807,8 +687,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
Intent(this, SettingsActivity::class.java)
|
||||
.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.check_for_update -> startActivity(Intent(this, CheckUpdateActivity::class.java))
|
||||
R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.v2ray.ang.ui
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -25,7 +26,8 @@ import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -187,7 +189,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
else -> mActivity.toast("else")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Error when sharing server", e)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
@@ -219,10 +221,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
* @param guid The server unique identifier
|
||||
*/
|
||||
private fun shareFullContent(guid: String) {
|
||||
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
|
||||
mActivity.toastSuccess(R.string.toast_success)
|
||||
} else {
|
||||
mActivity.toastError(R.string.toast_failure)
|
||||
mActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid)
|
||||
launch(Dispatchers.Main) {
|
||||
if (result == 0) {
|
||||
mActivity.toastSuccess(R.string.toast_success)
|
||||
} else {
|
||||
mActivity.toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +306,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
delay(500)
|
||||
V2RayServiceManager.startVService(mActivity)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.v2ray.ang.AppConfig
|
||||
@@ -21,15 +22,14 @@ import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.util.AppManagerUtil
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import es.dmoral.toasty.Toasty
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.Collator
|
||||
|
||||
class PerAppProxyActivity : BaseActivity() {
|
||||
private val binding by lazy {
|
||||
ActivityBypassListBinding.inflate(layoutInflater)
|
||||
}
|
||||
private val binding by lazy { ActivityBypassListBinding.inflate(layoutInflater) }
|
||||
|
||||
private var adapter: PerAppProxyAdapter? = null
|
||||
private var appsAll: List<AppInfo>? = null
|
||||
@@ -85,6 +85,10 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
|
||||
}
|
||||
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() {
|
||||
@@ -156,7 +160,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
toast(R.string.msg_downloading_content)
|
||||
binding.pbWaiting.show()
|
||||
|
||||
val url = AppConfig.androidpackagenamelistUrl
|
||||
val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
var content = HttpUtil.getUrlContent(url, 5000)
|
||||
if (content.isNullOrEmpty()) {
|
||||
@@ -164,7 +168,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
Log.d(ANG_PACKAGE, content)
|
||||
Log.i(AppConfig.TAG, content)
|
||||
selectProxyApp(content, true)
|
||||
toastSuccess(R.string.toast_success)
|
||||
binding.pbWaiting.hide()
|
||||
@@ -205,7 +209,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
adapter?.let { it ->
|
||||
it.apps.forEach block@{
|
||||
val packageName = it.packageName
|
||||
Log.d(ANG_PACKAGE, packageName)
|
||||
Log.i(AppConfig.TAG, packageName)
|
||||
if (!inProxyApps(proxyApps, packageName, force)) {
|
||||
adapter?.blacklist?.add(packageName)
|
||||
println(packageName)
|
||||
@@ -218,7 +222,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
adapter?.let { it ->
|
||||
it.apps.forEach block@{
|
||||
val packageName = it.packageName
|
||||
Log.d(ANG_PACKAGE, packageName)
|
||||
Log.i(AppConfig.TAG, packageName)
|
||||
if (inProxyApps(proxyApps, packageName, force)) {
|
||||
adapter?.blacklist?.add(packageName)
|
||||
println(packageName)
|
||||
@@ -229,7 +233,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Error selecting proxy app", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -4,10 +4,9 @@ import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -66,15 +65,9 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||
|
||||
val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "")
|
||||
found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) }
|
||||
binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
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])
|
||||
}
|
||||
binding.tvDomainStrategySummary.text = getDomainStrategy()
|
||||
binding.layoutDomainStrategy.setOnClickListener {
|
||||
setDomainStrategy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +90,22 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
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() {
|
||||
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||
@@ -110,7 +119,7 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
@@ -126,7 +135,7 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
val clipboard = try {
|
||||
Utils.getClipboard(this)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
|
||||
toastError(R.string.toast_failure)
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
|
||||
class ScSwitchActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.pm.PackageManager
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -21,6 +22,7 @@ import io.github.g00fy2.quickie.config.ScannerConfig
|
||||
|
||||
class ScannerActivity : BaseActivity() {
|
||||
|
||||
|
||||
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
|
||||
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
val uri = it.data?.data
|
||||
@@ -37,7 +39,7 @@ class ScannerActivity : BaseActivity() {
|
||||
finished(text)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
|
||||
toast(R.string.toast_decoding_failed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
@@ -14,13 +13,11 @@ import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig.DEFAULT_PORT
|
||||
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
|
||||
import com.v2ray.ang.AppConfig.REALITY
|
||||
import com.v2ray.ang.AppConfig.TLS
|
||||
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.R
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
@@ -120,6 +117,8 @@ class ServerActivity : BaseActivity() {
|
||||
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
|
||||
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
|
||||
private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.lay_spider_x) }
|
||||
private val et_mldsa65_verify: EditText? by lazy { findViewById(R.id.et_mldsa65_verify) }
|
||||
private val container_mldsa65_verify: LinearLayout? by lazy { findViewById(R.id.lay_mldsa65_verify) }
|
||||
private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) }
|
||||
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
|
||||
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
|
||||
@@ -256,9 +255,14 @@ class ServerActivity : BaseActivity() {
|
||||
// Case 1: Null or blank
|
||||
isBlank -> {
|
||||
listOf(
|
||||
container_sni, container_fingerprint, container_alpn,
|
||||
container_allow_insecure, container_public_key,
|
||||
container_short_id, container_spider_x
|
||||
container_sni,
|
||||
container_fingerprint,
|
||||
container_alpn,
|
||||
container_allow_insecure,
|
||||
container_public_key,
|
||||
container_short_id,
|
||||
container_spider_x,
|
||||
container_mldsa65_verify
|
||||
).forEach { it?.visibility = View.GONE }
|
||||
}
|
||||
|
||||
@@ -273,7 +277,8 @@ class ServerActivity : BaseActivity() {
|
||||
listOf(
|
||||
container_public_key,
|
||||
container_short_id,
|
||||
container_spider_x
|
||||
container_spider_x,
|
||||
container_mldsa65_verify
|
||||
).forEach { it?.visibility = View.GONE }
|
||||
}
|
||||
|
||||
@@ -287,7 +292,8 @@ class ServerActivity : BaseActivity() {
|
||||
listOf(
|
||||
container_public_key,
|
||||
container_short_id,
|
||||
container_spider_x
|
||||
container_spider_x,
|
||||
container_mldsa65_verify
|
||||
).forEach { it?.visibility = View.VISIBLE }
|
||||
}
|
||||
}
|
||||
@@ -329,7 +335,7 @@ class ServerActivity : BaseActivity() {
|
||||
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
|
||||
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
|
||||
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)
|
||||
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||
@@ -369,9 +375,12 @@ class ServerActivity : BaseActivity() {
|
||||
if (allowinsecure >= 0) {
|
||||
sp_allow_insecure?.setSelection(allowinsecure)
|
||||
}
|
||||
container_public_key?.visibility = View.GONE
|
||||
container_short_id?.visibility = View.GONE
|
||||
container_spider_x?.visibility = View.GONE
|
||||
listOf(
|
||||
container_public_key,
|
||||
container_short_id,
|
||||
container_spider_x,
|
||||
container_mldsa65_verify
|
||||
).forEach { it?.visibility = View.GONE }
|
||||
} else if (config.security == REALITY) {
|
||||
container_public_key?.visibility = View.VISIBLE
|
||||
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
||||
@@ -379,18 +388,23 @@ class ServerActivity : BaseActivity() {
|
||||
et_short_id?.text = Utils.getEditable(config.shortId.orEmpty())
|
||||
container_spider_x?.visibility = View.VISIBLE
|
||||
et_spider_x?.text = Utils.getEditable(config.spiderX.orEmpty())
|
||||
container_mldsa65_verify?.visibility = View.VISIBLE
|
||||
et_mldsa65_verify?.text = Utils.getEditable(config.mldsa65Verify.orEmpty())
|
||||
container_allow_insecure?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
if (config.security.isNullOrEmpty()) {
|
||||
container_sni?.visibility = View.GONE
|
||||
container_fingerprint?.visibility = View.GONE
|
||||
container_alpn?.visibility = View.GONE
|
||||
container_allow_insecure?.visibility = View.GONE
|
||||
container_public_key?.visibility = View.GONE
|
||||
container_short_id?.visibility = View.GONE
|
||||
container_spider_x?.visibility = View.GONE
|
||||
listOf(
|
||||
container_sni,
|
||||
container_fingerprint,
|
||||
container_alpn,
|
||||
container_allow_insecure,
|
||||
container_public_key,
|
||||
container_short_id,
|
||||
container_spider_x,
|
||||
container_mldsa65_verify
|
||||
).forEach { it?.visibility = View.GONE }
|
||||
}
|
||||
val network = Utils.arrayFind(networks, config.network.orEmpty())
|
||||
if (network >= 0) {
|
||||
@@ -422,7 +436,7 @@ class ServerActivity : BaseActivity() {
|
||||
et_public_key?.text = null
|
||||
et_reserved1?.text = Utils.getEditable("0,0,0")
|
||||
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)
|
||||
return true
|
||||
}
|
||||
@@ -481,7 +495,7 @@ class ServerActivity : BaseActivity() {
|
||||
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
||||
config.subscriptionId = subscriptionId.orEmpty()
|
||||
}
|
||||
Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "")
|
||||
//Log.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
|
||||
MmkvManager.encodeServerConfig(editGuid, config)
|
||||
toastSuccess(R.string.toast_success)
|
||||
finish()
|
||||
@@ -553,6 +567,7 @@ class ServerActivity : BaseActivity() {
|
||||
val publicKey = et_public_key?.text?.toString()
|
||||
val shortId = et_short_id?.text?.toString()
|
||||
val spiderX = et_spider_x?.text?.toString()
|
||||
val mldsa65Verify = et_mldsa65_verify?.text?.toString()
|
||||
|
||||
val allowInsecure =
|
||||
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
|
||||
@@ -569,6 +584,7 @@ class ServerActivity : BaseActivity() {
|
||||
config.publicKey = publicKey
|
||||
config.shortId = shortId
|
||||
config.spiderX = spiderX
|
||||
config.mldsa65Verify = mldsa65Verify
|
||||
}
|
||||
|
||||
private fun transportTypes(network: String?): Array<out String> {
|
||||
|
||||
@@ -2,11 +2,13 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.blacksquircle.ui.editorkit.utils.EditorTheme
|
||||
import com.blacksquircle.ui.language.json.JsonLanguage
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
@@ -76,7 +78,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
val profileItem = try {
|
||||
CustomFmt.parse(binding.editor.text.toString())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to parse custom configuration", e)
|
||||
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import com.v2ray.ang.AppConfig.VPN
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.toLongEx
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.service.SubscriptionUpdater
|
||||
import com.v2ray.ang.handler.SubscriptionUpdater
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.SettingsViewModel
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -44,6 +44,7 @@ class SettingsActivity : BaseActivity() {
|
||||
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
||||
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
||||
private val vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) }
|
||||
private val vpnInterfaceAddress by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) }
|
||||
|
||||
private val mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) }
|
||||
private val muxConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_CONCURRENCY) }
|
||||
@@ -161,7 +162,7 @@ class SettingsActivity : BaseActivity() {
|
||||
}
|
||||
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
|
||||
val nval = any as String
|
||||
delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval
|
||||
delayTestUrl?.summary = if (nval == "") AppConfig.DELAY_TEST_URL else nval
|
||||
true
|
||||
}
|
||||
mode?.setOnPreferenceChangeListener { _, newValue ->
|
||||
@@ -202,7 +203,7 @@ class SettingsActivity : BaseActivity() {
|
||||
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
||||
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
|
||||
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()
|
||||
}
|
||||
@@ -249,12 +250,15 @@ class SettingsActivity : BaseActivity() {
|
||||
|
||||
listOf(
|
||||
AppConfig.PREF_VPN_BYPASS_LAN,
|
||||
AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
|
||||
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
||||
AppConfig.PREF_MUX_XUDP_QUIC,
|
||||
AppConfig.PREF_FRAGMENT_PACKETS,
|
||||
AppConfig.PREF_LANGUAGE,
|
||||
AppConfig.PREF_UI_MODE_NIGHT,
|
||||
AppConfig.PREF_LOGLEVEL,
|
||||
AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
|
||||
AppConfig.PREF_INTELLIGENT_SELECTION_METHOD,
|
||||
AppConfig.PREF_MODE
|
||||
).forEach { key ->
|
||||
if (MmkvManager.decodeSettingsString(key) != null) {
|
||||
@@ -273,7 +277,7 @@ class SettingsActivity : BaseActivity() {
|
||||
localDnsPort?.isEnabled = vpn
|
||||
vpnDns?.isEnabled = vpn
|
||||
vpnBypassLan?.isEnabled = vpn
|
||||
vpn
|
||||
vpnInterfaceAddress?.isEnabled = vpn
|
||||
if (vpn) {
|
||||
updateLocalDns(
|
||||
MmkvManager.decodeSettingsBool(
|
||||
@@ -364,6 +368,6 @@ class SettingsActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
fun onModeHelpClicked(view: View) {
|
||||
Utils.openUri(this, AppConfig.v2rayNGWikiMode)
|
||||
Utils.openUri(this, AppConfig.APP_WIKI_MODE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
@@ -44,8 +45,10 @@ class SubEditActivity : BaseActivity() {
|
||||
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
|
||||
binding.etUrl.text = Utils.getEditable(subItem.url)
|
||||
binding.etFilter.text = Utils.getEditable(subItem.filter)
|
||||
binding.etIntelligentSelectionFilter.text = Utils.getEditable(subItem.intelligentSelectionFilter)
|
||||
binding.chkEnable.isChecked = subItem.enabled
|
||||
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
|
||||
binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl
|
||||
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
|
||||
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
|
||||
return true
|
||||
@@ -58,6 +61,7 @@ class SubEditActivity : BaseActivity() {
|
||||
binding.etRemarks.text = null
|
||||
binding.etUrl.text = null
|
||||
binding.etFilter.text = null
|
||||
binding.etIntelligentSelectionFilter.text = null
|
||||
binding.chkEnable.isChecked = true
|
||||
binding.etPreProfile.text = null
|
||||
binding.etNextProfile.text = null
|
||||
@@ -73,10 +77,12 @@ class SubEditActivity : BaseActivity() {
|
||||
subItem.remarks = binding.etRemarks.text.toString()
|
||||
subItem.url = binding.etUrl.text.toString()
|
||||
subItem.filter = binding.etFilter.text.toString()
|
||||
subItem.intelligentSelectionFilter = binding.etIntelligentSelectionFilter.text.toString()
|
||||
subItem.enabled = binding.chkEnable.isChecked
|
||||
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
|
||||
subItem.prevProfile = binding.etPreProfile.text.toString()
|
||||
subItem.nextProfile = binding.etNextProfile.text.toString()
|
||||
subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked
|
||||
|
||||
if (TextUtils.isEmpty(subItem.remarks)) {
|
||||
toast(R.string.sub_setting_remarks)
|
||||
@@ -90,7 +96,9 @@ class SubEditActivity : BaseActivity() {
|
||||
|
||||
if (!Utils.isValidSubUrl(subItem.url)) {
|
||||
toast(R.string.toast_insecure_url_protocol)
|
||||
//return false
|
||||
if (!subItem.allowInsecureUrl) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,19 +113,28 @@ class SubEditActivity : BaseActivity() {
|
||||
*/
|
||||
private fun deleteServer(): Boolean {
|
||||
if (editSubId.isNotEmpty()) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
MmkvManager.removeSubscription(editSubId)
|
||||
launch(Dispatchers.Main) {
|
||||
finish()
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
MmkvManager.removeSubscription(editSubId)
|
||||
launch(Dispatchers.Main) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// do nothing
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
MmkvManager.removeSubscription(editSubId)
|
||||
launch(Dispatchers.Main) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// do nothing
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ package com.v2ray.ang.ui
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
||||
@@ -18,6 +21,8 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||
import com.v2ray.ang.util.QRCodeDecoder
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
||||
|
||||
@@ -44,6 +49,10 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
||||
)
|
||||
}
|
||||
|
||||
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||
removeSubscription(subId, position)
|
||||
}
|
||||
|
||||
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||
if (!it.isPressed) return@setOnCheckedChangeListener
|
||||
subItem.enabled = isChecked
|
||||
@@ -52,9 +61,11 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(subItem.url)) {
|
||||
holder.itemSubSettingBinding.layoutUrl.visibility = View.GONE
|
||||
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
|
||||
holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
|
||||
} else {
|
||||
holder.itemSubSettingBinding.layoutUrl.visibility = View.VISIBLE
|
||||
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
|
||||
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
||||
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||
@@ -81,13 +92,39 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
||||
else -> mActivity.toast("else")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Share subscription failed", e)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeSubscription(subId: String, position: Int) {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
removeSubscriptionSub(subId, position)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
//do noting
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
removeSubscriptionSub(subId, position)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeSubscriptionSub(subId: String, position: Int) {
|
||||
mActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||
MmkvManager.removeSubscription(subId)
|
||||
launch(Dispatchers.Main) {
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, mActivity.subscriptions.size)
|
||||
mActivity.refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||
return MainViewHolder(
|
||||
ItemRecyclerSubSettingBinding.inflate(
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.v2ray.ang.ui
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
@@ -60,7 +61,7 @@ class TaskerActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to initialize Tasker settings", e)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
@@ -54,7 +55,7 @@ class UrlSchemeActivity : BaseActivity() {
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Error processing URL scheme", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ class UrlSchemeActivity : BaseActivity() {
|
||||
if (uriString.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
Log.d("UrlScheme", uriString)
|
||||
Log.i(AppConfig.TAG, uriString)
|
||||
|
||||
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
|
||||
val uri = Uri.parse(decodedUrl)
|
||||
@@ -70,7 +71,7 @@ class UrlSchemeActivity : BaseActivity() {
|
||||
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
|
||||
decodedUrl += "#${fragment}"
|
||||
}
|
||||
Log.d("UrlScheme-decodedUrl", decodedUrl)
|
||||
Log.i(AppConfig.TAG, decodedUrl)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -21,9 +21,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.v2ray.ang.AppConfig
|
||||
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.dto.AssetUrlItem
|
||||
import com.v2ray.ang.extension.concatUrl
|
||||
import com.v2ray.ang.extension.toTrafficString
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastError
|
||||
@@ -42,7 +43,7 @@ import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
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 builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
||||
@@ -89,6 +90,11 @@ class UserAssetActivity : BaseActivity() {
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||
binding.recyclerView.adapter = UserAssetAdapter()
|
||||
|
||||
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
|
||||
binding.layoutGeoFilesSources.setOnClickListener {
|
||||
setGeoFilesSources()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -110,6 +116,22 @@ class UserAssetActivity : BaseActivity() {
|
||||
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() {
|
||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Manifest.permission.READ_MEDIA_IMAGES
|
||||
@@ -163,7 +185,7 @@ class UserAssetActivity : BaseActivity() {
|
||||
}.also { cursor.close() }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to get cursor name", e)
|
||||
null
|
||||
}
|
||||
|
||||
@@ -190,7 +212,7 @@ class UserAssetActivity : BaseActivity() {
|
||||
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to import asset from URL", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -207,12 +229,16 @@ class UserAssetActivity : BaseActivity() {
|
||||
var resultCount = 0
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
assets.forEach {
|
||||
var result = downloadGeo(it.second, 15000, httpPort)
|
||||
if (!result) {
|
||||
result = downloadGeo(it.second, 15000, 0)
|
||||
try {
|
||||
var result = downloadGeo(it.second, 15000, httpPort)
|
||||
if (!result) {
|
||||
result = downloadGeo(it.second, 15000, 0)
|
||||
}
|
||||
if (result)
|
||||
resultCount++
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e)
|
||||
}
|
||||
if (result)
|
||||
resultCount++
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (resultCount > 0) {
|
||||
@@ -229,7 +255,7 @@ class UserAssetActivity : BaseActivity() {
|
||||
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
||||
val targetTemp = File(extDir, item.remarks + "_temp")
|
||||
val target = File(extDir, item.remarks)
|
||||
//Log.d(AppConfig.ANG_PACKAGE, url)
|
||||
Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}")
|
||||
|
||||
val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
|
||||
try {
|
||||
@@ -244,10 +270,10 @@ class UserAssetActivity : BaseActivity() {
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.ANG_PACKAGE, Log.getStackTraceString(e))
|
||||
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
|
||||
return false
|
||||
} finally {
|
||||
conn?.disconnect()
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +285,8 @@ class UserAssetActivity : BaseActivity() {
|
||||
list.add(
|
||||
Utils.getUuid() to AssetUrlItem(
|
||||
it,
|
||||
AppConfig.GeoUrl + it
|
||||
String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it),
|
||||
locked = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -310,7 +337,7 @@ class UserAssetActivity : BaseActivity() {
|
||||
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.layoutRemove.visibility = GONE
|
||||
} else {
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
|
||||
import com.v2ray.ang.dto.AssetUrlItem
|
||||
@@ -75,7 +77,11 @@ class UserAssetUrlActivity : BaseActivity() {
|
||||
// remove file associated with the asset
|
||||
val file = extDir.resolve(assetItem.remarks)
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
try {
|
||||
file.delete()
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assetId = Utils.getUuid()
|
||||
|
||||
@@ -25,7 +25,7 @@ object AppManagerUtil {
|
||||
|
||||
val appName = applicationInfo.loadLabel(packageManager).toString()
|
||||
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)
|
||||
apps.add(appInfo)
|
||||
@@ -33,4 +33,8 @@ object AppManagerUtil {
|
||||
|
||||
return@withContext apps
|
||||
}
|
||||
}
|
||||
|
||||
fun getLastUpdateTime(context: Context): Long =
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
package com.v2ray.ang.util
|
||||
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.util.Utils.encode
|
||||
import com.v2ray.ang.util.Utils.urlDecode
|
||||
import java.io.IOException
|
||||
import java.net.*
|
||||
import java.util.*
|
||||
import java.net.HttpURLConnection
|
||||
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 {
|
||||
|
||||
/**
|
||||
* Converts a URL string to its ASCII representation.
|
||||
* Converts the domain part of a URL string to its IDN (Punycode, ASCII Compatible Encoding) format.
|
||||
*
|
||||
* @param str The URL string to convert.
|
||||
* @return The ASCII representation of the URL.
|
||||
* For example, a URL like "https://例子.中国/path" will be converted to "https://xn--fsqu00a.xn--fiqs8s/path".
|
||||
*
|
||||
* @param str The URL string to convert (can contain non-ASCII characters in the domain).
|
||||
* @return The URL string with the domain part converted to ASCII-compatible (Punycode) format.
|
||||
*/
|
||||
fun idnToASCII(str: String): String {
|
||||
val url = URI(str)
|
||||
fun toIdnUrl(str: String): String {
|
||||
val url = URL(str)
|
||||
val host = url.host
|
||||
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
|
||||
if (host != asciiHost) {
|
||||
@@ -27,6 +36,67 @@ object HttpUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Unicode domain name to its IDN (Punycode, ASCII Compatible Encoding) format.
|
||||
* If the input is an IP address or already an ASCII domain, returns the original string.
|
||||
*
|
||||
* @param domain The domain string to convert (can include non-ASCII internationalized characters).
|
||||
* @return The domain in ASCII-compatible (Punycode) format, or the original string if input is an IP or already ASCII.
|
||||
*/
|
||||
fun toIdnDomain(domain: String): String {
|
||||
// Return as is if it's a pure IP address (IPv4 or IPv6)
|
||||
if (Utils.isPureIpAddress(domain)) {
|
||||
return domain
|
||||
}
|
||||
|
||||
// Return as is if already ASCII (English domain or already punycode)
|
||||
if (domain.all { it.code < 128 }) {
|
||||
return domain
|
||||
}
|
||||
|
||||
// Otherwise, convert to ASCII using IDN
|
||||
return IDN.toASCII(domain, IDN.ALLOW_UNASSIGNED)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -142,7 +212,7 @@ object HttpUtil {
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to create proxy connection", e)
|
||||
// If an exception occurs, close the connection and return null
|
||||
conn?.disconnect()
|
||||
return null
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.v2ray.ang.util
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonObject
|
||||
@@ -8,6 +9,7 @@ import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.v2ray.ang.AppConfig
|
||||
import java.lang.reflect.Type
|
||||
|
||||
object JsonUtil {
|
||||
@@ -70,7 +72,7 @@ object JsonUtil {
|
||||
try {
|
||||
return JsonParser.parseString(src).getAsJsonObject()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to parse JSON string", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ package com.v2ray.ang.util
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.service.V2RayTestService
|
||||
import java.io.Serializable
|
||||
|
||||
object MessageUtil {
|
||||
|
||||
|
||||
/**
|
||||
* Sends a message to the service.
|
||||
*
|
||||
@@ -46,7 +48,7 @@ object MessageUtil {
|
||||
intent.putExtra("content", content)
|
||||
ctx.startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to send message to test service", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +69,7 @@ object MessageUtil {
|
||||
intent.putExtra("content", content)
|
||||
ctx.sendBroadcast(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to send message with action: $action", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@ import android.webkit.URLUtil
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.util.Locale
|
||||
@@ -28,6 +30,10 @@ import java.util.UUID
|
||||
|
||||
object Utils {
|
||||
|
||||
private val IPV4_REGEX =
|
||||
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
||||
private val IPV6_REGEX = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
|
||||
|
||||
/**
|
||||
* Convert string to editable for Kotlin.
|
||||
*
|
||||
@@ -46,22 +52,7 @@ object Utils {
|
||||
* @return The index of the value in the array, or -1 if not found.
|
||||
*/
|
||||
fun arrayFind(array: Array<out String>, value: String): Int {
|
||||
for (i in array.indices) {
|
||||
if (array[i] == value) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string to an integer.
|
||||
*
|
||||
* @param str The string to parse.
|
||||
* @return The parsed integer, or 0 if parsing fails.
|
||||
*/
|
||||
fun parseInt(str: String): Int {
|
||||
return parseInt(str, 0)
|
||||
return array.indexOf(value)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +62,7 @@ object Utils {
|
||||
* @param default The default value if parsing fails.
|
||||
* @return The parsed integer, or the default value if parsing fails.
|
||||
*/
|
||||
fun parseInt(str: String?, default: Int): Int {
|
||||
fun parseInt(str: String?, default: Int = 0): Int {
|
||||
return str?.toIntOrNull() ?: default
|
||||
}
|
||||
|
||||
@@ -86,7 +77,7 @@ object Utils {
|
||||
val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
cmb.primaryClip?.getItemAt(0)?.text.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
|
||||
""
|
||||
}
|
||||
}
|
||||
@@ -103,7 +94,7 @@ object Utils {
|
||||
val clipData = ClipData.newPlainText(null, content)
|
||||
cmb.setPrimaryClip(clipData)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to set clipboard content", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,15 +115,17 @@ object Utils {
|
||||
* @return The decoded string, or null if decoding fails.
|
||||
*/
|
||||
fun tryDecodeBase64(text: String?): String? {
|
||||
if (text.isNullOrEmpty()) return null
|
||||
|
||||
try {
|
||||
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.i(ANG_PACKAGE, "Parse base64 standard failed $e")
|
||||
Log.e(AppConfig.TAG, "Failed to decode standard base64", e)
|
||||
}
|
||||
try {
|
||||
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e")
|
||||
Log.e(AppConfig.TAG, "Failed to decode URL-safe base64", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -147,7 +140,7 @@ object Utils {
|
||||
return try {
|
||||
Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to encode text to base64", e)
|
||||
""
|
||||
}
|
||||
}
|
||||
@@ -159,43 +152,38 @@ object Utils {
|
||||
* @return True if the string is a valid IP address, false otherwise.
|
||||
*/
|
||||
fun isIpAddress(value: String?): Boolean {
|
||||
if (value.isNullOrEmpty()) return false
|
||||
|
||||
try {
|
||||
if (value.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
var addr = value
|
||||
if (addr.isEmpty() || addr.isBlank()) {
|
||||
return false
|
||||
}
|
||||
var addr = value.trim()
|
||||
if (addr.isEmpty()) return false
|
||||
|
||||
//CIDR
|
||||
if (addr.indexOf("/") > 0) {
|
||||
if (addr.contains("/")) {
|
||||
val arr = addr.split("/")
|
||||
if (arr.count() == 2 && Integer.parseInt(arr[1]) > -1) {
|
||||
if (arr.size == 2 && arr[1].toIntOrNull() != null && arr[1].toInt() > -1) {
|
||||
addr = arr[0]
|
||||
}
|
||||
}
|
||||
|
||||
// "::ffff:192.168.173.22"
|
||||
// "[::ffff:192.168.173.22]:80"
|
||||
// Handle IPv4-mapped IPv6 addresses
|
||||
if (addr.startsWith("::ffff:") && '.' in addr) {
|
||||
addr = addr.drop(7)
|
||||
} else if (addr.startsWith("[::ffff:") && '.' in addr) {
|
||||
addr = addr.drop(8).replace("]", "")
|
||||
}
|
||||
|
||||
// addr = addr.toLowerCase()
|
||||
val octets = addr.split('.').toTypedArray()
|
||||
val octets = addr.split('.')
|
||||
if (octets.size == 4) {
|
||||
if (octets[3].indexOf(":") > 0) {
|
||||
if (octets[3].contains(":")) {
|
||||
addr = addr.substring(0, addr.indexOf(":"))
|
||||
}
|
||||
return isIpv4Address(addr)
|
||||
}
|
||||
|
||||
// Ipv6addr [2001:abc::123]:8080
|
||||
return isIpv6Address(addr)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to validate IP address", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -210,6 +198,21 @@ object Utils {
|
||||
return isIpv4Address(value) || isIpv6Address(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid domain name.
|
||||
*
|
||||
* A valid domain name must not be an IP address and must be a valid URL format.
|
||||
*
|
||||
* @param input The string to check.
|
||||
* @return True if the string is a valid domain name, false otherwise.
|
||||
*/
|
||||
fun isDomainName(input: String?): Boolean {
|
||||
if (input.isNullOrEmpty()) return false
|
||||
|
||||
// Must not be an IP address and must be a valid URL format
|
||||
return !isPureIpAddress(input) && isValidUrl(input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid IPv4 address.
|
||||
*
|
||||
@@ -217,9 +220,7 @@ object Utils {
|
||||
* @return True if the string is a valid IPv4 address, false otherwise.
|
||||
*/
|
||||
private fun isIpv4Address(value: String): Boolean {
|
||||
val regV4 =
|
||||
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
||||
return regV4.matches(value)
|
||||
return IPV4_REGEX.matches(value)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,13 +231,10 @@ object Utils {
|
||||
*/
|
||||
private fun isIpv6Address(value: String): Boolean {
|
||||
var addr = value
|
||||
if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) {
|
||||
addr = addr.drop(1)
|
||||
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
|
||||
if (addr.startsWith("[") && addr.endsWith("]")) {
|
||||
addr = addr.drop(1).dropLast(1)
|
||||
}
|
||||
val regV6 =
|
||||
Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
|
||||
return regV6.matches(addr)
|
||||
return IPV6_REGEX.matches(addr)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,10 +244,10 @@ object Utils {
|
||||
* @return True if the string is a CoreDNS address, false otherwise.
|
||||
*/
|
||||
fun isCoreDNSAddress(s: String): Boolean {
|
||||
return s.startsWith("https")
|
||||
|| s.startsWith("tcp")
|
||||
|| s.startsWith("quic")
|
||||
|| s == "localhost"
|
||||
return s.startsWith("https") ||
|
||||
s.startsWith("tcp") ||
|
||||
s.startsWith("quic") ||
|
||||
s == "localhost"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,21 +257,16 @@ object Utils {
|
||||
* @return True if the string is a valid URL, false otherwise.
|
||||
*/
|
||||
fun isValidUrl(value: String?): Boolean {
|
||||
try {
|
||||
if (value.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
if (Patterns.WEB_URL.matcher(value).matches()
|
||||
|| Patterns.DOMAIN_NAME.matcher(value).matches()
|
||||
|| URLUtil.isValidUrl(value)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (value.isNullOrEmpty()) return false
|
||||
|
||||
return try {
|
||||
Patterns.WEB_URL.matcher(value).matches() ||
|
||||
Patterns.DOMAIN_NAME.matcher(value).matches() ||
|
||||
URLUtil.isValidUrl(value)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
Log.e(AppConfig.TAG, "Failed to validate URL", e)
|
||||
false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,8 +276,12 @@ object Utils {
|
||||
* @param uriString The URI string to open.
|
||||
*/
|
||||
fun openUri(context: Context, uriString: String) {
|
||||
val uri = uriString.toUri()
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||
try {
|
||||
val uri = uriString.toUri()
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to open URI", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,7 +293,7 @@ object Utils {
|
||||
return try {
|
||||
UUID.randomUUID().toString().replace("-", "")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to generate UUID", e)
|
||||
""
|
||||
}
|
||||
}
|
||||
@@ -311,7 +308,7 @@ object Utils {
|
||||
return try {
|
||||
URLDecoder.decode(url, Charsets.UTF_8.toString())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to decode URL", e)
|
||||
url
|
||||
}
|
||||
}
|
||||
@@ -326,7 +323,7 @@ object Utils {
|
||||
return try {
|
||||
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to encode URL", e)
|
||||
url
|
||||
}
|
||||
}
|
||||
@@ -339,13 +336,18 @@ object Utils {
|
||||
* @return The content of the asset file as a string.
|
||||
*/
|
||||
fun readTextFromAssets(context: Context?, fileName: String): String {
|
||||
if (context == null) {
|
||||
return ""
|
||||
if (context == null) return ""
|
||||
|
||||
return try {
|
||||
context.assets.open(fileName).use { inputStream ->
|
||||
inputStream.bufferedReader().use { reader ->
|
||||
reader.readText()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e)
|
||||
""
|
||||
}
|
||||
val content = context.assets.open(fileName).bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,11 +357,15 @@ object Utils {
|
||||
* @return The path to the user asset directory.
|
||||
*/
|
||||
fun userAssetPath(context: Context?): String {
|
||||
if (context == null)
|
||||
return ""
|
||||
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS)
|
||||
?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
|
||||
return extDir.absolutePath
|
||||
if (context == null) return ""
|
||||
|
||||
return try {
|
||||
context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
|
||||
?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to get user asset path", e)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -369,11 +375,15 @@ object Utils {
|
||||
* @return The path to the backup directory.
|
||||
*/
|
||||
fun backupPath(context: Context?): String {
|
||||
if (context == null)
|
||||
return ""
|
||||
val extDir = context.getExternalFilesDir(AppConfig.DIR_BACKUPS)
|
||||
?: return context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
|
||||
return extDir.absolutePath
|
||||
if (context == null) return ""
|
||||
|
||||
return try {
|
||||
context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath
|
||||
?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to get backup path", e)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,8 +392,13 @@ object Utils {
|
||||
* @return The device ID for XUDP base key.
|
||||
*/
|
||||
fun getDeviceIdForXUDPBaseKey(): String {
|
||||
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
|
||||
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
||||
return try {
|
||||
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
|
||||
Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to generate device ID", e)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,11 +418,10 @@ object Utils {
|
||||
* @return The formatted IPv6 address, or the original address if not valid.
|
||||
*/
|
||||
fun getIpv6Address(address: String?): String {
|
||||
if (address == null) {
|
||||
return ""
|
||||
}
|
||||
if (address.isNullOrEmpty()) return ""
|
||||
|
||||
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
|
||||
String.format("[%s]", address)
|
||||
"[$address]"
|
||||
} else {
|
||||
address
|
||||
}
|
||||
@@ -431,8 +445,7 @@ object Utils {
|
||||
* @return The URL string with illegal characters replaced.
|
||||
*/
|
||||
fun fixIllegalUrl(str: String): String {
|
||||
return str
|
||||
.replace(" ", "%20")
|
||||
return str.replace(" ", "%20")
|
||||
.replace("|", "%7C")
|
||||
}
|
||||
|
||||
@@ -463,12 +476,23 @@ object Utils {
|
||||
* @return True if the string is a valid subscription URL, false otherwise.
|
||||
*/
|
||||
fun isValidSubUrl(value: String?): Boolean {
|
||||
if (value.isNullOrEmpty()) return false
|
||||
|
||||
try {
|
||||
if (value.isNullOrEmpty()) return false
|
||||
if (URLUtil.isHttpsUrl(value)) return true
|
||||
if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true
|
||||
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) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -489,7 +513,58 @@ object Utils {
|
||||
*
|
||||
* @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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.v2ray.ang.util
|
||||
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
@@ -61,7 +63,7 @@ object ZipUtil {
|
||||
zos.closeEntry()
|
||||
zos.close()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to zip folder", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -97,7 +99,7 @@ object ZipUtil {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(AppConfig.TAG, "Failed to unzip file", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.ServersCache
|
||||
@@ -63,7 +62,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
SpeedtestManager.closeAllTcpSockets()
|
||||
Log.i(ANG_PACKAGE, "Main ViewModel is cleared")
|
||||
Log.i(AppConfig.TAG, "Main ViewModel is cleared")
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@@ -385,6 +384,29 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
MmkvManager.encodeServerList(serverList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an intelligent selection configuration containing all currently filtered servers.
|
||||
*/
|
||||
fun createIntelligentSelectionAll() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val key = AngConfigManager.createIntelligentSelection(
|
||||
getApplication<AngApplication>(),
|
||||
serversCache.map { it.guid }.toList(),
|
||||
subscriptionId
|
||||
)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
if (key.isNullOrEmpty()) {
|
||||
getApplication<AngApplication>().toastError(R.string.toast_failure)
|
||||
} else {
|
||||
getApplication<AngApplication>().toastSuccess(R.string.toast_success)
|
||||
MmkvManager.setSelectServer(key)
|
||||
reloadServerList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes assets.
|
||||
* @param assets The asset manager.
|
||||
|
||||
@@ -26,7 +26,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
||||
override fun onCleared() {
|
||||
PreferenceManager.getDefaultSharedPreferences(getApplication())
|
||||
.unregisterOnSharedPreferenceChangeListener(this)
|
||||
Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared")
|
||||
Log.i(AppConfig.TAG, "Settings ViewModel is cleared")
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@@ -36,11 +36,12 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
||||
* @param key The key of the changed preference.
|
||||
*/
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key")
|
||||
Log.i(AppConfig.TAG, "Observe settings changed: $key")
|
||||
when (key) {
|
||||
AppConfig.PREF_MODE,
|
||||
AppConfig.PREF_VPN_DNS,
|
||||
AppConfig.PREF_VPN_BYPASS_LAN,
|
||||
AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
|
||||
AppConfig.PREF_REMOTE_DNS,
|
||||
AppConfig.PREF_DOMESTIC_DNS,
|
||||
AppConfig.PREF_DNS_HOSTS,
|
||||
@@ -48,6 +49,8 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
||||
AppConfig.PREF_LOCAL_DNS_PORT,
|
||||
AppConfig.PREF_SOCKS_PORT,
|
||||
AppConfig.PREF_LOGLEVEL,
|
||||
AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
|
||||
AppConfig.PREF_INTELLIGENT_SELECTION_METHOD,
|
||||
AppConfig.PREF_LANGUAGE,
|
||||
AppConfig.PREF_UI_MODE_NIGHT,
|
||||
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
||||
|
||||
@@ -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>
|
||||
@@ -5,7 +5,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.PerAppProxyActivity">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/pb_waiting"
|
||||
@@ -71,6 +72,23 @@
|
||||
android:textColor="@color/colorAccent"
|
||||
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>
|
||||
|
||||
@@ -83,7 +101,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
tools:context=".ui.PerAppProxyActivity" />
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" />
|
||||
|
||||
</LinearLayout>
|
||||
82
V2rayNG/app/src/main/res/layout/activity_check_update.xml
Normal file
82
V2rayNG/app/src/main/res/layout/activity_check_update.xml
Normal file
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android: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_source_code_24dp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/check_pre_release"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/update_check_pre_release"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="@color/colorAccent"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
</LinearLayout>
|
||||
|
||||
<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" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/update_check_for_update"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_version"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_about"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.SubSettingActivity">
|
||||
tools:context=".ui.RoutingEditActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -18,30 +18,35 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_domain_strategy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/padding_spacing_dp8"
|
||||
android:orientation="vertical">
|
||||
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/routing_settings_domain_strategy" />
|
||||
android:text="@string/routing_settings_domain_strategy"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/sp_domain_strategy"
|
||||
android:layout_width="match_parent"
|
||||
<TextView
|
||||
android:id="@+id/tv_domain_strategy_summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp8"
|
||||
android:entries="@array/routing_domain_strategy"
|
||||
android:paddingTop="@dimen/padding_spacing_dp8" />
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/padding_spacing_dp8"
|
||||
android:layout_margin="@dimen/padding_spacing_dp16"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.SubSettingActivity">
|
||||
tools:context=".ui.SubEditActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -26,7 +26,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_server"
|
||||
android:text="@string/title_sub_setting"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -93,6 +93,25 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp16"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sub_setting_intelligent_selection_filter" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_intelligent_selection_filter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -138,6 +157,28 @@
|
||||
|
||||
</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
|
||||
android:layout_width="match_parent"
|
||||
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>
|
||||
@@ -15,97 +15,144 @@
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusRight="@+id/layout_edit"
|
||||
android:nextFocusRight="@+id/layout_share"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_url"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
android:paddingStart="@dimen/padding_spacing_dp8">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
app:srcCompat="@drawable/ic_share_24dp" />
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="2"
|
||||
android:minLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusLeft="@+id/info_container"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusLeft="@+id/info_container"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
app:srcCompat="@drawable/ic_share_24dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
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_edit_24dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_remove"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
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_delete_24dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
app:srcCompat="@drawable/ic_edit_24dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/layout_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/padding_spacing_dp8"
|
||||
android:paddingEnd="@dimen/padding_spacing_dp8">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/chk_enable"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="@dimen/padding_spacing_dp8">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_url"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/chk_enable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -178,4 +178,25 @@
|
||||
android:nextFocusDown="@+id/sp_stream_fingerprint" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lay_mldsa65_verify"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp16"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/server_lab_mldsa65_verify" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_mldsa65_verify"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:nextFocusDown="@+id/sp_stream_fingerprint" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -35,6 +35,10 @@
|
||||
android:id="@+id/logcat"
|
||||
android:icon="@drawable/ic_logcat_24dp"
|
||||
android:title="@string/title_logcat" />
|
||||
<item
|
||||
android:id="@+id/check_for_update"
|
||||
android:icon="@drawable/ic_check_update_24dp"
|
||||
android:title="@string/update_check_for_update" />
|
||||
<item
|
||||
android:id="@+id/about"
|
||||
android:icon="@drawable/ic_about_24dp"
|
||||
|
||||
@@ -56,29 +56,6 @@
|
||||
android:id="@+id/import_manually_hysteria2"
|
||||
android:title="@string/menu_item_import_config_manually_hysteria2"
|
||||
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>
|
||||
</item>
|
||||
<item
|
||||
@@ -105,6 +82,10 @@
|
||||
android:icon="@drawable/ic_share_24dp"
|
||||
android:title="@string/title_export_all"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/intelligent_selection_all"
|
||||
android:title="@string/title_create_intelligent_selection_all_server"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/ping_all"
|
||||
android:title="@string/title_ping_all_server"
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
<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_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_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||
<string name="server_lab_remarks">ملاحظات</string>
|
||||
@@ -127,6 +122,7 @@
|
||||
<string name="title_user_asset_add_url">إضافة عنوان URL للأصل</string>
|
||||
<string name="msg_file_not_found">الملف غير موجود</string>
|
||||
<string name="msg_remark_is_duplicate">الملاحظات موجودة بالفعل</string>
|
||||
<string name="asset_geo_files_sources">Geo files source (optional)</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">جار التحميل</string>
|
||||
@@ -145,6 +141,7 @@
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">الإعدادات</string>
|
||||
<string name="title_advanced">إعدادات متقدمة</string>
|
||||
<string name="title_core_settings">إعدادات النواة</string>
|
||||
<string name="title_vpn_settings">إعدادات VPN</string>
|
||||
<string name="title_pref_per_app_proxy">الوكيل لكل تطبيق</string>
|
||||
<string name="summary_pref_per_app_proxy">عام: التطبيق المحدد هو وكيل، غير المحدد اتصال مباشر؛ \nوضع التجاوز: التطبيق المحدد متصل مباشرة، غير المحدد وكيل. \nخيار تحديد تطبيق الوكيل تلقائيًا في القائمة</string>
|
||||
@@ -185,6 +182,8 @@
|
||||
<string name="title_pref_vpn_dns">VPN DNS (IPv4/v6 فقط)</string>
|
||||
<string name="title_pref_vpn_bypass_lan">Does VPN bypass LAN</string>
|
||||
|
||||
<string name="title_pref_vpn_interface_address">VPN Interface Address</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">DNS المحلي (اختياري)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
@@ -242,6 +241,7 @@
|
||||
<string name="title_pref_auto_update_interval">فاصل التحديث التلقائي (بالدقائق، الحد الأدنى للقيمة 15)</string>
|
||||
|
||||
<string name="title_core_loglevel">مستوى السجل</string>
|
||||
<string name="title_outbound_domain_resolve_method">Outbound domain pre-resolve method</string>
|
||||
<string name="title_mode">الوضع</string>
|
||||
<string name="title_mode_help">انقر هنا للحصول على مزيد من المساعدة</string>
|
||||
<string name="title_language">اللغة</string>
|
||||
@@ -262,12 +262,14 @@
|
||||
<string name="sub_setting_filter">Remarks regular filter</string>
|
||||
<string name="sub_setting_enable">تفعيل التحديث</string>
|
||||
<string name="sub_auto_update">تفعيل التحديث التلقائي</string>
|
||||
<string name="sub_setting_pre_profile">Previous proxy remarks</string>
|
||||
<string name="sub_setting_next_profile">Next proxy remarks</string>
|
||||
<string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string>
|
||||
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
|
||||
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
|
||||
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
|
||||
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||
<string name="title_sub_update">تحديث الاشتراك (أول خطوة)</string>
|
||||
<string name="title_ping_all_server">Tcping لجميع الإعدادات</string>
|
||||
<string name="title_real_ping_all_server"> اختبر جميع الإعدادات (3)</string>
|
||||
<string name="title_create_intelligent_selection_all_server">Creating Intelligent Selection Current Group Configuration</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">الفرز حسب نتائج الاختبار (5)</string>
|
||||
<string name="title_filter_config">تصفية ملف التكوين</string>
|
||||
@@ -314,6 +316,13 @@
|
||||
<string name="title_pref_fragment_interval">فاصل الجزء (الحد الأدنى - الحد الأقصى)</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 name="update_checking_for_update">Checking for update…</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>رمز استجابة سريعة (QRcode)</item>
|
||||
<item>تصدير إلى الحافظة</item>
|
||||
@@ -350,4 +359,17 @@
|
||||
<item>Not Bypass</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>Do not resolve</item>
|
||||
<item>Resolve and add to DNS Hosts</item>
|
||||
<item>Resolve and replace domain</item>
|
||||
</string-array>
|
||||
<string name="intelligent_selection">Intelligent Selection</string>
|
||||
<string name="sub_setting_intelligent_selection_filter">Remarks Intelligent Selection regular filter</string>
|
||||
<string name="title_intelligent_selection_method">Intelligent Selection Method</string>
|
||||
<string-array name="intelligent_selection_method">
|
||||
<item>Least Ping</item>
|
||||
<item>Least Load</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
<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_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_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||
<string name="server_lab_remarks">মন্তব্য</string>
|
||||
@@ -85,6 +80,7 @@
|
||||
<string name="server_lab_preshared_key">PreSharedKey(optional)</string>
|
||||
<string name="server_lab_short_id" translatable="false">শর্ট আইডি</string>
|
||||
<string name="server_lab_spider_x" translatable="false">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key" translatable="false">সিক্রেট কী</string>
|
||||
<string name="server_lab_reserved">সংরক্ষিত (ঐচ্ছিক)</string>
|
||||
<string name="server_lab_local_address">স্থানীয় ঠিকানা (ঐচ্ছিক IPv4/IPv6, কমা দ্বারা পৃথক করা)</string>
|
||||
@@ -126,6 +122,7 @@
|
||||
<string name="title_user_asset_add_url">অ্যাসেট URL যোগ করুন</string>
|
||||
<string name="msg_file_not_found">ফাইল খুঁজে পাওয়া যায়নি</string>
|
||||
<string name="msg_remark_is_duplicate">মন্তব্য ইতিমধ্যে বিদ্যমান</string>
|
||||
<string name="asset_geo_files_sources">Geo files source (optional)</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">লোড হচ্ছে</string>
|
||||
@@ -143,6 +140,7 @@
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">সেটিংস</string>
|
||||
<string name="title_advanced">এডভান্সড সেটিংস</string>
|
||||
<string name="title_core_settings">কোর সেটিংস</string>
|
||||
<string name="title_vpn_settings">VPN সেটিংস</string>
|
||||
<string name="title_pref_per_app_proxy">প্রতি-অ্যাপ প্রক্সি</string>
|
||||
<string name="summary_pref_per_app_proxy">সাধারণ: চেকড অ্যাপ প্রক্সি, আনচেকড সরাসরি সংযোগ; \nবাইপাস মোড: চেকড অ্যাপ সরাসরি সংযুক্ত, আনচেকড প্রক্সি। \nমেনুতে প্রক্সি অ্যাপ্লিকেশন স্বয়ংক্রিয়ভাবে নির্বাচন করার বিকল্প</string>
|
||||
@@ -185,6 +183,8 @@
|
||||
<string name="title_pref_vpn_dns">VPN DNS (শুধুমাত্র IPv4/v6)</string>
|
||||
<string name="title_pref_vpn_bypass_lan">Does VPN bypass LAN</string>
|
||||
|
||||
<string name="title_pref_vpn_interface_address">VPN Interface Address</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">ঘরোয়া DNS (ঐচ্ছিক)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
@@ -242,6 +242,7 @@
|
||||
<string name="title_pref_auto_update_interval">অটো আপডেট ইন্টারভ্যাল (মিনিট, সর্বনিম্ন মান ১৫)</string>
|
||||
|
||||
<string name="title_core_loglevel">লগ স্তর</string>
|
||||
<string name="title_outbound_domain_resolve_method">Outbound domain pre-resolve method</string>
|
||||
<string name="title_mode">মোড</string>
|
||||
<string name="title_mode_help">আরো সাহায্যের জন্য ক্লিক করুন</string>
|
||||
<string name="title_language">ভাষা</string>
|
||||
@@ -262,12 +263,14 @@
|
||||
<string name="sub_setting_filter">Remarks regular filter</string>
|
||||
<string name="sub_setting_enable">আপডেট সক্রিয় করুন</string>
|
||||
<string name="sub_auto_update">স্বয়ংক্রিয় আপডেট সক্রিয় করুন</string>
|
||||
<string name="sub_setting_pre_profile">Previous proxy remarks</string>
|
||||
<string name="sub_setting_next_profile">Next proxy remarks</string>
|
||||
<string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string>
|
||||
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
|
||||
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
|
||||
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
|
||||
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||
<string name="title_sub_update">সাবস্ক্রিপশন আপডেট</string>
|
||||
<string name="title_ping_all_server">সব কনফিগারেশন TCPing</string>
|
||||
<string name="title_real_ping_all_server">সব কনফিগারেশন প্রকৃত বিলম্ব</string>
|
||||
<string name="title_create_intelligent_selection_all_server">Creating Intelligent Selection Current Group Configuration</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">টেস্ট ফলাফল দ্বারা সাজানো</string>
|
||||
<string name="title_filter_config">কনফিগারেশন ফাইল ফিল্টার করুন</string>
|
||||
@@ -313,6 +316,13 @@
|
||||
<string name="title_pref_fragment_interval">ফ্র্যাগমেন্ট ইন্টারভ্যাল (ন্যূনতম-সর্বাধিক)</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 name="update_checking_for_update">Checking for update…</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>QR কোড</item>
|
||||
<item>ক্লিপবোর্ডে রপ্তানি করুন</item>
|
||||
@@ -355,4 +365,17 @@
|
||||
<item>Not Bypass</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>Do not resolve</item>
|
||||
<item>Resolve and add to DNS Hosts</item>
|
||||
<item>Resolve and replace domain</item>
|
||||
</string-array>
|
||||
<string name="intelligent_selection">Intelligent Selection</string>
|
||||
<string name="sub_setting_intelligent_selection_filter">Remarks Intelligent Selection regular filter</string>
|
||||
<string name="title_intelligent_selection_method">Intelligent Selection Method</string>
|
||||
<string-array name="intelligent_selection_method">
|
||||
<item>Least Ping</item>
|
||||
<item>Least Load</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
@@ -7,9 +7,9 @@
|
||||
<string name="navigation_drawer_close">بستن نومگه کشاری</string>
|
||||
<string name="migration_success">مووفقیت من جاگورویی داده</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="toast_permission_denied">گرؽڌن موجوز مومکن نؽڌ</string>
|
||||
<string name="toast_permission_denied_notification">گرؽڌن موجوز وارسۊوی مومکن نؽڌ</string>
|
||||
@@ -26,7 +26,7 @@
|
||||
<string name="menu_item_del_config">پاک کردن کانفیگ</string>
|
||||
<string name="menu_item_import_config_qrcode">و من ٱووردن کانفیگ ز QRcode</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_vless">هؽل دستی[VLESS]</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_wireguard">هؽل دستی[Wireguard]</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_invalid_config_comfirm">پؽش ز پاک کردن کانفیگ نا موئتبر واجۊری کوݩ! پاک کردن کانفیگن قوۊل اکۊنی؟</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_id">نوم من توری</string>
|
||||
<string name="server_lab_id">نوم منتوری</string>
|
||||
<string name="server_lab_alterid">شناسه جایگۊزین</string>
|
||||
<string name="server_lab_security">ٱمنیت</string>
|
||||
<string name="server_lab_network">شبکه</string>
|
||||
@@ -73,21 +68,22 @@
|
||||
<string name="server_lab_stream_alpn">Alpn</string>
|
||||
<string name="server_lab_allow_insecure">اجازه نا ٱمن</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_id3">رزم</string>
|
||||
<string name="server_lab_security3">ٱمنیت</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_flow">جریان</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_spider_x">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key">کیلیت سیخومی</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="toast_success">وا مووفقیت ٱنجوم وابی</string>
|
||||
<string name="toast_failure">شکست خرد</string>
|
||||
@@ -101,11 +97,11 @@
|
||||
<string name="server_lab_content">موئتوا</string>
|
||||
<string name="toast_none_data_clipboard">هیچ داده ای من کلیپ بورد وۊجۊڌ نڌاره</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="toast_malformed_josn">کانفیگ زبال نؽڌ</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_lab_port_hop">پورت گوم (درگا سرورن ز نۊ هؽل اکونه)</string>
|
||||
<string name="server_lab_port_hop_interval">فاسله پورت گوم (سانیه)</string>
|
||||
@@ -121,29 +117,31 @@
|
||||
<string name="menu_item_add_file">ازاف کردن فایل</string>
|
||||
<string name="menu_item_add_url">ازاف کردن لینگ</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="title_user_asset_add_url">آدرس اینترنتی دارایین ازاف کۊنین</string>
|
||||
<string name="title_user_asset_add_url">نشۊوی اینترنتی دارایین ازاف کۊنین</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 -->
|
||||
<string name="msg_dialog_progress">هون بار ونی بۊ</string>
|
||||
<string name="msg_dialog_progress">هونی بار ونی بۊ</string>
|
||||
<string name="menu_item_search">پیتینیڌن</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="menu_item_select_proxy_app">پسند خوتکار پروکسی برنومه</string>
|
||||
<string name="msg_downloading_content">موئتوا هونی دانلود ابۊن</string>
|
||||
<string name="menu_item_export_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_enable">Enable per-app</string>
|
||||
<string name="per_app_proxy_settings">سامووا ب تفکیک برنومه</string>
|
||||
<string name="per_app_proxy_settings_enable">ر وندن ب تفکیک برنومه</string>
|
||||
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">سامووا</string>
|
||||
<string name="title_advanced">سامووا پؽش رئڌه</string>
|
||||
<string name="title_core_settings">سامووا هسته</string>
|
||||
<string name="title_vpn_settings">سامووا VPN</string>
|
||||
<string name="title_pref_per_app_proxy">پروکسی و ری برنومه</string>
|
||||
<string name="summary_pref_per_app_proxy">پوی وولاتی: برنومه واجۊری بیڌه پروکسی هڌ، منپیز موستقیم بؽ نشووه هڌ. هالت دور زیڌن: برنومه نشووک ناڌه موستقیمن منپیز هڌ، پروکسی نشووک زیڌه نؽڌ. گۊزینه پسند خوتکار برنومه پروکسی من نومگه</string>
|
||||
@@ -152,32 +150,32 @@
|
||||
|
||||
<string name="title_mux_settings">سامووا Mux</string>
|
||||
<string name="title_pref_mux_enabled">ر وندن Mux</string>
|
||||
<string name="summary_pref_mux_enabled">زل تر، ٱما گاشڌ منپیز زی قت بۊ بارت دؽوۉداری، TCP، UDP و QUIC ن ای لم سفارشی کۊنین.</string>
|
||||
<string name="summary_pref_mux_enabled">زل تر، ٱما گاشڌ منپیز زی قت بۊ\nمخزن ترافیک TCP وا 8 منپیز پؽش فرز، بارت دؽوۉداری UDP وو QUIC ن ای لم سفارشی کۊنین.</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_quic">دؽوۉداری QUIC من تونل mux</string>
|
||||
<string name="title_pref_mux_xudp_quic">دؽوۉداری QUIC من تۊنل mux</string>
|
||||
<string-array name="mux_xudp_quic_entries">
|
||||
<item>رڌ کردن</item>
|
||||
<item>موجاز</item>
|
||||
<item>گوم زیڌن</item>
|
||||
</string-array>
|
||||
|
||||
<string name="title_pref_speed_enabled">ر وندن نشۉݩ داڌن سورعت</string>
|
||||
<string name="summary_pref_speed_enabled">نشۉݩ داڌن سورعت هیم سکویی من وارسۊویا. نماڌ وارسۊوی و ری و کار گرؽڌن آلشت ابۊ.</string>
|
||||
<string name="title_pref_speed_enabled">ر وندن نشووݩ داڌن سورعت</string>
|
||||
<string name="summary_pref_speed_enabled">نشووݩ داڌن سورعت هیم سکویی من وارسۊویا. نماڌ وارسۊوی و ری و کار گرؽڌن آلشت ابۊ.</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="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="summary_pref_local_dns_enabled">DNS پردازشت وابیڌه و دس هسته ماژول DNS (پؽشنهاڌ ابۊ، ٱر نیاز هڌ ک جوستن تور وو ولات ٱسلین دور زنی)</string>
|
||||
<string name="summary_pref_local_dns_enabled">درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین فعال بۊ)</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="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="summary_pref_remote_dns">DNS</string>
|
||||
@@ -185,17 +183,19 @@
|
||||
<string name="title_pref_vpn_dns">VPN DNS (تینا IPv4/v6)</string>
|
||||
<string name="title_pref_vpn_bypass_lan">VPN ز شبکه مهلی اگوڌرته؟</string>
|
||||
|
||||
<string name="title_pref_vpn_interface_address">نشۊوی رابت VPN</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">DNS منی (اختیاری)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_dns_hosts">DNS هاست موستقیم (قالوو: دامنه: آدرس،...)</string>
|
||||
<string name="summary_pref_dns_hosts">دامنه:آدرس،...</string>
|
||||
<string name="title_pref_dns_hosts">DNS هاست موستقیم (قالوو: دامنه: نشۊوی،...)</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="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="title_pref_allow_insecure">اجازه نا ٱمن</string>
|
||||
@@ -208,20 +208,20 @@
|
||||
<string name="summary_pref_local_dns_port">پورت DNS مهلی</string>
|
||||
|
||||
<string name="title_pref_confirm_remove">قوۊل کردن پاک کردن کانفیگ</string>
|
||||
<string name="summary_pref_confirm_remove">سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ</string>
|
||||
<string name="summary_pref_confirm_remove">سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ.</string>
|
||||
|
||||
<string name="title_pref_start_scan_immediate">زی اسکنن ر ون</string>
|
||||
<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="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="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="title_pref_double_column_display">ر وندن نشووݩ داڌن دو سۊتۊنی</string>
|
||||
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ر وستن، وا برنومه ن ز نۊ ر ونین.</string>
|
||||
|
||||
<!-- AboutActivity -->
|
||||
<string name="title_pref_feedback">فشناڌن منشڌ</string>
|
||||
<string name="summary_pref_feedback">فشناڌن منشڌ یا داسوو موشکلا من Github</string>
|
||||
<string name="summary_pref_feedback">فشناڌن منشڌ یا داسووݩ موشکلا من Github</string>
|
||||
<string name="summary_pref_tg_group">ٱووڌن من جرگه تلگرام</string>
|
||||
<string name="toast_tg_app_not_found">برنومه تلگرامن نجوست</string>
|
||||
<string name="title_privacy_policy">هریم سیخومی</string>
|
||||
@@ -242,9 +242,10 @@
|
||||
<string name="title_pref_auto_update_interval">فاسله ورۊ کردن خوتکار (اقلن وا 15 دؽقه بۊ)</string>
|
||||
|
||||
<string name="title_core_loglevel">سئت داسووا</string>
|
||||
<string name="title_outbound_domain_resolve_method">بارت پؽش هل دامنه دری</string>
|
||||
<string name="title_mode">هالت</string>
|
||||
<string name="title_mode_help">سی دووسمندیا وو هیاری بیشتر، ری ای هؽل بزݩ</string>
|
||||
<string name="title_language">زۉݩ</string>
|
||||
<string name="title_language">زووݩ</string>
|
||||
<string name="title_ui_settings">سامووا رابت منتوری</string>
|
||||
<string name="title_pref_ui_mode_night">سامووا هالت رابت منتوری</string>
|
||||
|
||||
@@ -262,16 +263,18 @@
|
||||
<string name="sub_setting_filter">نوم موستعار فیلتر</string>
|
||||
<string name="sub_setting_enable">فعال بیڌن ورۊ کردن</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_next_profile">نوم موستعار پروکسی نیایی</string>
|
||||
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</string>
|
||||
<string name="title_sub_update">ورۊ کردن اشتراک جرگه سکویی</string>
|
||||
<string name="title_ping_all_server">Tcping کانفیگا جرگه سکویی</string>
|
||||
<string name="title_real_ping_all_server">تئخیر واقعی کانفیگا جرگه سکویی</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">ترتیب و ری نتیجیل آزمایش</string>
|
||||
<string name="title_create_intelligent_selection_all_server">وورکل پسند هۊشمند کانفیگ جرگه سکویی</string>
|
||||
<string name="title_user_asset_setting">فایلا بونچک جوقرافیایی</string>
|
||||
<string name="title_sort_by_test_results">ترتیب و ری نتیجه یل آزمایش</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_config_count">پاک کردن %d کانفیگ</string>
|
||||
@@ -307,7 +310,7 @@
|
||||
<string name="connection_test_pending">منپیزن واجۊری کوݩ</string>
|
||||
<string name="connection_test_testing">هونی آزمایش ابۊ…</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_fail">اینترنت من دسرس نؽ</string>
|
||||
<string name="connection_test_error_status_code">کود ختا: #%d</string>
|
||||
@@ -316,11 +319,18 @@
|
||||
|
||||
<string name="import_subscription_success">اشتراک وا مووفقیت زفت زابی</string>
|
||||
<string name="import_subscription_failure">اشتراک زفت نوابی</string>
|
||||
<string name="title_fragment_settings">سامووا Fragment</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">ر وندن Fragment</string>
|
||||
<string name="title_fragment_settings">سامووا فرگمنت</string>
|
||||
<string name="title_pref_fragment_packets">کتنا فرگمنت</string>
|
||||
<string name="title_pref_fragment_length">تۊل کتنا فرگمنت (هدقل-هدکسر)</string>
|
||||
<string name="title_pref_fragment_interval">فاسله منجا کتنا فرگمنت (هدقل-هدکسر)</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 name="update_checking_for_update">ورۊ رسۊوی ن هونی واجۊری اکونه...</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>QRcode</item>
|
||||
@@ -330,10 +340,10 @@
|
||||
|
||||
<string-array name="share_method_more">
|
||||
<item>QRcode</item>
|
||||
<item>Export to clipboard</item>
|
||||
<item>Export full configuration to clipboard</item>
|
||||
<item>Edit</item>
|
||||
<item>Delete</item>
|
||||
<item>و در کشیڌن من کلیپ بورد</item>
|
||||
<item>و در کشیڌن پوی کانفیگ من کلیپ بورد</item>
|
||||
<item>آلشت</item>
|
||||
<item>پاک کردن</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="share_sub_method">
|
||||
@@ -365,4 +375,17 @@
|
||||
<item>دور زیڌه نبۊ</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>هل وو فسل مکۊنین</item>
|
||||
<item>هل وو ٱووردن و میزبووݩ یل دامنه DNS</item>
|
||||
<item>هل وو جایونی دامنه</item>
|
||||
</string-array>
|
||||
<string name="intelligent_selection">پسند هۊشمند</string>
|
||||
<string name="sub_setting_intelligent_selection_filter">گوڌنا دیاری پسند هۊشمند فیلتر مئمۊلی</string>
|
||||
<string name="title_intelligent_selection_method">بارت پسند هۊشمند</string>
|
||||
<string-array name="intelligent_selection_method">
|
||||
<item>بلم ترین پینگ</item>
|
||||
<item>هدقل بار</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
<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_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_invalid_config_comfirm">لطفا قبل از حذف کانفیگ نامعتبر بررسی کنید! حذف کانفیگ را تایید می کنید؟</string>
|
||||
<string name="server_lab_remarks">ملاحظات</string>
|
||||
@@ -85,6 +80,7 @@
|
||||
<string name="server_lab_preshared_key">کلید رمزگذاری اضافی (اختیاری)</string>
|
||||
<string name="server_lab_short_id">ShortID</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key">کلید خصوصی</string>
|
||||
<string name="server_lab_reserved">Reserved (اختیاری، جدا شده با کاما)</string>
|
||||
<string name="server_lab_local_address">آدرس محلی (IPv4/IPv6 اختیاری، جدا شده با کاما)</string>
|
||||
@@ -124,6 +120,7 @@
|
||||
<string name="title_user_asset_add_url">آدرس اینترنتی را اضافه کنید</string>
|
||||
<string name="msg_file_not_found">فایل پیدا نشد</string>
|
||||
<string name="msg_remark_is_duplicate">نام قبلاً وجود دارد</string>
|
||||
<string name="asset_geo_files_sources">منبع فایل های جغرافیایی (اختیاری)</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">بارگذاری</string>
|
||||
@@ -141,6 +138,7 @@
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">تنظیمات</string>
|
||||
<string name="title_advanced">تنظیمات پیشرفته</string>
|
||||
<string name="title_core_settings">تنظیمات هسته</string>
|
||||
<string name="title_vpn_settings">تنظیمات VPN</string>
|
||||
<string name="title_pref_per_app_proxy">پروکسی به تفکیک برنامه</string>
|
||||
<string name="summary_pref_per_app_proxy">عمومی: برنامه انتخاب شده از طریق یک پروکسی متصل می شود، برنامه انتخاب نشده مستقیماً متصل می شود. \nحالت دور زدن: برنامه انتخاب شده مستقیماً متصل می شود، برنامه انتخاب نشده از طریق یک پروکسی متصل می شود. \nانتخاب خودکار برنامه های پراکسی در منو امکان پذیر است.</string>
|
||||
@@ -175,7 +173,7 @@
|
||||
<string name="summary_pref_fake_dns_enabled">دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند)</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="summary_pref_remote_dns">DNS</string>
|
||||
@@ -183,6 +181,8 @@
|
||||
<string name="title_pref_vpn_dns">VPN DNS (فقط IPv4/v6)</string>
|
||||
<string name="title_pref_vpn_bypass_lan">آیا VPN از شبکه محلی عبور می کند؟</string>
|
||||
|
||||
<string name="title_pref_vpn_interface_address">VPN Interface Address</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">DNS داخلی (اختیاری)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
@@ -239,6 +239,7 @@
|
||||
<string name="summary_pref_auto_update_subscription">اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند.</string>
|
||||
<string name="title_pref_auto_update_interval">فاصله به روزرسانی خودکار ( حداقل مقدار ، 15 دقیقه )</string>
|
||||
<string name="title_core_loglevel">سطح گزارشات</string>
|
||||
<string name="title_outbound_domain_resolve_method">Outbound domain pre-resolve method</string>
|
||||
<string name="title_mode">حالت</string>
|
||||
<string name="title_mode_help">برای اطلاعات و راهنمایی بیشتر، روی این متن کلیک کنید</string>
|
||||
<string name="title_language">زبان</string>
|
||||
@@ -259,13 +260,15 @@
|
||||
<string name="sub_setting_filter">نام مستعار فیلتر</string>
|
||||
<string name="sub_setting_enable">فعال کردن بهروزرسانی</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_next_profile">نام مستعار پروکسی بعدی</string>
|
||||
<string name="sub_setting_pre_profile_tip">لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است</string>
|
||||
<string name="title_sub_update">بهروزرسانی گروه فعلی اشتراک</string>
|
||||
<string name="title_ping_all_server">TCPING کانفیگ های گروه فعلی</string>
|
||||
<string name="title_real_ping_all_server">تاخیر واقعی کانفیگ های گروه فعلی</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_create_intelligent_selection_all_server">Creating Intelligent Selection Current Group Configuration</string>
|
||||
<string name="title_user_asset_setting">فایل های منبع جغرافیایی</string>
|
||||
<string name="title_sort_by_test_results">مرتب سازی بر اساس نتایج آزمایش</string>
|
||||
<string name="title_filter_config">فیلتر کردن کانفیگها</string>
|
||||
<string name="filter_config_all">همه گروههای اشتراک</string>
|
||||
@@ -304,7 +307,7 @@
|
||||
<string name="connection_test_pending">اتصال را بررسی کنید</string>
|
||||
<string name="connection_test_testing">در حال آزمایش...</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_fail">اینترنت در دسترس نیست</string>
|
||||
<string name="connection_test_error_status_code">کد خطا: #%d</string>
|
||||
@@ -319,6 +322,13 @@
|
||||
<string name="title_pref_fragment_interval">فاصله بین بسته های فرگمنت (حداقل-حداکثر)</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 name="update_checking_for_update">در حال بررسی برای بهروزرسانی…</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>QRcode</item>
|
||||
<item>خروجی گرفتن در کلیپ بورد</item>
|
||||
@@ -364,4 +374,17 @@
|
||||
<item>دور زده نشود</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>Do not resolve</item>
|
||||
<item>Resolve and add to DNS Hosts</item>
|
||||
<item>Resolve and replace domain</item>
|
||||
</string-array>
|
||||
<string name="intelligent_selection">Intelligent Selection</string>
|
||||
<string name="sub_setting_intelligent_selection_filter">Remarks Intelligent Selection regular filter</string>
|
||||
<string name="title_intelligent_selection_method">Intelligent Selection Method</string>
|
||||
<string-array name="intelligent_selection_method">
|
||||
<item>Least Ping</item>
|
||||
<item>Least Load</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
<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_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_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string>
|
||||
<string name="server_lab_remarks">Название</string>
|
||||
@@ -85,6 +80,7 @@
|
||||
<string name="server_lab_preshared_key">Дополнительный ключ шифрования (необязательно)</string>
|
||||
<string name="server_lab_short_id">ShortID</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key">Закрытый ключ</string>
|
||||
<string name="server_lab_reserved">Reserved (необязательно, через запятую)</string>
|
||||
<string name="server_lab_local_address">Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
|
||||
@@ -126,6 +122,7 @@
|
||||
<string name="title_user_asset_add_url">Добавить URL ресурса</string>
|
||||
<string name="msg_file_not_found">Файл не найден</string>
|
||||
<string name="msg_remark_is_duplicate">Название уже существует</string>
|
||||
<string name="asset_geo_files_sources">Источник геофайлов (необязательно)</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">Загрузка…</string>
|
||||
@@ -143,6 +140,7 @@
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">Настройки</string>
|
||||
<string name="title_advanced">Расширенные настройки</string>
|
||||
<string name="title_core_settings">Настройки ядра</string>
|
||||
<string name="title_vpn_settings">Настройки VPN</string>
|
||||
<string name="title_pref_per_app_proxy">Прокси для выбранных приложений</string>
|
||||
<string name="summary_pref_per_app_proxy">Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string>
|
||||
@@ -156,9 +154,9 @@
|
||||
<string name="title_pref_mux_xudp_concurency">XUDP-соединения (диапазон от 1 до 1024)</string>
|
||||
<string name="title_pref_mux_xudp_quic">Обработка QUIC в мультиплексном туннеле</string>
|
||||
<string-array name="mux_xudp_quic_entries">
|
||||
<item>отклонять</item>
|
||||
<item>разрешать</item>
|
||||
<item>пропускать</item>
|
||||
<item>Отклонять</item>
|
||||
<item>Разрешать</item>
|
||||
<item>Пропускать</item>
|
||||
</string-array>
|
||||
|
||||
<string name="title_pref_speed_enabled">Отображение скорости</string>
|
||||
@@ -176,13 +174,15 @@
|
||||
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</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="summary_pref_remote_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_vpn_dns">VPN DNS (только IPv4/v6)</string>
|
||||
<string name="title_pref_vpn_bypass_lan">VPN пропускает LAN</string>
|
||||
<string name="title_pref_vpn_bypass_lan">VPN обходит LAN</string>
|
||||
|
||||
<string name="title_pref_vpn_interface_address">VPN частный IP</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">Внутренняя DNS (необязательно)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
@@ -241,6 +241,7 @@
|
||||
<string name="title_pref_auto_update_interval">Интервал автообновления (минут, не менее 15)</string>
|
||||
|
||||
<string name="title_core_loglevel">Подробность ведения журнала</string>
|
||||
<string name="title_outbound_domain_resolve_method">Предварительное определение исходящего домена</string>
|
||||
<string name="title_mode">Режим</string>
|
||||
<string name="title_mode_help">Нажмите для получения дополнительной информации</string>
|
||||
<string name="title_language">Язык</string>
|
||||
@@ -261,14 +262,16 @@
|
||||
<string name="sub_setting_filter">Название фильтра</string>
|
||||
<string name="sub_setting_enable">Использовать обновление</string>
|
||||
<string name="sub_auto_update">Использовать автообновление</string>
|
||||
<string name="sub_setting_pre_profile">Название предыдущего прокси</string>
|
||||
<string name="sub_setting_next_profile">Название следующего прокси</string>
|
||||
<string name="sub_setting_pre_profile_tip">Название должно существовать и быть уникальным</string>
|
||||
<string name="sub_allow_insecure_url">Разрешать незащищённые HTTP-адреса</string>
|
||||
<string name="sub_setting_pre_profile">Предыдущая конфигурация прокси</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_ping_all_server">Проверка профилей группы</string>
|
||||
<string name="title_ping_all_server">Проверить профили группы</string>
|
||||
<string name="title_real_ping_all_server">Время отклика профилей группы</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">Сортировка по результатам теста</string>
|
||||
<string name="title_create_intelligent_selection_all_server">Создать конфигурацию умного выбора</string>
|
||||
<string name="title_user_asset_setting">Файлы ресурсов</string>
|
||||
<string name="title_sort_by_test_results">Сортировать по результатам теста</string>
|
||||
<string name="title_filter_config">Фильтр групп</string>
|
||||
<string name="filter_config_all">Все группы</string>
|
||||
<string name="title_del_duplicate_config_count">Удалено дубликатов профилей: %d</string>
|
||||
@@ -306,7 +309,7 @@
|
||||
<string name="connection_test_pending">Проверить подключение</string>
|
||||
<string name="connection_test_testing">Проверка…</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_fail">Интернет недоступен</string>
|
||||
<string name="connection_test_error_status_code">Код ошибки: #%d</string>
|
||||
@@ -321,6 +324,13 @@
|
||||
<string name="title_pref_fragment_interval">Интервал фрагментов (от - до)</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 name="update_checking_for_update">Проверка обновления…</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>QR-код</item>
|
||||
<item>Экспорт в буфер обмена</item>
|
||||
@@ -360,8 +370,21 @@
|
||||
|
||||
<string-array name="vpn_bypass_lan">
|
||||
<item>Как в профиле</item>
|
||||
<item>Пропускает</item>
|
||||
<item>Не пропускает</item>
|
||||
<item>Обходит</item>
|
||||
<item>Не обходит</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>Не определять</item>
|
||||
<item>Определять и добавлять к узлам DNS</item>
|
||||
<item>Определять и заменять домен</item>
|
||||
</string-array>
|
||||
<string name="intelligent_selection">Умный выбор</string>
|
||||
<string name="sub_setting_intelligent_selection_filter">Название фильтра умного выбора</string>
|
||||
<string name="title_intelligent_selection_method">Принцип умного выбора</string>
|
||||
<string-array name="intelligent_selection_method">
|
||||
<item>Наименьшая задержка</item>
|
||||
<item>Наименьшая нагрузка</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -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_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_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_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||
<string name="server_lab_remarks">Tên cấu hình</string>
|
||||
@@ -85,6 +80,7 @@
|
||||
<string name="server_lab_preshared_key">PreSharedKey(optional)</string>
|
||||
<string name="server_lab_short_id">ShortId</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key">SecretKey</string>
|
||||
<string name="server_lab_reserved">Reserved (Không bắt buộc)</string>
|
||||
<string name="server_lab_local_address">Địa chỉ cục bộ (IPv4 / IPv6, phân cách bằng dấu phẩy)</string>
|
||||
@@ -124,6 +120,7 @@
|
||||
<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_remark_is_duplicate">Nhận xét đã tồn tại!</string>
|
||||
<string name="asset_geo_files_sources">Geo files source (optional)</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">Đang tải...</string>
|
||||
@@ -142,6 +139,7 @@
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">Cài đặt</string>
|
||||
<string name="title_advanced">Cài đặt nâng cao</string>
|
||||
<string name="title_core_settings">Cài đặt lõi</string>
|
||||
<string name="title_vpn_settings">Cài đặt VPN</string>
|
||||
<string name="title_pref_per_app_proxy">Proxy theo Ứng dụng</string>
|
||||
<string name="summary_pref_per_app_proxy">- Bình thường: Ứng dụng đã chọn sẽ kết nối thông qua Proxy, chưa chọn sẽ kết nối trực tiếp. \n- Chế độ Bypass: Ứng dụng đã chọn sẽ kết nối trực tiếp, chưa chọn sẽ kết nối qua Proxy. \n- Nếu bạn đang ở Trung Quốc thì vào Menu, chọn Tự động chọn ứng dụng Proxy.</string>
|
||||
@@ -185,6 +183,8 @@
|
||||
<string name="title_pref_vpn_dns">VPN DNS (Chỉ IPv4 / IPv6)</string>
|
||||
<string name="title_pref_vpn_bypass_lan">Does VPN bypass LAN</string>
|
||||
|
||||
<string name="title_pref_vpn_interface_address">VPN Interface Address</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">DNS nội địa (Không bắt buộc)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
@@ -242,6 +242,7 @@
|
||||
<string name="title_pref_auto_update_interval">Thời gian cập nhật tự động (Phút, giá trị tối thiểu là 15)</string>
|
||||
|
||||
<string name="title_core_loglevel">Cấp độ nhật ký</string>
|
||||
<string name="title_outbound_domain_resolve_method">Outbound domain pre-resolve method</string>
|
||||
<string name="title_mode">Chế độ kết nối</string>
|
||||
<string name="title_mode_help">Nhấn vào đây nếu bạn cần trợ giúp!</string>
|
||||
<string name="title_language">Ngôn ngữ</string>
|
||||
@@ -262,12 +263,14 @@
|
||||
<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_auto_update">Bật tự động cập nhật</string>
|
||||
<string name="sub_setting_pre_profile">Previous proxy remarks</string>
|
||||
<string name="sub_setting_next_profile">Next proxy remarks</string>
|
||||
<string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string>
|
||||
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
|
||||
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
|
||||
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
|
||||
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||
<string name="title_sub_update">Cập nhật các gói đăng ký</string>
|
||||
<string name="title_ping_all_server">Ping tất cả máy chủ</string>
|
||||
<string name="title_real_ping_all_server">Kiểm tra HTTP tất cả máy chủ</string>
|
||||
<string name="title_create_intelligent_selection_all_server">Creating Intelligent Selection Current Group Configuration</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">Sắp xếp lại theo lần kiểm tra cuối cùng</string>
|
||||
<string name="title_filter_config">Lọc cấu hình theo các gói đăng ký</string>
|
||||
@@ -309,6 +312,19 @@
|
||||
<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="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 name="update_checking_for_update">Checking for update…</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>Xuất ra mã QR (Chụp màn hình để lưu)</item>
|
||||
<item>Sao chép vào Clipboard</item>
|
||||
@@ -340,16 +356,23 @@
|
||||
<item>Sáng</item>
|
||||
<item>Tối</item>
|
||||
</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">
|
||||
<item>Follow config</item>
|
||||
<item>Bypass</item>
|
||||
<item>Not Bypass</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>Do not resolve</item>
|
||||
<item>Resolve and add to DNS Hosts</item>
|
||||
<item>Resolve and replace domain</item>
|
||||
</string-array>
|
||||
<string name="intelligent_selection">Intelligent Selection</string>
|
||||
<string name="sub_setting_intelligent_selection_filter">Remarks Intelligent Selection regular filter</string>
|
||||
<string name="title_intelligent_selection_method">Intelligent Selection Method</string>
|
||||
<string-array name="intelligent_selection_method">
|
||||
<item>Least Ping</item>
|
||||
<item>Least Load</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
<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_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_invalid_config_comfirm">删除前请先测试!确认删除?</string>
|
||||
<string name="server_lab_remarks">别名 (remarks)</string>
|
||||
@@ -85,6 +80,7 @@
|
||||
<string name="server_lab_preshared_key">PreSharedKey (optional)</string>
|
||||
<string name="server_lab_short_id">ShortId</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key">SecretKey</string>
|
||||
<string name="server_lab_reserved">Reserved (可选,逗号隔开)</string>
|
||||
<string name="server_lab_local_address">本地地址 (可选 IPv4/IPv6,逗号隔开)</string>
|
||||
@@ -124,6 +120,7 @@
|
||||
<string name="title_user_asset_add_url">添加资产网址</string>
|
||||
<string name="msg_file_not_found">文件未找到</string>
|
||||
<string name="msg_remark_is_duplicate">备注已经存在</string>
|
||||
<string name="asset_geo_files_sources">Geo 文件来源 (可选)</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">正在加载</string>
|
||||
@@ -141,6 +138,7 @@
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">设置</string>
|
||||
<string name="title_advanced">进阶设置</string>
|
||||
<string name="title_core_settings">核心设置</string>
|
||||
<string name="title_vpn_settings">VPN 设置</string>
|
||||
<string name="title_pref_per_app_proxy">分应用</string>
|
||||
<string name="summary_pref_per_app_proxy">常规: 勾选的 App 被代理, 未勾选的直连;\n绕行模式: 勾选的 App 直连, 未勾选的被代理.\n不明白者在菜单中选择自动选中需代理应用</string>
|
||||
@@ -182,6 +180,8 @@
|
||||
<string name="title_pref_vpn_dns">VPN DNS (仅支持 IPv4/v6)</string>
|
||||
<string name="title_pref_vpn_bypass_lan">VPN 是否绕过局域网</string>
|
||||
|
||||
<string name="title_pref_vpn_interface_address">VPN 接口地址</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">境内 DNS (可选)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
@@ -239,6 +239,7 @@
|
||||
<string name="title_pref_auto_update_interval">自动更新间隔(分钟,最小值 15)</string>
|
||||
|
||||
<string name="title_core_loglevel">日志级别</string>
|
||||
<string name="title_outbound_domain_resolve_method">Outbound 域名预解析方式</string>
|
||||
<string name="title_mode">模式</string>
|
||||
<string name="title_mode_help">点此查看更多帮助</string>
|
||||
<string name="title_language">语言</string>
|
||||
@@ -259,12 +260,14 @@
|
||||
<string name="sub_setting_filter">别名正则过滤</string>
|
||||
<string name="sub_setting_enable">启用更新</string>
|
||||
<string name="sub_auto_update">启用自动更新</string>
|
||||
<string name="sub_setting_pre_profile">前置代理别名</string>
|
||||
<string name="sub_setting_next_profile">落地代理別名</string>
|
||||
<string name="sub_setting_pre_profile_tip">请确保别名存在并唯一</string>
|
||||
<string name="sub_allow_insecure_url">允许不安全的 HTTP 地址</string>
|
||||
<string name="sub_setting_pre_profile">前置代理配置文件别名</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_ping_all_server">测试当前组配置 Tcping</string>
|
||||
<string name="title_real_ping_all_server">测试当前组配置真连接</string>
|
||||
<string name="title_create_intelligent_selection_all_server">生成当前组智能选择配置</string>
|
||||
<string name="title_user_asset_setting">资源文件</string>
|
||||
<string name="title_sort_by_test_results">按测试结果排序</string>
|
||||
<string name="title_filter_config">过滤配置文件</string>
|
||||
@@ -313,6 +316,13 @@
|
||||
<string name="title_pref_fragment_interval">分片间隔(最小 - 最大)</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 name="update_checking_for_update">正在检查更新中…</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>二维码</item>
|
||||
<item>导出至剪贴板</item>
|
||||
@@ -356,4 +366,17 @@
|
||||
<item>不绕过</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>不解析</item>
|
||||
<item>解析后添加至 DNS Hosts</item>
|
||||
<item>解析后替换原域名</item>
|
||||
</string-array>
|
||||
<string name="intelligent_selection">智能选择</string>
|
||||
<string name="sub_setting_intelligent_selection_filter">别名智能选择正则过滤</string>
|
||||
<string name="title_intelligent_selection_method">智能选择方式</string>
|
||||
<string-array name="intelligent_selection_method">
|
||||
<item>最低延迟</item>
|
||||
<item>最稳定</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user