mirror of
https://github.com/2dust/v2rayNG.git
synced 2026-10-29 22:49:23 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15b4fff8e4 | ||
|
|
1007f54743 | ||
|
|
5369303e18 | ||
|
|
318afdbafc | ||
|
|
ec04df4693 | ||
|
|
53cccd9089 | ||
|
|
f226ace30d | ||
|
|
10a4221b25 | ||
|
|
ffac1476c0 | ||
|
|
7a0472b63d | ||
|
|
62cecc4d0d | ||
|
|
eb56b2e89e | ||
|
|
e83dba94a0 | ||
|
|
42c12fdef4 | ||
|
|
226b36903e | ||
|
|
de351d4438 | ||
|
|
5436d67e47 | ||
|
|
1c3d247071 | ||
|
|
d3e1e7c6fe | ||
|
|
a65c57ea0d | ||
|
|
8586c2550f | ||
|
|
fa6c4807f3 | ||
|
|
6e65f46e3f | ||
|
|
98e7b8e1a1 | ||
|
|
7dd42528db | ||
|
|
e580b76e9e | ||
|
|
c4d02c00ce | ||
|
|
007a4e674f | ||
|
|
46af9d803c | ||
|
|
32a9263e0a | ||
|
|
253e171fec | ||
|
|
31b66a705b | ||
|
|
23c7cd5b95 | ||
|
|
32a37dd626 | ||
|
|
53e84d5fd8 | ||
|
|
0519d02001 | ||
|
|
6dfed5bbc2 | ||
|
|
dcd060e350 | ||
|
|
71f94db929 | ||
|
|
d06b9cb999 | ||
|
|
f53a91241c | ||
|
|
3fb067dc99 | ||
|
|
02103287e5 | ||
|
|
b103e3aec9 | ||
|
|
19fc65d128 | ||
|
|
27cafd1052 | ||
|
|
a155b24c1e | ||
|
|
e805fcf088 | ||
|
|
95e37db42e | ||
|
|
fe812c4456 | ||
|
|
d5af0fe735 | ||
|
|
9126aa9c9e | ||
|
|
4a763d2ec5 | ||
|
|
08d6de4504 | ||
|
|
629d410189 | ||
|
|
39d2513df1 | ||
|
|
3cb4df474b | ||
|
|
b64bb92c8c | ||
|
|
7f0e2f801d | ||
|
|
0f7cee0613 | ||
|
|
b2e10fc60b | ||
|
|
aa3f17fb51 | ||
|
|
d64fc627bb | ||
|
|
6934ab3e5d | ||
|
|
028d13aef8 | ||
|
|
4a78c165bc | ||
|
|
c1033a006f | ||
|
|
e8a42013c0 | ||
|
|
94e5263250 | ||
|
|
7c68aa9d83 | ||
|
|
ef13e336da | ||
|
|
d16cb21059 | ||
|
|
237837e7be | ||
|
|
2a6edd98be | ||
|
|
9238b80675 | ||
|
|
bb5e6f1959 | ||
|
|
1cc0865411 | ||
|
|
cb8d979f23 | ||
|
|
5932b0db29 | ||
|
|
dbe4fe78b5 | ||
|
|
b5bcd81ae4 | ||
|
|
d45ccd7953 | ||
|
|
6815563902 | ||
|
|
7c38ad7c57 | ||
|
|
885aeb384e |
No files matched your search
+43
-17
@@ -10,37 +10,40 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v4.0.1
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
log-accepted-android-sdk-licenses: false
|
||||
cmdline-tools-version: '13114758'
|
||||
packages: 'platforms;android-36.1 build-tools;36.1.0 platform-tools'
|
||||
cmdline-tools-version: '14742923'
|
||||
packages: 'platforms;android-37.0 build-tools;37.0.0 platform-tools'
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
|
||||
--channel=0 \
|
||||
--install "ndk;28.2.13676358"
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358" >> $GITHUB_ENV
|
||||
--install "ndk;29.0.14206865"
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.14206865" >> $GITHUB_ENV
|
||||
sed -i '10i\
|
||||
\
|
||||
ndkVersion = "28.2.13676358"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
|
||||
ndkVersion = "29.0.14206865"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
|
||||
|
||||
- name: Restore cached libhevtun
|
||||
id: cache-libhevtun-restore
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/libs
|
||||
key: libhevtun-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hev-socks5-tunnel/HEAD') }}-${{ hashFiles('compile-hevtun.sh') }}
|
||||
@@ -52,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Save libhevtun
|
||||
if: steps.cache-libhevtun-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/libs
|
||||
key: libhevtun-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hev-socks5-tunnel/HEAD') }}-${{ hashFiles('compile-hevtun.sh') }}
|
||||
@@ -119,11 +122,34 @@ jobs:
|
||||
name: x86-apk
|
||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*x86*.apk
|
||||
|
||||
- name: Upload to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
if: github.event.inputs.release_tag != ''
|
||||
with:
|
||||
file: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*.apk
|
||||
tag: ${{ github.event.inputs.release_tag }}
|
||||
file_glob: true
|
||||
prerelease: true
|
||||
sign-and-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-26.04
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
|
||||
steps:
|
||||
- name: Download APK artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: release
|
||||
|
||||
- name: Sign APK files
|
||||
run: |
|
||||
printf '%s' "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import
|
||||
KEY=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/ {print $10; exit}')
|
||||
mkdir -p release-files
|
||||
|
||||
find release -type f -name '*.apk' | while read apk; do
|
||||
cp "$apk" release-files/
|
||||
gpg --batch --yes --local-user "$KEY" --detach-sign --output "release-files/$(basename "$apk").sig" "$apk"
|
||||
done
|
||||
|
||||
gpg --armor --export "$KEY" > release-files/v2rayN-public-key.asc
|
||||
|
||||
- name: Upload APKs, signatures and public key to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
file: release-files/*
|
||||
tag: ${{ inputs.release_tag }}
|
||||
file_glob: true
|
||||
prerelease: true
|
||||
@@ -11,6 +11,6 @@ jobs:
|
||||
go:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Validate Fastlane Supply Metadata
|
||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0
|
||||
+1
-1
Submodule AndroidLibXrayLite updated: 1b0ec8e111...3b5a9c858c.
@@ -3,30 +3,88 @@
|
||||
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)
|
||||
|
||||
### Telegram Channel
|
||||
[github_2dust](https://t.me/github_2dust)
|
||||
---
|
||||
|
||||
### Usage
|
||||
## Download / 下载
|
||||
|
||||
Download the latest release here:
|
||||
|
||||
在这里下载最新版本:
|
||||
|
||||
[https://github.com/2dust/v2rayNG/releases](https://github.com/2dust/v2rayNG/releases)
|
||||
|
||||
> [!TIP]
|
||||
> v2rayNG is the mobile version. For the desktop version, please visit the v2rayN \
|
||||
> v2rayNG 是手机版,电脑版请访问 v2rayN
|
||||
>
|
||||
> https://github.com/2dust/v2rayN
|
||||
|
||||
---
|
||||
|
||||
### Geoip and Geosite
|
||||
|
||||
#### 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)
|
||||
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (note: it needs a working proxy)
|
||||
- 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)
|
||||
- possible to use a 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)
|
||||
More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||
|
||||
### Development guide
|
||||
### Geoip 与 Geosite
|
||||
|
||||
Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
|
||||
The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite).
|
||||
For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile) and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
|
||||
- geoip.dat 和 geosite.dat 文件位于 `Android/data/com.v2ray.ang/files/assets`(部分设备路径可能不同)
|
||||
- 下载功能将获取该 [仓库](https://github.com/Loyalsoldier/v2ray-rules-dat) 中的增强版本(注意:此功能需要一个可用的代理)
|
||||
- 最新官方 [域名列表](https://github.com/Loyalsoldier/v2ray-rules-dat) 和 [IP 列表](https://github.com/Loyalsoldier/geoip) 可手动导入
|
||||
- 也可在同一文件夹中使用第三方 dat 文件,例如 [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)
|
||||
|
||||
v2rayNG can run on Android Emulators. For WSA, VPN permission need to be granted via
|
||||
`appops set [package name] ACTIVATE_VPN allow`
|
||||
更多内容请见我们的 [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||
|
||||
---
|
||||
|
||||
## Development guide / 开发指南
|
||||
|
||||
### Note
|
||||
|
||||
- Android project under the V2rayNG folder can be compiled directly in Android Studio, or using the Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
|
||||
- The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite). For a quick start, read the guides for [Go Mobile](https://github.com/golang/go/wiki/Mobile) and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/).
|
||||
- v2rayNG can run on Android Emulators. For WSA, VPN permission needs to be granted via `appops set [package name] ACTIVATE_VPN allow`.
|
||||
|
||||
### 提示
|
||||
|
||||
- V2rayNG 文件夹下的 Android 项目可直接在 Android Studio 中编译,或使用 Gradle wrapper 编译。但 aar 内置的 v2ray core(可能)已过时。
|
||||
- aar 可由 Golang 项目 [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) 或 [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite) 编译而成。快速入门可参考 [Go Mobile](https://github.com/golang/go/wiki/Mobile) 指南和 [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)。
|
||||
- v2rayNG 可在 Android 模拟器上运行。对于 WSA,需要通过 `appops set [package name] ACTIVATE_VPN allow` 授予 VPN 权限。
|
||||
|
||||
---
|
||||
|
||||
|
||||
## GPG Verification / GPG 签名校验
|
||||
|
||||
Release files are signed with GPG to verify authenticity and integrity, helping prevent mirror, ISP, or CDN hijacking.
|
||||
|
||||
发布文件已使用 GPG 签名,可用于校验文件真实性与完整性,预防镜像站、运营商或 CDN 劫持。
|
||||
|
||||
### Fingerprint / 公钥指纹
|
||||
|
||||
```text
|
||||
7694 5E9F 3E9A 168F 8070 F195 805D 661C
|
||||
134D FAF6 8903 C199 463C 31E5 AE90 3AE0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Community / 社区
|
||||
|
||||
Telegram Group / Telegram 群组:
|
||||
|
||||
[https://t.me/v2rayN](https://t.me/v2rayN)
|
||||
|
||||
Telegram Channel / Telegram 频道:
|
||||
|
||||
[https://t.me/github_2dust](https://t.me/github_2dust)
|
||||
@@ -6,14 +6,14 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "com.v2ray.ang"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 726
|
||||
versionName = "2.1.6"
|
||||
targetSdk = 37
|
||||
versionCode = 736
|
||||
versionName = "2.2.6"
|
||||
multiDexEnabled = true
|
||||
|
||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||
@@ -21,7 +21,7 @@ android {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
if (abiFilterList != null && abiFilterList.isNotEmpty()) {
|
||||
if (!abiFilterList.isNullOrEmpty()) {
|
||||
include(*abiFilterList.toTypedArray())
|
||||
} else {
|
||||
include(
|
||||
|
||||
@@ -29,16 +29,16 @@
|
||||
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"
|
||||
tools:ignore="ForegroundServicesPolicy" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
android:minSdkVersion="34" />
|
||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
@@ -202,6 +202,17 @@
|
||||
android:value="proxy" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.CoreRootService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:label="@string/app_name"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="proxy" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.CoreTestService"
|
||||
android:exported="false"
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -73,8 +74,10 @@
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"domainStrategy": "UseIP"
|
||||
"streamSettings": {
|
||||
"sockopt": {
|
||||
"domainStrategy": "UseIP"
|
||||
}
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -46,7 +47,8 @@
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -89,8 +91,10 @@
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"domainStrategy": "UseIP"
|
||||
"streamSettings": {
|
||||
"sockopt": {
|
||||
"domainStrategy": "UseIP"
|
||||
}
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@ object AppConfig {
|
||||
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
|
||||
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
|
||||
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
|
||||
const val PREF_FRAGMENT_MAXSPLIT = "pref_fragment_maxsplit"
|
||||
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
|
||||
const val SUBSCRIPTION_MIN_INTERVAL_MINUTES = 15L
|
||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
||||
@@ -55,7 +56,6 @@ object AppConfig {
|
||||
const val PREF_IPV6_ENABLED = "pref_ipv6_enabled"
|
||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
||||
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
||||
const val PREF_ENABLE_LOCAL_PROXY = "pref_enable_local_proxy"
|
||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||
const val PREF_DYNAMIC_SOCKS_PORT = "pref_dynamic_socks_port"
|
||||
@@ -70,6 +70,8 @@ object AppConfig {
|
||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||
const val PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method"
|
||||
const val PREF_MODE = "pref_mode"
|
||||
const val PREF_ROOT_MODE_ENABLE = "pref_root_mode_enabled"
|
||||
const val PREF_ROOT_LAN_SHARING = "pref_root_lan_sharing"
|
||||
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"
|
||||
@@ -105,7 +107,8 @@ object AppConfig {
|
||||
const val TAG_FRAGMENT = "fragment"
|
||||
const val TAG_DNS = "dns-module"
|
||||
const val TAG_DOMESTIC_DNS = "domestic-dns"
|
||||
const val TAG_BALANCER = "proxy-round"
|
||||
const val TAG_BALANCER = "balancer-main"
|
||||
const val TAG_BALANCER_PRE = "balancer"
|
||||
|
||||
/** Network-related constants. */
|
||||
const val UPLINK = "uplink"
|
||||
@@ -180,6 +183,8 @@ object AppConfig {
|
||||
const val CUSTOM = ""
|
||||
const val SHADOWSOCKS = "ss://"
|
||||
const val SOCKS = "socks://"
|
||||
const val SOCKS4 = "socks4://"
|
||||
const val SOCKS5 = "socks5://"
|
||||
const val HTTP = "http://"
|
||||
const val VLESS = "vless://"
|
||||
const val TROJAN = "trojan://"
|
||||
@@ -193,6 +198,28 @@ object AppConfig {
|
||||
const val VPN = "VPN"
|
||||
const val VPN_MTU = 1500
|
||||
|
||||
/** Root (system-wide) mode runtime constants. */
|
||||
const val ROOT_RUNTIME_DIR = "root"
|
||||
const val ROOT_IPTABLES_CHAIN = "V2RAY_NG"
|
||||
const val ROOT_FWMARK = 255 // defensive RETURN tag; hev's only upstream socket is loopback (already bypassed)
|
||||
const val ROOT_MARK_ROUTE = 1 // packets we want pushed into the tun device
|
||||
const val ROOT_ROUTE_TABLE = 2024
|
||||
const val ROOT_RULE_PRIORITY = 1000
|
||||
const val ROOT_TUN_NAME = "v2raytun0"
|
||||
const val ROOT_TUN_ADDR_V4 = "198.18.0.1/15"
|
||||
const val ROOT_TUN_ADDR_V6 = "fdfe:dcba:9876::1/64"
|
||||
|
||||
// hev-socks5-tunnel run as a standalone root binary (reuses the same project already
|
||||
// bundled for the VPN hev path; distinct filename from the JNI lib to avoid collision).
|
||||
const val ROOT_TUN2SOCKS_BIN = "libhevsockstun.so"
|
||||
const val ROOT_FWD_CHAIN = "V2RAY_NG_FWD" // FORWARD chain for LAN/tethering sharing
|
||||
const val ROOT_DNS_CHAIN = "V2RAY_NG_DNS" // nat chain for tethered-client DNS DNAT
|
||||
const val ROOT_V6_CHAIN = "V2RAY_NG6" // ip6tables filter/OUTPUT chain: blackhole native IPv6 when it isn't tunneled
|
||||
const val ROOT_V6_FWD_CHAIN = "V2RAY_NG6_FWD" // ip6tables FORWARD chain: route or reject tethered clients' native IPv6
|
||||
const val ROOT_V6_PRE_CHAIN = "V2RAY_NG6_PRE" // ip6tables mangle/PREROUTING chain: mark forwarded clients' IPv6 into the tun
|
||||
const val ROOT_LAN_DNS = "1.1.1.1" // fallback resolver for tethered clients when no plain-IPv4 DNS is configured
|
||||
const val ROOT_OOM_SCORE = "-1000" // oom_score_adj that makes the LMK never kill us
|
||||
|
||||
/** hev-sock5-tunnel read-write-timeout value */
|
||||
const val HEVTUN_RW_TIMEOUT = "300,60"
|
||||
|
||||
@@ -201,13 +228,19 @@ object AppConfig {
|
||||
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
|
||||
|
||||
// Android Private DNS constants
|
||||
const val DNS_DNSPOD_DOMAIN = "dot.pub"
|
||||
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
||||
const val DNS_CISCO_SSE_DOMAIN = "dns.sse.cisco.com"
|
||||
const val DNS_CISCO_UMBRELLA_DOMAIN = "dns.umbrella.com"
|
||||
const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one"
|
||||
const val DNS_CLOUDFLARE_ONEDOT_DNS_DOMAIN = "1dot1dot1dot1.cloudflare-dns.com"
|
||||
const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com"
|
||||
const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com"
|
||||
const val DNS_CLOUDFLARE_WARP_DOMAIN = "engage.cloudflareclient.com"
|
||||
const val DNS_DNSPOD_DOH_DOMAIN = "doh.pub"
|
||||
const val DNS_DNSPOD_DOT_DOMAIN = "dot.pub"
|
||||
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
||||
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
||||
const val DNS_SB_DOMAIN = "dns.sb"
|
||||
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
||||
|
||||
const val DEFAULT_PORT = 443
|
||||
@@ -221,12 +254,18 @@ object AppConfig {
|
||||
const val UNIDENTIFIED_PACKAGE = "__unknown_app__"
|
||||
|
||||
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
||||
val DNS_CISCO_SSE_ADDRESSES = arrayListOf("208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53")
|
||||
val DNS_CISCO_UMBRELLA_ADDRESSES = arrayListOf("208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53")
|
||||
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_ONEDOT_DNS_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("162.159.61.8", "172.64.41.8", "2a06:98c1:52::8", "2803:f800:53::8")
|
||||
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_CLOUDFLARE_WARP_ADDRESSES = arrayListOf("162.159.192.1", "2606:4700:d0::a29f:c001")
|
||||
val DNS_DNSPOD_DOH_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||
val DNS_DNSPOD_DOT_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_SB_ADDRESSES = arrayListOf("45.11.45.11", "185.222.222.222", "2a09::", "2a11::")
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.v2ray.ang.contracts
|
||||
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
|
||||
interface MainAdapterListener : BaseAdapterListener {
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ package com.v2ray.ang.core
|
||||
import android.content.Context
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.CoreConfigContext
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.CoreResolvedType
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
@@ -13,58 +14,130 @@ import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
/**
|
||||
* Builds [com.v2ray.ang.dto.CoreConfigContext] from the selected profile.
|
||||
* Keeps parsing and resolution logic out of [CoreConfigManager].
|
||||
* Build runtime context from the selected profile.
|
||||
*
|
||||
* All outbound type analysis is completed here for both the selected profile
|
||||
* and routing targets. Custom profiles are returned immediately without
|
||||
* entering the normal analysis flow.
|
||||
*/
|
||||
object CoreConfigContextBuilder {
|
||||
|
||||
/** Loads profile by guid and returns a resolved runtime context. */
|
||||
/**
|
||||
* Load one profile and produce a fully analyzed context.
|
||||
*
|
||||
* Null is returned only when the selected profile cannot be loaded.
|
||||
*/
|
||||
fun build(context: Context, guid: String): CoreConfigContext? {
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return null
|
||||
|
||||
// CUSTOM: return immediately — CoreConfigManager handles this path on its own.
|
||||
if (config.configType == EConfigType.CUSTOM) {
|
||||
return CoreConfigContext(
|
||||
context = context,
|
||||
guid = guid,
|
||||
selectedProfile = config,
|
||||
resolvedProfiles = listOf(config),
|
||||
resolvedType = CoreResolvedType.CUSTOM,
|
||||
)
|
||||
return CoreConfigContext(context = context, guid = guid, isCustom = true)
|
||||
}
|
||||
|
||||
// Pre-resolve custom outbound profiles from routing rulesets
|
||||
val customOutbounds = resolveCustomOutbounds()
|
||||
// Step 1: Resolve the main outbound (always tag = TAG_PROXY).
|
||||
val primaryResolvedOutbound = resolveOutbound(AppConfig.TAG_PROXY, config) ?: run {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve main outbound for '${config.remarks}'")
|
||||
return null
|
||||
}
|
||||
|
||||
// Determine resolved profiles and type based on config type
|
||||
val (resolvedProfiles, resolvedType) = when (config.configType) {
|
||||
EConfigType.POLICYGROUP -> {
|
||||
val profiles = resolvePolicyGroupProfiles(config)
|
||||
Pair(profiles, CoreResolvedType.POLICYGROUP)
|
||||
}
|
||||
// Step 2: Resolve all non-builtin routing outbound tags.
|
||||
val routingResolvedOutbounds = resolveRoutingOutbounds()
|
||||
val routingDomainRules = collectRoutingDomainRulesForDns()
|
||||
|
||||
return CoreConfigContext(
|
||||
context = context,
|
||||
guid = guid,
|
||||
resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds,
|
||||
routingDomainRules = routingDomainRules,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one outbound target into a normalized outbound entry.
|
||||
*
|
||||
* Custom profiles are ignored at this stage and produce no entry.
|
||||
*/
|
||||
private fun resolveOutbound(tag: String, profile: ProfileItem): CoreConfigContext.ResolvedOutbound? {
|
||||
if (profile.configType == EConfigType.CUSTOM) {
|
||||
return null
|
||||
}
|
||||
|
||||
val (resolvedProfiles, resolvedType) = when (profile.configType) {
|
||||
EConfigType.POLICYGROUP -> Pair(
|
||||
resolvePolicyGroupProfiles(profile),
|
||||
CoreResolvedType.POLICYGROUP,
|
||||
)
|
||||
|
||||
EConfigType.PROXYCHAIN -> {
|
||||
val profiles = resolveProxyChainProfiles(config)
|
||||
Pair(profiles, CoreResolvedType.PROXYCHAIN)
|
||||
val chainProfiles = resolveProxyChainProfiles(profile)
|
||||
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
|
||||
Pair(chainProfiles, type)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val chainProfiles = resolveProxyChainProfilesFromGroup(config)
|
||||
val chainProfiles = resolveProxyChainProfilesFromGroup(profile)
|
||||
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
|
||||
Pair(chainProfiles, type)
|
||||
}
|
||||
}
|
||||
|
||||
// Create context with common fields
|
||||
return CoreConfigContext(
|
||||
context = context,
|
||||
guid = guid,
|
||||
selectedProfile = config,
|
||||
return CoreConfigContext.ResolvedOutbound(
|
||||
tag = tag,
|
||||
profile = profile,
|
||||
resolvedProfiles = resolvedProfiles,
|
||||
resolvedType = resolvedType,
|
||||
customOutboundProfiles = customOutbounds,
|
||||
)
|
||||
}
|
||||
|
||||
/** Resolves policy-group members with the same filters as runtime build. */
|
||||
/**
|
||||
* Collect and resolve non-builtin routing targets from enabled rules.
|
||||
*
|
||||
* Invalid or empty targets are skipped and handled by fallback logic later.
|
||||
*/
|
||||
private fun resolveRoutingOutbounds(): List<CoreConfigContext.ResolvedOutbound> {
|
||||
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return emptyList()
|
||||
val resolvedOutbounds = mutableListOf<CoreConfigContext.ResolvedOutbound>()
|
||||
val processedTags = mutableSetOf<String>()
|
||||
|
||||
try {
|
||||
rulesetItems
|
||||
.filter { it.enabled }
|
||||
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
|
||||
.filter { tag -> tag !in AppConfig.BUILTIN_OUTBOUND_TAGS }
|
||||
.distinct()
|
||||
.forEach { tag ->
|
||||
if (tag in processedTags) {
|
||||
return@forEach
|
||||
}
|
||||
processedTags.add(tag)
|
||||
|
||||
try {
|
||||
val profile = SettingsManager.getServerViaRemarks(tag) ?: run {
|
||||
LogUtil.w(AppConfig.TAG, "Routing tag '$tag' has no matching profile — will fall back to proxy at routing time")
|
||||
return@forEach
|
||||
}
|
||||
val resolvedOutbound = resolveOutbound(tag, profile) ?: run {
|
||||
LogUtil.w(AppConfig.TAG, "Cannot use CUSTOM profile as routing outbound for tag '$tag', skipping")
|
||||
return@forEach
|
||||
}
|
||||
if (resolvedOutbound.resolvedProfiles.isEmpty()) {
|
||||
LogUtil.w(AppConfig.TAG, "Routing outbound '$tag' resolved to empty list, skipping")
|
||||
return@forEach
|
||||
}
|
||||
resolvedOutbounds.add(resolvedOutbound)
|
||||
LogUtil.d(AppConfig.TAG, "Resolved routing outbound: tag='$tag', type='${resolvedOutbound.resolvedType}', profiles=${resolvedOutbound.resolvedProfiles.size}")
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve routing outbound for tag '$tag', skipping", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve routing outbounds from rulesets", e)
|
||||
}
|
||||
|
||||
return resolvedOutbounds
|
||||
}
|
||||
|
||||
private fun resolvePolicyGroupProfiles(config: ProfileItem): List<ProfileItem> {
|
||||
try {
|
||||
val serverList = MmkvManager.decodeAllServerList()
|
||||
@@ -92,18 +165,15 @@ object CoreConfigContextBuilder {
|
||||
}
|
||||
}
|
||||
.filter { it.server.isNotNullEmpty() }
|
||||
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||
.filter { it.configType != EConfigType.CUSTOM }
|
||||
.filter { it.configType != EConfigType.POLICYGROUP }
|
||||
.filter { it.configType != EConfigType.PROXYCHAIN }
|
||||
.filter { Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||
.filter { !it.configType.isComplexType() }
|
||||
.toList()
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve policy group profiles for config '${config.remarks}'", e)
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve policy group profiles for '${config.remarks}'", e)
|
||||
return listOf(config)
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolves proxy-chain members with the same filters as runtime build. */
|
||||
private fun resolveProxyChainProfiles(config: ProfileItem): List<ProfileItem> {
|
||||
if (config.proxyChainProfiles.isNullOrBlank()) {
|
||||
return listOf(config)
|
||||
@@ -114,26 +184,22 @@ object CoreConfigContextBuilder {
|
||||
.asSequence()
|
||||
.mapNotNull { remark -> SettingsManager.getServerViaRemarks(remark) }
|
||||
.filter { it.server.isNotNullEmpty() }
|
||||
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||
.filter { it.configType != EConfigType.CUSTOM }
|
||||
.filter { it.configType != EConfigType.POLICYGROUP }
|
||||
.filter { it.configType != EConfigType.PROXYCHAIN }
|
||||
.filter { Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||
.filter { !it.configType.isComplexType() }
|
||||
.toList()
|
||||
.reversed()
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles for config '${config.remarks}'", e)
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles for '${config.remarks}'", e)
|
||||
return listOf(config)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves chain nodes in fixed order: next -> current -> prev.
|
||||
* If chain cannot be built, caller treats result as normal mode.
|
||||
* Resolve chain nodes from subscription neighbors in order: next, current, prev.
|
||||
*
|
||||
* When no chain is available, return a single-node result.
|
||||
*/
|
||||
private fun resolveProxyChainProfilesFromGroup(config: ProfileItem): List<ProfileItem> {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
|
||||
return listOf(config)
|
||||
}
|
||||
if (config.subscriptionId.isEmpty()) {
|
||||
return listOf(config)
|
||||
}
|
||||
@@ -141,56 +207,43 @@ object CoreConfigContextBuilder {
|
||||
try {
|
||||
val subItem = MmkvManager.decodeSubscription(config.subscriptionId) ?: return listOf(config)
|
||||
val resolved = mutableListOf<ProfileItem>()
|
||||
|
||||
// Keep the same practical chain order as current runtime assembly:
|
||||
// next -> current -> prev
|
||||
SettingsManager.getServerViaRemarks(subItem.nextProfile)?.let { resolved.add(it) }
|
||||
resolved.add(config)
|
||||
SettingsManager.getServerViaRemarks(subItem.prevProfile)?.let { resolved.add(it) }
|
||||
|
||||
return resolved
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles from group for config '${config.remarks}'", e)
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain from group for '${config.remarks}'", e)
|
||||
return listOf(config)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves custom outbound profiles from routing rulesets.
|
||||
* Scans rulesets for non-builtin outbound tags and looks up matching profiles by remarks.
|
||||
* Returns a map of tag -> ProfileItem for independent processing.
|
||||
* Collect enabled routing domain rules in original order for DNS segmentation.
|
||||
*
|
||||
* outbounds are normalized into three tags only: proxy / direct / block.
|
||||
*/
|
||||
private fun resolveCustomOutbounds(): Map<String, ProfileItem> {
|
||||
val customMap = mutableMapOf<String, ProfileItem>()
|
||||
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return customMap
|
||||
private fun collectRoutingDomainRulesForDns(): List<CoreConfigContext.RoutingDomainRule> {
|
||||
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return emptyList()
|
||||
val result = mutableListOf<CoreConfigContext.RoutingDomainRule>()
|
||||
|
||||
try {
|
||||
val processedTags = mutableSetOf<String>()
|
||||
|
||||
rulesetItems
|
||||
.filter { it.enabled }
|
||||
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
|
||||
.filter { tag -> tag !in AppConfig.BUILTIN_OUTBOUND_TAGS }
|
||||
.distinct()
|
||||
.forEach { tag ->
|
||||
if (tag in processedTags) return@forEach
|
||||
processedTags.add(tag)
|
||||
|
||||
try {
|
||||
val profile = SettingsManager.getServerViaRemarks(tag) ?: run {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
customMap[tag] = profile
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve custom outbound for tag '$tag', skipping", e)
|
||||
}
|
||||
rulesetItems
|
||||
.asSequence()
|
||||
.filter { it.enabled }
|
||||
.filter { !it.domain.isNullOrEmpty() }
|
||||
.forEach { rule ->
|
||||
val normalizedOutboundTag = when (rule.outboundTag) {
|
||||
AppConfig.TAG_DIRECT -> AppConfig.TAG_DIRECT
|
||||
AppConfig.TAG_BLOCKED -> AppConfig.TAG_BLOCKED
|
||||
else -> AppConfig.TAG_PROXY
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to resolve custom outbound profiles", e)
|
||||
}
|
||||
result.add(
|
||||
CoreConfigContext.RoutingDomainRule(
|
||||
domain = rule.domain.orEmpty(),
|
||||
outboundTag = normalizedOutboundTag
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return customMap
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,11 +1,10 @@
|
||||
package com.v2ray.ang.core
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
@@ -73,41 +72,6 @@ object CoreOutboundBuilder {
|
||||
outbound.mux?.concurrency = -1
|
||||
}
|
||||
|
||||
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||
var localTunAddr = if (outbound.settings?.address == null) {
|
||||
listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
|
||||
} else {
|
||||
outbound.settings?.address as List<*>
|
||||
}
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) != true) {
|
||||
localTunAddr = listOf(localTunAddr.first())
|
||||
}
|
||||
outbound.settings?.address = localTunAddr
|
||||
}
|
||||
|
||||
if (outbound.streamSettings?.network == AppConfig.DEFAULT_NETWORK
|
||||
&& outbound.streamSettings?.tcpSettings?.header?.type == AppConfig.HEADER_TYPE_HTTP
|
||||
) {
|
||||
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
|
||||
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
||||
|
||||
val requestString: String by lazy {
|
||||
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":["no-cache"]}}"""
|
||||
}
|
||||
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
||||
requestString,
|
||||
OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
||||
)
|
||||
outbound.streamSettings?.tcpSettings?.header?.request?.path =
|
||||
if (path.isNullOrEmpty()) {
|
||||
listOf("/")
|
||||
} else {
|
||||
path
|
||||
}
|
||||
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
|
||||
return false
|
||||
@@ -291,9 +255,23 @@ object CoreOutboundBuilder {
|
||||
private fun toOutboundWireguard(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = createInitOutbound(EConfigType.WIREGUARD)
|
||||
|
||||
val rawAddresses = profileItem.localAddress
|
||||
?.split(",")
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.ifEmpty { null }
|
||||
?: listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
|
||||
|
||||
val addresses = if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) == true) {
|
||||
rawAddresses
|
||||
} else {
|
||||
val ipv4Addresses = rawAddresses.filter { !it.contains(":") }
|
||||
ipv4Addresses.ifEmpty { listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4) }
|
||||
}
|
||||
|
||||
outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = profileItem.secretKey
|
||||
wireguard.address = (profileItem.localAddress ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||
wireguard.address = addresses
|
||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||
peer.publicKey = profileItem.publicKey.orEmpty()
|
||||
peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank()
|
||||
@@ -328,6 +306,23 @@ object CoreOutboundBuilder {
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
private fun createTcpHttpRequest(
|
||||
host: String?,
|
||||
path: String?
|
||||
): OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean {
|
||||
val requestString =
|
||||
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
|
||||
val request = JsonUtil.fromJson(
|
||||
requestString,
|
||||
OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
||||
) ?: OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
|
||||
|
||||
val parsedHost = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
request.headers.Host = parsedHost.ifEmpty { null }
|
||||
request.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }.ifEmpty { listOf("/") }
|
||||
return request
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures transport settings for an outbound connection.
|
||||
*
|
||||
@@ -358,13 +353,9 @@ object CoreOutboundBuilder {
|
||||
val tcpSetting = OutboundBean.StreamSettingsBean.TcpSettingsBean()
|
||||
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
||||
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
||||
val requestObj = OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
|
||||
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
tcpSetting.header.request = requestObj
|
||||
sni = requestObj.headers.Host?.getOrNull(0)
|
||||
}
|
||||
val requestObj = createTcpHttpRequest(host, path)
|
||||
tcpSetting.header.request = requestObj
|
||||
sni = requestObj.headers.Host?.getOrNull(0)
|
||||
} else {
|
||||
tcpSetting.header.type = "none"
|
||||
sni = host
|
||||
@@ -377,37 +368,40 @@ object CoreOutboundBuilder {
|
||||
profileItem.kcpMtu?.let { kcpSetting.mtu = it }
|
||||
profileItem.kcpTti?.let { kcpSetting.tti = it }
|
||||
streamSettings.kcpSettings = kcpSetting
|
||||
val udpMaskList = mutableListOf<OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean>()
|
||||
val udpMaskList =
|
||||
mutableListOf<OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean>()
|
||||
if (!headerType.isNullOrEmpty() && headerType != "none") {
|
||||
val kcpHeaderType = when {
|
||||
headerType == "wechat-video" -> "header-wechat"
|
||||
else -> "header-$headerType"
|
||||
headerType == "wechat-video" -> "wechat"
|
||||
else -> headerType
|
||||
}
|
||||
udpMaskList.add(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = kcpHeaderType,
|
||||
settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
||||
type = "mkcp-legacy",
|
||||
settings =
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
domain = host
|
||||
header = kcpHeaderType,
|
||||
value = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
||||
host
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
if (seed.isNullOrEmpty()) {
|
||||
udpMaskList.add(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "mkcp-original"
|
||||
type = "mkcp-legacy"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
udpMaskList.add(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "mkcp-aes128gcm",
|
||||
type = "mkcp-legacy",
|
||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
password = seed
|
||||
value = seed
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -419,7 +413,7 @@ object CoreOutboundBuilder {
|
||||
|
||||
NetworkType.WS.type -> {
|
||||
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
|
||||
wssetting.headers.Host = host.orEmpty()
|
||||
wssetting.host = host.orEmpty()
|
||||
sni = host
|
||||
wssetting.path = path ?: "/"
|
||||
streamSettings.wsSettings = wssetting
|
||||
@@ -554,7 +548,7 @@ object CoreOutboundBuilder {
|
||||
*/
|
||||
fun populateTlsSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
|
||||
val streamSecurity = profileItem.security.orEmpty()
|
||||
val allowInsecure = profileItem.insecure == true
|
||||
val allowInsecure = profileItem.insecure == true && profileItem.pinnedCA256.isNullOrEmpty()
|
||||
val sni = if (profileItem.sni.isNullOrEmpty()) {
|
||||
when {
|
||||
sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt
|
||||
@@ -573,6 +567,7 @@ object CoreOutboundBuilder {
|
||||
fingerprint = profileItem.fingerPrint.nullIfBlank(),
|
||||
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
|
||||
echConfigList = profileItem.echConfigList.nullIfBlank(),
|
||||
verifyPeerCertByName = profileItem.verifyPeerCertByName.nullIfBlank(),
|
||||
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
|
||||
publicKey = profileItem.publicKey.nullIfBlank(),
|
||||
shortId = profileItem.shortId.nullIfBlank(),
|
||||
@@ -620,10 +615,6 @@ object CoreOutboundBuilder {
|
||||
&& packets == "tlshello"
|
||||
) {
|
||||
packets = "1-3"
|
||||
} else if (streamSettings.security == AppConfig.TLS
|
||||
&& packets != "tlshello"
|
||||
) {
|
||||
packets = "tlshello"
|
||||
}
|
||||
|
||||
val fragmentMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
@@ -633,7 +624,9 @@ object CoreOutboundBuilder {
|
||||
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
||||
?: "50-100",
|
||||
delay = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||
?: "10-20"
|
||||
?: "10-20",
|
||||
maxSplit = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_MAXSPLIT)
|
||||
?: "10"
|
||||
)
|
||||
)
|
||||
val noiseMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
@@ -694,4 +687,4 @@ object CoreOutboundBuilder {
|
||||
}
|
||||
return resolvedIps.first()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,18 @@ import androidx.core.content.ContextCompat
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.contracts.ServiceControl
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.dto.OutboundTrafficStat
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.NotificationManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.SpeedtestManager
|
||||
import com.v2ray.ang.root.RootManager
|
||||
import com.v2ray.ang.service.CoreProxyOnlyService
|
||||
import com.v2ray.ang.service.CoreRootService
|
||||
import com.v2ray.ang.service.CoreVpnService
|
||||
import com.v2ray.ang.service.DialerNativeService
|
||||
import com.v2ray.ang.service.DialerWebviewService
|
||||
@@ -144,9 +148,7 @@ object CoreServiceManager {
|
||||
error(context.getString(R.string.toast_config_file_invalid))
|
||||
}
|
||||
|
||||
if (config.configType != EConfigType.CUSTOM
|
||||
&& config.configType != EConfigType.POLICYGROUP
|
||||
&& config.configType != EConfigType.PROXYCHAIN
|
||||
if (!config.configType.isComplexType()
|
||||
&& !Utils.isValidUrl(config.server)
|
||||
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
||||
) {
|
||||
@@ -160,14 +162,27 @@ object CoreServiceManager {
|
||||
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
||||
// if (!result.status) error(result.errorMessage.ifBlank { "Failed to get V2Ray config" })
|
||||
|
||||
if (config.insecure == true) {
|
||||
context.toastError(R.string.toast_allow_insecure_deprecated)
|
||||
context.toastError(R.string.toast_allow_insecure_deprecated)
|
||||
}
|
||||
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING)) {
|
||||
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
||||
} else {
|
||||
context.toast(R.string.toast_services_start)
|
||||
}
|
||||
|
||||
val isVpnMode = SettingsManager.isVpnMode()
|
||||
val intent = if (isVpnMode) {
|
||||
val isRootMode = SettingsManager.isRootMode()
|
||||
if (isRootMode && !RootManager.isRootAvailable()) {
|
||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: root mode requires root but none available")
|
||||
error(context.getString(R.string.toast_root_required))
|
||||
}
|
||||
|
||||
val intent = if (isRootMode) {
|
||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting Root service")
|
||||
Intent(context.applicationContext, CoreRootService::class.java)
|
||||
} else if (SettingsManager.isVpnMode()) {
|
||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service")
|
||||
Intent(context.applicationContext, CoreVpnService::class.java)
|
||||
} else {
|
||||
@@ -175,7 +190,20 @@ object CoreServiceManager {
|
||||
Intent(context.applicationContext, CoreProxyOnlyService::class.java)
|
||||
}
|
||||
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
try {
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
} catch (e: SecurityException) {
|
||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Missing permission to start foreground service", e)
|
||||
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
|
||||
} catch (e: RuntimeException) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
e.javaClass.name == "android.app.ForegroundServiceStartNotAllowedException"
|
||||
) {
|
||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Foreground service start not allowed", e)
|
||||
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,7 +285,7 @@ object CoreServiceManager {
|
||||
}
|
||||
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||
NotificationManager.startSpeedNotification(currentConfig)
|
||||
NotificationManager.startSpeedNotification()
|
||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
|
||||
}
|
||||
|
||||
@@ -299,13 +327,32 @@ object CoreServiceManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the statistics for a given tag and link.
|
||||
* @param tag The tag to query.
|
||||
* @param link The link to query.
|
||||
* @return The statistics value.
|
||||
* Queries and resets all outbound traffic counters in one core call.
|
||||
* Go side format: tag,direction,value;tag,direction,value;
|
||||
*/
|
||||
fun queryStats(tag: String, link: String): Long {
|
||||
return coreController.queryStats(tag, link)
|
||||
fun queryAllOutboundTrafficStats(): List<OutboundTrafficStat> {
|
||||
val payload = coreController.queryAllOutboundTrafficStats()
|
||||
|
||||
val result = ArrayList<OutboundTrafficStat>()
|
||||
|
||||
payload.split(';').forEach { entry ->
|
||||
if (entry.isBlank()) return@forEach
|
||||
|
||||
val parts = entry.split(',', limit = 3)
|
||||
if (parts.size != 3) return@forEach
|
||||
|
||||
val value = parts[2].toLongOrNull() ?: return@forEach
|
||||
|
||||
result.add(
|
||||
OutboundTrafficStat(
|
||||
tag = parts[0],
|
||||
direction = parts[1],
|
||||
value = value,
|
||||
)
|
||||
)
|
||||
}
|
||||
// LogUtil.d(AppConfig.TAG, "Queried outbound traffic stats: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -488,12 +535,12 @@ object CoreServiceManager {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen off")
|
||||
NotificationManager.stopSpeedNotification(currentConfig)
|
||||
NotificationManager.stopSpeedNotification()
|
||||
}
|
||||
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen on")
|
||||
NotificationManager.startSpeedNotification(currentConfig)
|
||||
NotificationManager.startSpeedNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
data class CertSha256Request(
|
||||
val address: String = "",
|
||||
val port: Int = 443,
|
||||
val serverName: String? = null,
|
||||
val timeoutMs: Long = 5000L,
|
||||
)
|
||||
|
||||
data class CertSha256Result(
|
||||
val sha256: String = "",
|
||||
val error: String = "",
|
||||
)
|
||||
@@ -1,16 +1,25 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
import android.content.Context
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.CoreResolvedType
|
||||
|
||||
/**
|
||||
* Runtime context produced by the builder and consumed by CoreConfigManager.
|
||||
*/
|
||||
data class CoreConfigContext(
|
||||
val context: Context,
|
||||
val guid: String,
|
||||
val selectedProfile: ProfileItem,
|
||||
val resolvedProfiles: List<ProfileItem>,
|
||||
val resolvedType: CoreResolvedType,
|
||||
val customOutboundProfiles: Map<String, ProfileItem> = emptyMap(),
|
||||
)
|
||||
val isCustom: Boolean = false,
|
||||
val resolvedOutbounds: List<ResolvedOutbound> = emptyList(),
|
||||
val routingDomainRules: List<RoutingDomainRule> = emptyList(),
|
||||
) {
|
||||
data class ResolvedOutbound(
|
||||
val tag: String,
|
||||
val profile: ProfileItem,
|
||||
val resolvedProfiles: List<ProfileItem>,
|
||||
val resolvedType: CoreResolvedType,
|
||||
)
|
||||
|
||||
data class RoutingDomainRule(
|
||||
val domain: List<String>,
|
||||
val outboundTag: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
data class OutboundTrafficStat(
|
||||
val tag: String,
|
||||
val direction: String,
|
||||
val value: Long,
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
data class UrlContentRequest(
|
||||
val url: String?,
|
||||
val timeout: Int = 15000,
|
||||
val httpPort: Int = 0,
|
||||
val proxyUsername: String? = null,
|
||||
val proxyPassword: String? = null,
|
||||
val userAgent: String? = null
|
||||
)
|
||||
@@ -68,7 +68,6 @@ data class V2rayConfig(
|
||||
var protocol: String,
|
||||
var settings: OutSettingsBean? = null,
|
||||
var streamSettings: StreamSettingsBean? = null,
|
||||
val proxySettings: Any? = null,
|
||||
val sendThrough: String? = null,
|
||||
var mux: MuxBean? = MuxBean(false)
|
||||
) {
|
||||
@@ -82,7 +81,6 @@ data class V2rayConfig(
|
||||
var address: Any? = null,
|
||||
var port: Int? = null,
|
||||
/*Freedom*/
|
||||
var domainStrategy: String? = null,
|
||||
val redirect: String? = null,
|
||||
val userLevel: Int? = null,
|
||||
/*Loopback*/
|
||||
@@ -181,7 +179,7 @@ data class V2rayConfig(
|
||||
@SerializedName("Accept-Encoding")
|
||||
val acceptEncoding: List<String>? = null,
|
||||
val Connection: List<String>? = null,
|
||||
val Pragma: String? = null
|
||||
val Pragma: Any? = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -199,13 +197,12 @@ data class V2rayConfig(
|
||||
|
||||
data class WsSettingsBean(
|
||||
var path: String? = null,
|
||||
var headers: HeadersBean = HeadersBean(),
|
||||
var host: String? = null,
|
||||
var headers: Map<String, String>? = null,
|
||||
val maxEarlyData: Int? = null,
|
||||
val useBrowserForwarding: Boolean? = null,
|
||||
val acceptProxyProtocol: Boolean? = null
|
||||
) {
|
||||
data class HeadersBean(var Host: String = "")
|
||||
}
|
||||
)
|
||||
|
||||
data class HttpupgradeSettingsBean(
|
||||
var path: String? = null,
|
||||
@@ -256,9 +253,9 @@ data class V2rayConfig(
|
||||
val disableSystemRoot: Boolean? = null,
|
||||
val enableSessionResumption: Boolean? = null,
|
||||
var echConfigList: String? = null,
|
||||
var verifyPeerCertByName: String? = null,
|
||||
var pinnedPeerCertSha256: String? = null,
|
||||
// REALITY settings
|
||||
val show: Boolean = false,
|
||||
var publicKey: String? = null,
|
||||
var shortId: String? = null,
|
||||
var spiderX: String? = null,
|
||||
@@ -298,12 +295,13 @@ data class V2rayConfig(
|
||||
) {
|
||||
data class MaskSettingsBean(
|
||||
val password: String? = null,
|
||||
val domain: String? = null,
|
||||
val header: String? = null,
|
||||
val value: String? = null,
|
||||
// fragment
|
||||
val packets: String? = null,
|
||||
val length: String? = null,
|
||||
val delay: String? = null,
|
||||
// val maxSplit: String? = null,
|
||||
val maxSplit: String? = null,
|
||||
// noise
|
||||
val reset: Int? = null,
|
||||
val noise: List<NoiseMaskBean>? = null
|
||||
|
||||
@@ -16,5 +16,7 @@ data class VmessQRCode(
|
||||
var sni: String = "",
|
||||
var alpn: String = "",
|
||||
var fp: String = "",
|
||||
var insecure: String = ""
|
||||
var insecure: String = "",
|
||||
var vcn: String = "",
|
||||
var pcs: String = ""
|
||||
)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.dto
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
data class AssetUrlCache(
|
||||
val guid: String,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.dto
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
data class AssetUrlItem(
|
||||
var remarks: String = "",
|
||||
+4
-11
@@ -1,10 +1,6 @@
|
||||
package com.v2ray.ang.dto
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||
import com.v2ray.ang.AppConfig.PORT_SOCKS
|
||||
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
@@ -46,6 +42,7 @@ data class ProfileItem(
|
||||
var fingerPrint: String? = null,
|
||||
var insecure: Boolean? = null,
|
||||
var echConfigList: String? = null,
|
||||
var verifyPeerCertByName: String? = null,
|
||||
var pinnedCA256: String? = null,
|
||||
|
||||
var publicKey: String? = null,
|
||||
@@ -80,13 +77,9 @@ data class ProfileItem(
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllOutboundTags(): MutableList<String> {
|
||||
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
|
||||
}
|
||||
|
||||
fun getServerAddressAndPort(): String {
|
||||
if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
|
||||
return "$LOOPBACK:$PORT_SOCKS"
|
||||
return "${AppConfig.LOOPBACK}:${AppConfig.PORT_SOCKS}"
|
||||
}
|
||||
return Utils.getIpv6Address(server) + ":" + serverPort
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.dto
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
data class RulesetItem(
|
||||
var remarks: String? = "",
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.dto
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
||||
fun getTestDelayString(): String {
|
||||
@@ -7,4 +7,4 @@ data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
||||
}
|
||||
return testDelayMillis.toString() + "ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.dto
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
data class ServersCache(
|
||||
val guid: String,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.dto
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
data class SubscriptionCache(
|
||||
val guid: String,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.dto
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
data class SubscriptionItem(
|
||||
var remarks: String = "",
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.v2ray.ang.dto
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
data class WebDavConfig(
|
||||
val baseUrl: String,
|
||||
@@ -6,4 +6,4 @@ data class WebDavConfig(
|
||||
val password: String? = null,
|
||||
val remoteBasePath: String = "/",
|
||||
val timeoutSeconds: Long = 30
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.v2ray.ang.enums
|
||||
|
||||
/**
|
||||
* Load-balancing strategy types supported by xray balancer.
|
||||
*
|
||||
* @param policyGroupType The string value written into the xray config JSON.
|
||||
* @param policyGroupTypeValue The numeric string stored in [com.v2ray.ang.dto.entities.ProfileItem.policyGroupType].
|
||||
* @param requiresBurstObservatory Whether this strategy needs a burstObservatory (leastLoad).
|
||||
* @param requiresObservatory Whether this strategy needs an observatory (leastPing).
|
||||
*/
|
||||
enum class BalancerStrategyType(
|
||||
val policyGroupType: String,
|
||||
val policyGroupTypeValue: String,
|
||||
val requiresBurstObservatory: Boolean = false,
|
||||
val requiresObservatory: Boolean = false,
|
||||
) {
|
||||
LEAST_LOAD("leastLoad", "1", requiresBurstObservatory = true),
|
||||
RANDOM("random", "2"),
|
||||
ROUND_ROBIN("roundRobin", "3"),
|
||||
LEAST_PING("leastPing", "", requiresObservatory = true); // default / else
|
||||
|
||||
companion object {
|
||||
fun from(policyGroupType: String?): BalancerStrategyType =
|
||||
entries.firstOrNull { it.policyGroupTypeValue == policyGroupType } ?: LEAST_PING
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ enum class CoreResolvedType {
|
||||
NORMAL,
|
||||
POLICYGROUP,
|
||||
PROXYCHAIN,
|
||||
CUSTOM,
|
||||
}
|
||||
@@ -13,21 +13,16 @@ enum class PermissionType {
|
||||
override fun getPermission(): String = Manifest.permission.CAMERA
|
||||
},
|
||||
|
||||
/** Read storage / media permission (adapts to Android version) */
|
||||
READ_STORAGE {
|
||||
override fun getPermission(): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Manifest.permission.READ_MEDIA_IMAGES
|
||||
} else {
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** Notification permission (Android 13+) */
|
||||
POST_NOTIFICATIONS {
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
override fun getPermission(): String = Manifest.permission.POST_NOTIFICATIONS
|
||||
},
|
||||
|
||||
/** Local network access permission (Android 17+) */
|
||||
ACCESS_LOCAL_NETWORK {
|
||||
@RequiresApi(Build.VERSION_CODES.CINNAMON_BUN)
|
||||
override fun getPermission(): String = Manifest.permission.ACCESS_LOCAL_NETWORK
|
||||
};
|
||||
|
||||
/** Return the actual Android permission string */
|
||||
@@ -37,8 +32,8 @@ enum class PermissionType {
|
||||
fun getLabel(): String {
|
||||
return when (this) {
|
||||
CAMERA -> "Camera"
|
||||
READ_STORAGE -> "Storage"
|
||||
POST_NOTIFICATIONS -> "Notification"
|
||||
ACCESS_LOCAL_NETWORK -> "Local Network"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import es.dmoral.toasty.Toasty
|
||||
import java.io.Serializable
|
||||
import java.net.URI
|
||||
@@ -200,4 +201,22 @@ fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean =
|
||||
}
|
||||
return regex?.containsMatchIn(this)
|
||||
?: this.contains(keyword, ignoreCase = ignoreCase)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the config type is a group type (PolicyGroup or ProxyChain).
|
||||
*
|
||||
* @return True if the config type is PolicyGroup or ProxyChain, false otherwise.
|
||||
*/
|
||||
fun EConfigType.isGroupType(): Boolean {
|
||||
return this == EConfigType.POLICYGROUP || this == EConfigType.PROXYCHAIN
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the config type is a complex type (Custom, PolicyGroup, or ProxyChain).
|
||||
*
|
||||
* @return True if the config type is Custom, PolicyGroup, or ProxyChain, false otherwise.
|
||||
*/
|
||||
fun EConfigType.isComplexType(): Boolean {
|
||||
return this == EConfigType.CUSTOM || this == EConfigType.POLICYGROUP || this == EConfigType.PROXYCHAIN
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
@@ -50,9 +50,8 @@ open class FmtBase {
|
||||
*
|
||||
* @param config the ProfileItem object to populate
|
||||
* @param queryParam the query parameters to use for populating the ProfileItem
|
||||
* @param allowInsecure whether to allow insecure connections
|
||||
*/
|
||||
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
|
||||
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>) {
|
||||
config.network = queryParam["type"] ?: NetworkType.TCP.type
|
||||
config.headerType = queryParam["headerType"]
|
||||
config.host = queryParam["host"]
|
||||
@@ -79,12 +78,13 @@ open class FmtBase {
|
||||
config.insecure = when {
|
||||
allowInsecureKeys.any { queryParam[it] == "1" } -> true
|
||||
allowInsecureKeys.any { queryParam[it] == "0" } -> false
|
||||
else -> allowInsecure
|
||||
else -> false
|
||||
}
|
||||
config.sni = queryParam["sni"]
|
||||
config.fingerPrint = queryParam["fp"]
|
||||
config.alpn = queryParam["alpn"]
|
||||
config.echConfigList = queryParam["ech"]
|
||||
config.verifyPeerCertByName = queryParam["vcn"]
|
||||
config.pinnedCA256 = queryParam["pcs"]
|
||||
config.publicKey = queryParam["pbk"]
|
||||
config.shortId = queryParam["sid"]
|
||||
@@ -105,6 +105,7 @@ open class FmtBase {
|
||||
config.sni?.nullIfBlank()?.let { dicQuery["sni"] = it }
|
||||
config.alpn?.nullIfBlank()?.let { dicQuery["alpn"] = it }
|
||||
config.echConfigList?.nullIfBlank()?.let { dicQuery["ech"] = it }
|
||||
config.verifyPeerCertByName?.nullIfBlank()?.let { dicQuery["vcn"] = it }
|
||||
config.pinnedCA256?.nullIfBlank()?.let { dicQuery["pcs"] = it }
|
||||
config.fingerPrint?.nullIfBlank()?.let { dicQuery["fp"] = it }
|
||||
config.publicKey?.nullIfBlank()?.let { dicQuery["pbk"] = it }
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -19,7 +18,6 @@ object Hysteria2Fmt : FmtBase() {
|
||||
* @return the parsed ProfileItem object, or null if parsing fails
|
||||
*/
|
||||
fun parse(str: String): ProfileItem {
|
||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
@@ -33,7 +31,7 @@ object Hysteria2Fmt : FmtBase() {
|
||||
if (!uri.rawQuery.isNullOrEmpty()) {
|
||||
val queryParam = getQueryParam(uri)
|
||||
|
||||
getItemFormQuery(config, queryParam, allowInsecure)
|
||||
getItemFormQuery(config, queryParam)
|
||||
|
||||
config.security = queryParam["security"] ?: AppConfig.TLS
|
||||
config.obfsPassword = queryParam["obfs-password"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
@@ -26,7 +26,11 @@ object SocksFmt : FmtBase() {
|
||||
config.serverPort = uri.port.toString()
|
||||
|
||||
if (uri.userInfo?.isEmpty() == false) {
|
||||
val result = Utils.decode(uri.userInfo).split(":", limit = 2)
|
||||
val result = if (uri.userInfo.contains(":")) {
|
||||
uri.userInfo.split(":", limit = 2)
|
||||
} else {
|
||||
Utils.decode(uri.userInfo).split(":", limit = 2)
|
||||
}
|
||||
if (result.count() == 2) {
|
||||
config.username = result.first()
|
||||
config.password = result.last()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -17,7 +16,6 @@ object TrojanFmt : FmtBase() {
|
||||
* @return the parsed ProfileItem object, or null if parsing fails
|
||||
*/
|
||||
fun parse(str: String): ProfileItem {
|
||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
@@ -29,11 +27,11 @@ object TrojanFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) {
|
||||
config.network = NetworkType.TCP.type
|
||||
config.security = AppConfig.TLS
|
||||
config.insecure = allowInsecure
|
||||
config.insecure = false
|
||||
} else {
|
||||
val queryParam = getQueryParam(uri)
|
||||
|
||||
getItemFormQuery(config, queryParam, allowInsecure)
|
||||
getItemFormQuery(config, queryParam)
|
||||
config.security = queryParam["security"] ?: AppConfig.TLS
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -17,7 +15,6 @@ object VlessFmt : FmtBase() {
|
||||
* @return the parsed ProfileItem object, or null if parsing fails
|
||||
*/
|
||||
fun parse(str: String): ProfileItem? {
|
||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||
val config = ProfileItem.create(EConfigType.VLESS)
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
@@ -30,7 +27,7 @@ object VlessFmt : FmtBase() {
|
||||
config.password = uri.userInfo
|
||||
config.method = queryParam["encryption"] ?: "none"
|
||||
|
||||
getItemFormQuery(config, queryParam, allowInsecure)
|
||||
getItemFormQuery(config, queryParam)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@ package com.v2ray.ang.fmt
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.VmessQRCode
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
@@ -26,7 +25,6 @@ object VmessFmt : FmtBase() {
|
||||
return parseVmessStd(str)
|
||||
}
|
||||
|
||||
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||
val config = ProfileItem.create(EConfigType.VMESS)
|
||||
|
||||
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
||||
@@ -87,8 +85,11 @@ object VmessFmt : FmtBase() {
|
||||
config.insecure = when (vmessQRCode.insecure) {
|
||||
"1" -> true
|
||||
"0" -> false
|
||||
else -> allowInsecure
|
||||
else -> false
|
||||
}
|
||||
config.verifyPeerCertByName = vmessQRCode.vcn
|
||||
config.pinnedCA256 = vmessQRCode.pcs
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -142,6 +143,8 @@ object VmessFmt : FmtBase() {
|
||||
false -> "0"
|
||||
else -> ""
|
||||
}
|
||||
vmessQRCode.vcn = config.verifyPeerCertByName.orEmpty()
|
||||
vmessQRCode.pcs = config.pinnedCA256.orEmpty()
|
||||
|
||||
val json = JsonUtil.toJson(vmessQRCode)
|
||||
return Utils.encode(json)
|
||||
@@ -154,7 +157,6 @@ object VmessFmt : FmtBase() {
|
||||
* @return the parsed ProfileItem object, or null if parsing fails
|
||||
*/
|
||||
fun parseVmessStd(str: String): ProfileItem? {
|
||||
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||
val config = ProfileItem.create(EConfigType.VMESS)
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
@@ -167,7 +169,7 @@ object VmessFmt : FmtBase() {
|
||||
config.password = uri.userInfo
|
||||
config.method = AppConfig.DEFAULT_SECURITY
|
||||
|
||||
getItemFormQuery(config, queryParam, allowInsecure)
|
||||
getItemFormQuery(config, queryParam)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
|
||||
@@ -4,13 +4,13 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.text.TextUtils
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.HY2
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.core.CoreConfigManager
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.SubscriptionCache
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
||||
import com.v2ray.ang.dto.UrlContentRequest
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.fmt.CustomFmt
|
||||
@@ -30,6 +30,21 @@ import java.net.URI
|
||||
|
||||
object AngConfigManager {
|
||||
|
||||
// Parser mapping for different config types (lazy initialized)
|
||||
private val configFmtParsers: Map<String, (String) -> ProfileItem?> by lazy {
|
||||
mapOf(
|
||||
EConfigType.VMESS.protocolScheme to VmessFmt::parse,
|
||||
EConfigType.SHADOWSOCKS.protocolScheme to ShadowsocksFmt::parse,
|
||||
EConfigType.SOCKS.protocolScheme to SocksFmt::parse,
|
||||
AppConfig.SOCKS4 to SocksFmt::parse,
|
||||
AppConfig.SOCKS5 to SocksFmt::parse,
|
||||
EConfigType.TROJAN.protocolScheme to TrojanFmt::parse,
|
||||
EConfigType.VLESS.protocolScheme to VlessFmt::parse,
|
||||
EConfigType.WIREGUARD.protocolScheme to WireguardFmt::parse,
|
||||
EConfigType.HYSTERIA2.protocolScheme to Hysteria2Fmt::parse,
|
||||
AppConfig.HY2 to Hysteria2Fmt::parse
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares the configuration to the clipboard.
|
||||
@@ -219,15 +234,8 @@ object AngConfigManager {
|
||||
if (servers == null) {
|
||||
return 0
|
||||
}
|
||||
// Find the currently selected server that matches the subscription ID
|
||||
val removedSelected = if (subid.isNotBlank() && !append) {
|
||||
MmkvManager.getSelectServer()
|
||||
.takeIf { it?.isNotBlank() == true }
|
||||
?.let { MmkvManager.decodeServerConfig(it) }
|
||||
?.takeIf { it.subscriptionId == subid }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
// Find the currently selected server that belongs to the same subscription before replacement.
|
||||
val removedSelected = getRemovedSelectedProfile(subid, append)
|
||||
|
||||
val subItem = MmkvManager.decodeSubscription(subid)
|
||||
|
||||
@@ -273,7 +281,6 @@ object AngConfigManager {
|
||||
|
||||
// Read serverList once
|
||||
val serverList = MmkvManager.decodeServerList(subid)
|
||||
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
||||
|
||||
configs.forEach { config ->
|
||||
val key = Utils.getUuid()
|
||||
@@ -282,10 +289,6 @@ object AngConfigManager {
|
||||
|
||||
if (!serverList.contains(key)) {
|
||||
serverList.add(0, key)
|
||||
if (needSetSelected) {
|
||||
MmkvManager.setSelectServer(key)
|
||||
needSetSelected = false
|
||||
}
|
||||
}
|
||||
keyToProfile[key] = config
|
||||
}
|
||||
@@ -308,7 +311,18 @@ object AngConfigManager {
|
||||
* @return Matched key or null
|
||||
*/
|
||||
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
|
||||
if (keyToProfile.isEmpty() || target == null) return null
|
||||
if (keyToProfile.isEmpty()) return null
|
||||
if (target == null) return null
|
||||
|
||||
// Level 0: Full match (remarks + server + port + password)
|
||||
if (target.remarks.isNotBlank()) {
|
||||
keyToProfile.entries.firstOrNull { (_, saved) ->
|
||||
isSameText(saved.remarks, target.remarks) &&
|
||||
isSameText(saved.server, target.server) &&
|
||||
isSameText(saved.serverPort, target.serverPort) &&
|
||||
isSameText(saved.password, target.password)
|
||||
}?.key?.let { return it }
|
||||
}
|
||||
|
||||
// Level 1: Match by remarks
|
||||
if (target.remarks.isNotBlank()) {
|
||||
@@ -335,7 +349,20 @@ object AngConfigManager {
|
||||
isSameText(saved.server, target.server)
|
||||
}?.key?.let { return it }
|
||||
|
||||
return null
|
||||
// If old selected node cannot be matched, fall back to the first imported config.
|
||||
return keyToProfile.keys.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently selected profile if it belongs to the target subscription and will be replaced.
|
||||
*/
|
||||
private fun getRemovedSelectedProfile(subid: String, append: Boolean): ProfileItem? {
|
||||
if (subid.isBlank() || append) return null
|
||||
|
||||
return MmkvManager.getSelectServer()
|
||||
.takeIf { it?.isNotBlank() == true }
|
||||
?.let { MmkvManager.decodeServerConfig(it) }
|
||||
?.takeIf { it.subscriptionId == subid }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,18 +398,25 @@ object AngConfigManager {
|
||||
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
||||
|
||||
if (serverList.isNotEmpty()) {
|
||||
val removedSelected = getRemovedSelectedProfile(subid, append)
|
||||
if (!append) {
|
||||
MmkvManager.removeServerViaSubid(subid)
|
||||
}
|
||||
var count = 0
|
||||
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
||||
for (srv in serverList.reversed()) {
|
||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
||||
config.subscriptionId = subid
|
||||
config.description = generateDescription(config)
|
||||
val key = MmkvManager.encodeServerConfig("", config)
|
||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
||||
keyToProfile[key] = config
|
||||
count += 1
|
||||
}
|
||||
if (count > 0) {
|
||||
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||
}
|
||||
return count
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -442,22 +476,8 @@ object AngConfigManager {
|
||||
return null
|
||||
}
|
||||
|
||||
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
|
||||
VmessFmt.parse(str)
|
||||
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
|
||||
ShadowsocksFmt.parse(str)
|
||||
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
|
||||
SocksFmt.parse(str)
|
||||
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
|
||||
TrojanFmt.parse(str)
|
||||
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
|
||||
VlessFmt.parse(str)
|
||||
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
|
||||
WireguardFmt.parse(str)
|
||||
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
|
||||
Hysteria2Fmt.parse(str)
|
||||
} else {
|
||||
null
|
||||
val config = configFmtParsers.firstNotNullOfOrNull { (scheme, parser) ->
|
||||
if (str.startsWith(scheme)) parser(str) else null
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
@@ -535,14 +555,28 @@ object AngConfigManager {
|
||||
|
||||
var configText = try {
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort, proxyUsername, proxyPassword)
|
||||
HttpUtil.getUrlContentWithUserAgent(
|
||||
UrlContentRequest(
|
||||
url = url,
|
||||
userAgent = userAgent,
|
||||
timeout = 15000,
|
||||
httpPort = httpPort,
|
||||
proxyUsername = proxyUsername,
|
||||
proxyPassword = proxyPassword
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
||||
""
|
||||
}
|
||||
if (configText.isEmpty()) {
|
||||
configText = try {
|
||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent)
|
||||
HttpUtil.getUrlContentWithUserAgent(
|
||||
UrlContentRequest(
|
||||
url = url,
|
||||
userAgent = userAgent
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||
""
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.v2ray.ang.handler
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.CertSha256Request
|
||||
import com.v2ray.ang.dto.CertSha256Result
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import libv2ray.Libv2ray
|
||||
|
||||
object CertificateFingerprintManager {
|
||||
private const val TIMEOUT_MS = 5000L
|
||||
|
||||
fun fetchForManualFill(profile: ProfileItem): String? {
|
||||
val request = buildRequest(profile) ?: return null
|
||||
val result = if (profile.configType == EConfigType.HYSTERIA2) {
|
||||
fetch("quic", request) { Libv2ray.fetchQuicCertSha256(it) }
|
||||
} else {
|
||||
fetch("tls", request) { Libv2ray.fetchTlsCertSha256(it) }
|
||||
}
|
||||
|
||||
return result
|
||||
?.takeIf { it.error.isBlank() }
|
||||
?.sha256
|
||||
?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
private fun buildRequest(profile: ProfileItem): CertSha256Request? {
|
||||
if (!isFetchable(profile)) return null
|
||||
|
||||
val server = profile.server?.trim()?.takeIf { it.isNotEmpty() } ?: return null
|
||||
val port = profile.serverPort?.toIntOrNull()?.takeIf { it > 0 } ?: AppConfig.DEFAULT_PORT
|
||||
|
||||
return CertSha256Request(
|
||||
address = resolveDialAddress(server),
|
||||
port = port,
|
||||
serverName = inferServerName(profile),
|
||||
timeoutMs = TIMEOUT_MS,
|
||||
)
|
||||
}
|
||||
|
||||
private fun isFetchable(profile: ProfileItem): Boolean {
|
||||
return profile.configType == EConfigType.HYSTERIA2 || profile.security == AppConfig.TLS
|
||||
}
|
||||
|
||||
private fun fetch(
|
||||
type: String,
|
||||
request: CertSha256Request,
|
||||
fetcher: (String) -> String,
|
||||
): CertSha256Result? {
|
||||
return try {
|
||||
JsonUtil.fromJsonSafe(fetcher(JsonUtil.toJson(request)), CertSha256Result::class.java)
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
LogUtil.e(AppConfig.TAG, "Fetch $type cert SHA-256 API missing in libv2ray", e)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Fetch $type cert SHA-256 failed", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveDialAddress(server: String): String {
|
||||
if (Utils.isPureIpAddress(server) || !Utils.isDomainName(server)) return server
|
||||
|
||||
val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6, false)
|
||||
return HttpUtil.resolveHostToIP(server, preferIpv6)
|
||||
?.firstOrNull()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: server
|
||||
}
|
||||
|
||||
private fun inferServerName(profile: ProfileItem): String? {
|
||||
val sni = profile.sni?.takeIf { it.isNotBlank() }
|
||||
return sni?.takeUnless { Utils.isPureIpAddress(it) }
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig.DEFAULT_SUBSCRIPTION_ID
|
||||
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
|
||||
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
|
||||
import com.v2ray.ang.dto.AssetUrlCache
|
||||
import com.v2ray.ang.dto.AssetUrlItem
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.RulesetItem
|
||||
import com.v2ray.ang.dto.ServerAffiliationInfo
|
||||
import com.v2ray.ang.dto.SubscriptionCache
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import com.v2ray.ang.dto.WebDavConfig
|
||||
import com.v2ray.ang.dto.entities.AssetUrlCache
|
||||
import com.v2ray.ang.dto.entities.AssetUrlItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.RulesetItem
|
||||
import com.v2ray.ang.dto.entities.ServerAffiliationInfo
|
||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.dto.entities.WebDavConfig
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
@@ -102,7 +102,7 @@ object MmkvManager {
|
||||
return if (json.isNullOrBlank()) {
|
||||
mutableListOf()
|
||||
} else {
|
||||
JsonUtil.fromJson(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
|
||||
JsonUtil.fromJsonSafe(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ object MmkvManager {
|
||||
if (json.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
return JsonUtil.fromJson(json, ProfileItem::class.java)
|
||||
return JsonUtil.fromJsonSafe(json, ProfileItem::class.java)
|
||||
}
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ object MmkvManager {
|
||||
if (json.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
|
||||
return JsonUtil.fromJsonSafe(json, ServerAffiliationInfo::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,9 +286,13 @@ object MmkvManager {
|
||||
*/
|
||||
fun removeAllServer(): Int {
|
||||
val count = profileFullStorage.allKeys()?.count() ?: 0
|
||||
mainStorage.clearAll()
|
||||
profileFullStorage.clearAll()
|
||||
serverAffStorage.clearAll()
|
||||
serverRawStorage.clearAll()
|
||||
|
||||
decodeSubscriptions().forEach { sub ->
|
||||
encodeServerList(mutableListOf(), sub.guid)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -374,7 +378,7 @@ object MmkvManager {
|
||||
decodeSubsList().forEach { key ->
|
||||
val json = subStorage.decodeString(key)
|
||||
if (!json.isNullOrBlank()) {
|
||||
val item = JsonUtil.fromJson(json, SubscriptionItem::class.java) ?: SubscriptionItem()
|
||||
val item = JsonUtil.fromJsonSafe(json, SubscriptionItem::class.java) ?: SubscriptionItem()
|
||||
subscriptions.add(SubscriptionCache(key, item))
|
||||
}
|
||||
}
|
||||
@@ -420,7 +424,7 @@ object MmkvManager {
|
||||
*/
|
||||
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
|
||||
val json = subStorage.decodeString(subscriptionId) ?: return null
|
||||
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
|
||||
return JsonUtil.fromJsonSafe(json, SubscriptionItem::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,7 +446,7 @@ object MmkvManager {
|
||||
return if (json.isNullOrBlank()) {
|
||||
mutableListOf()
|
||||
} else {
|
||||
JsonUtil.fromJson(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
|
||||
JsonUtil.fromJsonSafe(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +464,7 @@ object MmkvManager {
|
||||
assetStorage.allKeys()?.forEach { key ->
|
||||
val json = assetStorage.decodeString(key)
|
||||
if (!json.isNullOrBlank()) {
|
||||
val item = JsonUtil.fromJson(json, AssetUrlItem::class.java) ?: AssetUrlItem()
|
||||
val item = JsonUtil.fromJsonSafe(json, AssetUrlItem::class.java) ?: AssetUrlItem()
|
||||
assetUrlItems.add(AssetUrlCache(key, item))
|
||||
}
|
||||
}
|
||||
@@ -495,7 +499,7 @@ object MmkvManager {
|
||||
*/
|
||||
fun decodeAsset(assetid: String): AssetUrlItem? {
|
||||
val json = assetStorage.decodeString(assetid) ?: return null
|
||||
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
|
||||
return JsonUtil.fromJsonSafe(json, AssetUrlItem::class.java)
|
||||
}
|
||||
|
||||
//endregion
|
||||
@@ -510,7 +514,7 @@ object MmkvManager {
|
||||
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
||||
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
||||
if (ruleset.isNullOrEmpty()) return null
|
||||
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java)?.toMutableList() ?: mutableListOf()
|
||||
return JsonUtil.fromJsonSafe(ruleset, Array<RulesetItem>::class.java)?.toMutableList() ?: mutableListOf()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -714,7 +718,7 @@ object MmkvManager {
|
||||
*/
|
||||
fun decodeWebDavConfig(): WebDavConfig? {
|
||||
val json = mainStorage.decodeString(KEY_WEBDAV_CONFIG) ?: return null
|
||||
return JsonUtil.fromJson(json, WebDavConfig::class.java)
|
||||
return JsonUtil.fromJsonSafe(json, WebDavConfig::class.java)
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.core.app.NotificationCompat
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.core.CoreServiceManager
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.extension.toSpeedString
|
||||
import com.v2ray.ang.ui.MainActivity
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
@@ -43,53 +43,15 @@ object NotificationManager {
|
||||
* Starts the speed notification.
|
||||
* @param currentConfig The current profile configuration.
|
||||
*/
|
||||
fun startSpeedNotification(currentConfig: ProfileItem?) {
|
||||
fun startSpeedNotification() {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return
|
||||
if (speedNotificationJob != null || CoreServiceManager.isRunning() == false) return
|
||||
|
||||
var lastZeroSpeed = false
|
||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||
outboundTags?.remove(AppConfig.TAG_DIRECT)
|
||||
|
||||
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
while (isActive) {
|
||||
val queryTime = System.currentTimeMillis()
|
||||
val sinceLastQueryIn = (queryTime - lastQueryTime)
|
||||
|
||||
// If the query interval is too short, skip this round to avoid excessive CPU usage
|
||||
if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
|
||||
LogUtil.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
|
||||
lastQueryTime = queryTime
|
||||
delay(QUERY_INTERVAL_MS)
|
||||
continue
|
||||
}
|
||||
val sinceLastQueryInSeconds = sinceLastQueryIn / 1000.0
|
||||
|
||||
var proxyTotal = 0L
|
||||
val text = StringBuilder()
|
||||
outboundTags?.forEach {
|
||||
val up = CoreServiceManager.queryStats(it, AppConfig.UPLINK)
|
||||
val down = CoreServiceManager.queryStats(it, AppConfig.DOWNLINK)
|
||||
if (up + down > 0) {
|
||||
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
||||
proxyTotal += up + down
|
||||
}
|
||||
}
|
||||
val directUplink = CoreServiceManager.queryStats(AppConfig.TAG_DIRECT, AppConfig.UPLINK)
|
||||
val directDownlink = CoreServiceManager.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, AppConfig.TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
||||
directDownlink / sinceLastQueryInSeconds
|
||||
)
|
||||
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
||||
}
|
||||
lastZeroSpeed = zeroSpeed
|
||||
lastQueryTime = queryTime
|
||||
lastZeroSpeed = updateSpeedNotificationOnce(lastZeroSpeed)
|
||||
delay(QUERY_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
@@ -143,7 +105,7 @@ object NotificationManager {
|
||||
stopV2RayPendingIntent
|
||||
)
|
||||
.addAction(
|
||||
R.drawable.ic_delete_24dp,
|
||||
R.drawable.ic_restore_24dp,
|
||||
service.getString(R.string.title_service_restart),
|
||||
restartV2RayPendingIntent
|
||||
)
|
||||
@@ -168,13 +130,12 @@ object NotificationManager {
|
||||
|
||||
/**
|
||||
* Stops the speed notification.
|
||||
* @param currentConfig The current profile configuration.
|
||||
*/
|
||||
fun stopSpeedNotification(currentConfig: ProfileItem?) {
|
||||
fun stopSpeedNotification() {
|
||||
speedNotificationJob?.let {
|
||||
it.cancel()
|
||||
speedNotificationJob = null
|
||||
updateNotification(currentConfig?.remarks, 0, 0)
|
||||
updateNotification("", 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +208,69 @@ object NotificationManager {
|
||||
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the speed notification once.
|
||||
* Queries traffic stats, separates proxy and direct, and updates the notification.
|
||||
* @param lastZeroSpeed The previous zero speed state.
|
||||
* @return The current zero speed state.
|
||||
*/
|
||||
private fun updateSpeedNotificationOnce(lastZeroSpeed: Boolean): Boolean {
|
||||
val queryTime = System.currentTimeMillis()
|
||||
val sinceLastQueryIn = (queryTime - lastQueryTime)
|
||||
|
||||
// If the query interval is too short, skip this round to avoid excessive CPU usage
|
||||
if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
|
||||
LogUtil.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
|
||||
lastQueryTime = queryTime
|
||||
return lastZeroSpeed
|
||||
}
|
||||
val sinceLastQueryInSeconds = sinceLastQueryIn / 1000.0
|
||||
|
||||
var proxyUplink = 0L
|
||||
var proxyDownlink = 0L
|
||||
var directUplink = 0L
|
||||
var directDownlink = 0L
|
||||
|
||||
CoreServiceManager.queryAllOutboundTrafficStats().forEach { stat ->
|
||||
when {
|
||||
stat.tag == AppConfig.TAG_DIRECT -> {
|
||||
when (stat.direction) {
|
||||
AppConfig.UPLINK -> directUplink += stat.value
|
||||
AppConfig.DOWNLINK -> directDownlink += stat.value
|
||||
}
|
||||
}
|
||||
|
||||
stat.tag.startsWith(AppConfig.TAG_PROXY) -> {
|
||||
when (stat.direction) {
|
||||
AppConfig.UPLINK -> proxyUplink += stat.value
|
||||
AppConfig.DOWNLINK -> proxyDownlink += stat.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val proxyTotal = proxyUplink + proxyDownlink
|
||||
val directTotal = directUplink + directDownlink
|
||||
val zeroSpeed = proxyTotal + directTotal == 0L
|
||||
if (!zeroSpeed || !lastZeroSpeed) {
|
||||
val text = StringBuilder()
|
||||
appendSpeedString(
|
||||
text, AppConfig.TAG_PROXY,
|
||||
proxyUplink / sinceLastQueryInSeconds,
|
||||
proxyDownlink / sinceLastQueryInSeconds
|
||||
)
|
||||
|
||||
appendSpeedString(
|
||||
text, AppConfig.TAG_DIRECT,
|
||||
directUplink / sinceLastQueryInSeconds,
|
||||
directDownlink / sinceLastQueryInSeconds
|
||||
)
|
||||
updateNotification(text.toString(), proxyTotal, directTotal)
|
||||
}
|
||||
lastQueryTime = queryTime
|
||||
return zeroSpeed
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service instance.
|
||||
* @return The service instance.
|
||||
|
||||
@@ -12,10 +12,10 @@ import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
import com.v2ray.ang.AppConfig.VPN
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.RulesetItem
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.RulesetItem
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.Language
|
||||
import com.v2ray.ang.enums.RoutingType
|
||||
@@ -73,7 +73,7 @@ object SettingsManager {
|
||||
return null
|
||||
}
|
||||
|
||||
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java)?.toMutableList()
|
||||
return JsonUtil.fromJsonSafe(assets, Array<RulesetItem>::class.java)?.toMutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +97,7 @@ object SettingsManager {
|
||||
}
|
||||
|
||||
try {
|
||||
val rulesetList = JsonUtil.fromJson(content, Array<RulesetItem>::class.java)?.toMutableList()
|
||||
val rulesetList = JsonUtil.fromJsonSafe(content, Array<RulesetItem>::class.java)?.toMutableList()
|
||||
if (rulesetList.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
@@ -191,7 +191,7 @@ object SettingsManager {
|
||||
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 v2rayConfig = JsonUtil.fromJsonSafe(raw, V2rayConfig::class.java)
|
||||
val exist = v2rayConfig?.routing?.rules?.filter { it.outboundTag == TAG_DIRECT }?.any {
|
||||
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
|
||||
}
|
||||
@@ -246,6 +246,20 @@ object SettingsManager {
|
||||
.firstOrNull { it.remarks == remarks }
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects non-empty profile remarks while excluding specific config types.
|
||||
*/
|
||||
fun getProfileRemarks(excludeConfigTypes: Set<EConfigType> = setOf(EConfigType.CUSTOM)): List<String> {
|
||||
return decodeAllServerList()
|
||||
.asSequence()
|
||||
.mapNotNull { guid -> decodeServerConfig(guid) }
|
||||
.filter { profile -> profile.configType !in excludeConfigTypes }
|
||||
.map { it.remarks.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.distinct()
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the subscription.
|
||||
* If there are no remaining subscriptions,
|
||||
@@ -473,6 +487,13 @@ object SettingsManager {
|
||||
return mode == null || mode == VPN
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a root (system-wide) run mode is selected.
|
||||
*/
|
||||
fun isRootMode(): Boolean {
|
||||
return MmkvManager.decodeSettingsBool(AppConfig.PREF_ROOT_MODE_ENABLE, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if process routing can be used.
|
||||
*/
|
||||
@@ -513,6 +534,7 @@ object SettingsManager {
|
||||
ensureDefaultValue(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
|
||||
ensureDefaultValue(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
|
||||
ensureDefaultValue(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
||||
ensureDefaultValue(AppConfig.PREF_FRAGMENT_MAXSPLIT, "10")
|
||||
}
|
||||
|
||||
private fun ensureDefaultValue(key: String, default: String) {
|
||||
@@ -571,7 +593,7 @@ object SettingsManager {
|
||||
return
|
||||
}
|
||||
|
||||
val guids = JsonUtil.fromJson(oldJson, Array<String>::class.java) ?: run {
|
||||
val guids = JsonUtil.fromJsonSafe(oldJson, Array<String>::class.java) ?: run {
|
||||
MmkvManager.encodeSettings(migrationKey, true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package com.v2ray.ang.handler
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.IPAPIInfo
|
||||
import com.v2ray.ang.dto.UrlContentRequest
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
@@ -14,29 +13,6 @@ import java.net.UnknownHostException
|
||||
|
||||
object SpeedtestManager {
|
||||
|
||||
private val tcpTestingSockets = ArrayList<Socket?>()
|
||||
|
||||
/**
|
||||
* Measures the TCP connection time to a given URL and port.
|
||||
*
|
||||
* @param url The URL to connect to.
|
||||
* @param port The port to connect to.
|
||||
* @return The connection time in milliseconds, or -1 if the connection failed.
|
||||
*/
|
||||
suspend fun tcping(url: String, port: Int): Long {
|
||||
var time = -1L
|
||||
for (k in 0 until 2) {
|
||||
val one = socketConnectTime(url, port)
|
||||
if (!currentCoroutineContext().isActive) {
|
||||
break
|
||||
}
|
||||
if (one != -1L && (time == -1L || one < time)) {
|
||||
time = one
|
||||
}
|
||||
}
|
||||
return time
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the time taken to establish a TCP connection to a given URL and port.
|
||||
*
|
||||
@@ -44,42 +20,34 @@ object SpeedtestManager {
|
||||
* @param port The port to connect to.
|
||||
* @return The connection time in milliseconds, or -1 if the connection failed.
|
||||
*/
|
||||
fun socketConnectTime(url: String, port: Int): Long {
|
||||
fun socketConnectTime(url: String, port: Int, timeoutMs: Int = 1500): Long {
|
||||
var socket: Socket? = null
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
try {
|
||||
val socket = Socket()
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.add(socket)
|
||||
}
|
||||
val start = System.currentTimeMillis()
|
||||
socket.connect(InetSocketAddress(url, port), 3000)
|
||||
val time = System.currentTimeMillis() - start
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.remove(socket)
|
||||
}
|
||||
socket.close()
|
||||
return time
|
||||
socket = Socket()
|
||||
socket.connect(InetSocketAddress(url, port), timeoutMs)
|
||||
|
||||
return System.currentTimeMillis() - start
|
||||
} catch (e: UnknownHostException) {
|
||||
LogUtil.e(AppConfig.TAG, "Unknown host: $url", e)
|
||||
} catch (e: IOException) {
|
||||
LogUtil.e(AppConfig.TAG, "socketConnectTime IOException: $e")
|
||||
LogUtil.e(AppConfig.TAG, "socketConnectTime IOException: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
|
||||
} finally {
|
||||
socket?.let { s ->
|
||||
try {
|
||||
if (!s.isClosed) {
|
||||
s.close()
|
||||
}
|
||||
} catch (closeEx: IOException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all TCP sockets that are currently being tested.
|
||||
*/
|
||||
fun closeAllTcpSockets() {
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.forEach {
|
||||
it?.close()
|
||||
}
|
||||
tcpTestingSockets.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun getRemoteIPInfo(): String? {
|
||||
val url = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL)
|
||||
.takeIf { !it.isNullOrBlank() } ?: AppConfig.IP_API_URL
|
||||
@@ -88,8 +56,16 @@ object SpeedtestManager {
|
||||
val proxyPassword = SettingsManager.getSocksPassword()
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
if (httpPort == 0) return null
|
||||
val content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: return null
|
||||
val ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
|
||||
val content = HttpUtil.getUrlContent(
|
||||
UrlContentRequest(
|
||||
url = url,
|
||||
timeout = 5000,
|
||||
httpPort = httpPort,
|
||||
proxyUsername = proxyUsername,
|
||||
proxyPassword = proxyPassword
|
||||
)
|
||||
) ?: return null
|
||||
val ipInfo = JsonUtil.fromJsonSafe(content, IPAPIInfo::class.java) ?: return null
|
||||
|
||||
val ip = listOf(
|
||||
ipInfo.ip,
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.work.workDataOf
|
||||
import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.SubscriptionCache
|
||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||
import com.v2ray.ang.enums.NotificationChannelType
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.NotificationHelper
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.dto.UrlContentRequest
|
||||
import com.v2ray.ang.extension.concatUrl
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
@@ -23,19 +24,32 @@ object UpdateCheckerManager {
|
||||
val proxyUsername = SettingsManager.getSocksUsername()
|
||||
val proxyPassword = SettingsManager.getSocksPassword()
|
||||
|
||||
var response = HttpUtil.getUrlContent(url, 5000)
|
||||
var response = HttpUtil.getUrlContent(
|
||||
UrlContentRequest(
|
||||
url = url,
|
||||
timeout = 5000
|
||||
)
|
||||
)
|
||||
if (response.isNullOrEmpty()) {
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
response = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword)
|
||||
response = HttpUtil.getUrlContent(
|
||||
UrlContentRequest(
|
||||
url = url,
|
||||
timeout = 5000,
|
||||
httpPort = httpPort,
|
||||
proxyUsername = proxyUsername,
|
||||
proxyPassword = proxyPassword
|
||||
)
|
||||
)
|
||||
?: throw IllegalStateException("Failed to get response")
|
||||
}
|
||||
|
||||
val latestRelease = if (includePreRelease) {
|
||||
JsonUtil.fromJson(response, Array<GitHubRelease>::class.java)
|
||||
JsonUtil.fromJsonSafe(response, Array<GitHubRelease>::class.java)
|
||||
?.firstOrNull()
|
||||
?: throw IllegalStateException("No pre-release found")
|
||||
} else {
|
||||
JsonUtil.fromJson(response, GitHubRelease::class.java)
|
||||
JsonUtil.fromJsonSafe(response, GitHubRelease::class.java)
|
||||
}
|
||||
if (latestRelease == null) {
|
||||
return@withContext CheckUpdateResult(hasUpdate = false)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.v2ray.ang.handler
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.WebDavConfig
|
||||
import com.v2ray.ang.dto.entities.WebDavConfig
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.v2ray.ang.root
|
||||
|
||||
import android.content.Context
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
|
||||
object RootLanSharing {
|
||||
|
||||
private var lanSharingStarted = false
|
||||
private var lanShareJob: Job? = null
|
||||
|
||||
/**
|
||||
* Optional root feature: share the proxy with tethered LAN/USB clients while the
|
||||
* device itself stays on the VpnService. Runs a dedicated client hev-socks5-tunnel
|
||||
* off the main thread so the su calls don't block service startup.
|
||||
* The cheap, usually-false preference is checked first so the common path
|
||||
* short-circuits before touching root state.
|
||||
*/
|
||||
fun startClientSharing(context: Context): Boolean {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_ROOT_LAN_SHARING) && RootManager.cachedRoot()) {
|
||||
if (lanShareJob != null) return false
|
||||
|
||||
lanSharingStarted = true
|
||||
lanShareJob = CoroutineScope(Dispatchers.IO).launch { RootProxyManager.startClientSharing(context) }
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove LAN/tethering sharing rules + helper before stopping the core. Wait for the
|
||||
* async setup to finish first, otherwise a stop during setup tears down before the
|
||||
* rules are installed and they leak (orphan FORWARD/policy-routing rules + client tun).
|
||||
*/
|
||||
fun stopClientSharing(context: Context) {
|
||||
if (!lanSharingStarted) return
|
||||
|
||||
lanSharingStarted = false
|
||||
runBlocking { lanShareJob?.cancelAndJoin() }
|
||||
lanShareJob = null
|
||||
RootProxyManager.stop(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.v2ray.ang.root
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.root.RootManager.isRootAvailable
|
||||
import com.v2ray.ang.root.RootManager.refresh
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Detects whether the device grants root (`su`) access.
|
||||
*
|
||||
* The result is cached after the first successful probe. Probing spawns `su` and
|
||||
* blocks, so [refresh] (a suspending call on [Dispatchers.IO]) should be used from UI
|
||||
* code; [isRootAvailable] may block and must not be called on the main thread the first time.
|
||||
*/
|
||||
object RootManager {
|
||||
|
||||
@Volatile
|
||||
private var cached: Boolean? = null
|
||||
|
||||
/** Last known result without probing. Defaults to false when never probed. */
|
||||
fun cachedRoot(): Boolean = cached ?: false
|
||||
|
||||
/**
|
||||
* Returns whether root is available, probing once if unknown.
|
||||
* May block while `su` is spawned; avoid calling on the main thread before a probe.
|
||||
*/
|
||||
fun isRootAvailable(forceRefresh: Boolean = false): Boolean {
|
||||
if (!forceRefresh) cached?.let { return it }
|
||||
val result = probe()
|
||||
cached = result
|
||||
return result
|
||||
}
|
||||
|
||||
/** Probes root off the main thread, updates the cache, and returns the result. */
|
||||
suspend fun refresh(): Boolean = withContext(Dispatchers.IO) {
|
||||
val result = probe()
|
||||
cached = result
|
||||
result
|
||||
}
|
||||
|
||||
private fun probe(): Boolean {
|
||||
return try {
|
||||
val process = ProcessBuilder("su", "-c", "id -u")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
val output = process.inputStream.bufferedReader().use { it.readText() }.trim()
|
||||
val finished = process.waitFor(10, TimeUnit.SECONDS)
|
||||
if (!finished) {
|
||||
process.destroy()
|
||||
LogUtil.w(AppConfig.TAG, "RootManager: su probe timed out")
|
||||
return false
|
||||
}
|
||||
val isRoot = process.exitValue() == 0 && output.lineSequence().lastOrNull()?.trim() == "0"
|
||||
LogUtil.i(AppConfig.TAG, "RootManager: root available = $isRoot")
|
||||
isRoot
|
||||
} catch (e: Exception) {
|
||||
LogUtil.w(AppConfig.TAG, "RootManager: no root access (${e.message})")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
package com.v2ray.ang.root
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Process
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.root.RootProxyManager.TABLE
|
||||
import com.v2ray.ang.root.RootProxyManager.TUN
|
||||
import com.v2ray.ang.root.RootProxyManager.teardown
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.PackageUidResolver
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Installs and removes the iptables / ip-rule routing that pushes system-wide traffic
|
||||
*
|
||||
* A bundled `hev-socks5-tunnel` binary (run as root) creates a tun device and forwards it to
|
||||
* the in-process core's SOCKS inbound; a mangle MARK chain plus a dedicated routing table /
|
||||
* ip rule steer all traffic into the tun. Full TCP + UDP.
|
||||
*
|
||||
* All rules live in dedicated chains ([AppConfig.ROOT_IPTABLES_CHAIN] in the mangle
|
||||
* table, [AppConfig.ROOT_FWD_CHAIN] for LAN sharing) plus a dedicated routing table, so
|
||||
* [teardown] is a clean, bounded flush. Teardown runs before every setup (to clear stale
|
||||
* rules) and on every stop path — leaving rules behind after the core dies would break
|
||||
* the device's connectivity.
|
||||
*/
|
||||
object RootProxyManager {
|
||||
|
||||
private const val CHAIN = AppConfig.ROOT_IPTABLES_CHAIN
|
||||
private const val TUN = AppConfig.ROOT_TUN_NAME
|
||||
private const val TABLE = AppConfig.ROOT_ROUTE_TABLE
|
||||
private const val PRIORITY = AppConfig.ROOT_RULE_PRIORITY
|
||||
private const val FWMARK = AppConfig.ROOT_FWMARK
|
||||
private const val MARK = AppConfig.ROOT_MARK_ROUTE
|
||||
|
||||
// Local / private / multicast destinations that must never be proxied.
|
||||
private val bypassCidrs = listOf(
|
||||
"0.0.0.0/8", "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16",
|
||||
"172.16.0.0/12", "192.168.0.0/16", "224.0.0.0/4", "240.0.0.0/4"
|
||||
)
|
||||
|
||||
// IPv6 equivalents (loopback, link-local, ULA/private, multicast). Feeding the v4 list
|
||||
// above to ip6tables silently fails, so the v6 chain needs its own.
|
||||
private val bypassCidrsV6 = listOf(
|
||||
"::1/128", "fe80::/10", "fc00::/7", "ff00::/8"
|
||||
)
|
||||
|
||||
fun start(context: Context): Boolean {
|
||||
teardown(context)
|
||||
val script = buildTun2socksSetup(context) ?: return false
|
||||
val result = RootShell.runScript(context, "setup_rules.sh", script)
|
||||
if (!result.success) {
|
||||
LogUtil.e(AppConfig.TAG, "RootProxyManager: setup failed, rolling back:\n${result.output}")
|
||||
teardown(context)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up LAN/tethering sharing while the device itself uses another mode (e.g. VPN
|
||||
* mode). Runs a dedicated client tun2socks into the in-process core's SOCKS inbound
|
||||
* and forwards tethered clients into it, WITHOUT capturing the device's own traffic
|
||||
* (that keeps flowing through the VpnService). Requires root.
|
||||
*/
|
||||
fun startClientSharing(context: Context): Boolean {
|
||||
teardown(context)
|
||||
val script = buildTun2socksSetup(context, captureDeviceTraffic = false, forceLanShare = true)
|
||||
?: return false
|
||||
val result = RootShell.runScript(context, "setup_rules.sh", script)
|
||||
if (!result.success) {
|
||||
LogUtil.e(AppConfig.TAG, "RootProxyManager: client sharing setup failed:\n${result.output}")
|
||||
teardown(context)
|
||||
return false
|
||||
}
|
||||
LogUtil.i(AppConfig.TAG, "RootProxyManager: LAN client sharing installed")
|
||||
return true
|
||||
}
|
||||
|
||||
/** Remove all rules and stop helper processes. Safe to call repeatedly. */
|
||||
fun stop(context: Context) {
|
||||
teardown(context)
|
||||
LogUtil.i(AppConfig.TAG, "RootProxyManager: rules removed")
|
||||
}
|
||||
|
||||
private fun teardown(context: Context) {
|
||||
RootShell.runScript(context, "teardown_rules.sh", buildTeardown(context))
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- TUN2SOCKS
|
||||
|
||||
/**
|
||||
* @param captureDeviceTraffic when true (Root mode) the device's own OUTPUT traffic is
|
||||
* marked into the tun. When false (VPN-mode LAN sharing) the device keeps using the
|
||||
* VpnService and only forwarded clients are routed into this tun.
|
||||
* @param forceLanShare force the LAN/tethering forward rules on regardless of the pref
|
||||
* (used by VPN-mode sharing, where the whole point is forwarding clients).
|
||||
*/
|
||||
private fun buildTun2socksSetup(
|
||||
context: Context,
|
||||
captureDeviceTraffic: Boolean = true,
|
||||
forceLanShare: Boolean = false,
|
||||
): String? {
|
||||
val bin = File(context.applicationInfo.nativeLibraryDir, AppConfig.ROOT_TUN2SOCKS_BIN)
|
||||
if (!bin.exists()) {
|
||||
LogUtil.e(AppConfig.TAG, "RootProxyManager: hev-socks5-tunnel binary missing at ${bin.absolutePath}")
|
||||
return null
|
||||
}
|
||||
val appUid = context.applicationInfo.uid
|
||||
val port = SettingsManager.getSocksPort()
|
||||
val runDir = File(context.filesDir, AppConfig.ROOT_RUNTIME_DIR).apply { mkdirs() }
|
||||
val pidFile = File(runDir, "tun2socks.pid").absolutePath
|
||||
val logFile = File(runDir, "tun2socks.log").absolutePath
|
||||
val cfgFile = File(runDir, "tun2socks.yml").absolutePath
|
||||
val oomGuardPid = File(runDir, "oomguard.pid").absolutePath
|
||||
val ipv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED)
|
||||
val lanShare = forceLanShare || MmkvManager.decodeSettingsBool(AppConfig.PREF_ROOT_LAN_SHARING)
|
||||
val corePid = Process.myPid()
|
||||
|
||||
// Per-app proxy/bypass (mirrors what VpnService does via allowed/disallowed apps).
|
||||
val perAppEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)
|
||||
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
|
||||
val selectedUids = if (perAppEnabled) {
|
||||
val pkgs = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)?.toList().orEmpty()
|
||||
if (pkgs.isNotEmpty()) PackageUidResolver.packageNamesToUids(context, pkgs) else emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
return buildString {
|
||||
appendLine("set -e")
|
||||
appendLine("BIN='${bin.absolutePath}'")
|
||||
// Protect the core (this app process) from the Android low-memory killer.
|
||||
// system_server keeps recomputing oom_score_adj for app processes, so a single
|
||||
// write would be reverted — re-pin it from a small root loop instead.
|
||||
appendLine("nohup sh -c 'while true; do echo ${AppConfig.ROOT_OOM_SCORE} > /proc/$corePid/oom_score_adj 2>/dev/null; sleep 5; done' >/dev/null 2>&1 &")
|
||||
appendLine("echo \$! > '$oomGuardPid'")
|
||||
// tun device node
|
||||
appendLine("if [ ! -e /dev/net/tun ]; then mkdir -p /dev/net; mknod /dev/net/tun c 10 200; chmod 666 /dev/net/tun; fi")
|
||||
// hev-socks5-tunnel config: it creates the tun ($TUN) itself and forwards it to the
|
||||
// in-process core's SOCKS inbound on loopback. MTU comes from the existing VPN MTU
|
||||
// setting. No fwmark on hev's sockets: its only upstream connection is to 127.0.0.1
|
||||
// (loopback, already RETURNed by the 127.0.0.0/8 bypass) and the core's real outbound
|
||||
// runs as the app uid (RETURNed by the uid-owner rule), so traffic can't loop.
|
||||
appendLine("cat > '$cfgFile' <<'HEVCFG'")
|
||||
append(buildHevConfig(port, ipv6))
|
||||
appendLine("HEVCFG")
|
||||
appendLine("nohup \"\$BIN\" '$cfgFile' >'$logFile' 2>&1 &")
|
||||
appendLine("T2S_PID=\$!")
|
||||
appendLine("echo \$T2S_PID > '$pidFile'")
|
||||
appendLine("echo ${AppConfig.ROOT_OOM_SCORE} > /proc/\$T2S_PID/oom_score_adj 2>/dev/null || true")
|
||||
// wait for the interface hev creates to appear
|
||||
appendLine("i=0; while [ \$i -lt 20 ]; do ip link show $TUN >/dev/null 2>&1 && break; sleep 0.3; i=\$((i+1)); done")
|
||||
appendLine("ip link show $TUN >/dev/null 2>&1 || { echo 'tun device did not come up'; cat '$logFile' 2>/dev/null; exit 1; }")
|
||||
// relax reverse-path filtering for the tun
|
||||
appendLine("echo 0 > /proc/sys/net/ipv4/conf/$TUN/rp_filter 2>/dev/null || true")
|
||||
appendLine("echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter 2>/dev/null || true")
|
||||
// address + default route in a dedicated table
|
||||
appendLine("ip addr add ${AppConfig.ROOT_TUN_ADDR_V4} dev $TUN 2>/dev/null || true")
|
||||
appendLine("ip link set dev $TUN up")
|
||||
appendLine("ip route replace default dev $TUN table $TABLE")
|
||||
appendLine("ip rule add fwmark $MARK table $TABLE priority $PRIORITY")
|
||||
// mark the device's own packets into the tun (Root mode only)
|
||||
if (captureDeviceTraffic) {
|
||||
append(buildMangleMarking("iptables", appUid, perAppEnabled, bypassApps, selectedUids))
|
||||
}
|
||||
// optionally route hotspot / USB-tethered clients through the tun too
|
||||
if (lanShare) {
|
||||
append(buildLanShareSetup(captureDeviceTraffic, ipv6))
|
||||
}
|
||||
if (captureDeviceTraffic) {
|
||||
// IPv6 is best-effort: never fail the (working) IPv4 setup over it.
|
||||
appendLine("set +e")
|
||||
if (ipv6) {
|
||||
// route the device's v6 into the tun, same as v4
|
||||
appendLine("ip -6 addr add ${AppConfig.ROOT_TUN_ADDR_V6} dev $TUN 2>/dev/null || true")
|
||||
appendLine("ip -6 route replace default dev $TUN table $TABLE 2>/dev/null || true")
|
||||
appendLine("ip -6 rule add fwmark $MARK table $TABLE priority $PRIORITY 2>/dev/null || true")
|
||||
append(buildMangleMarking("ip6tables", appUid, perAppEnabled, bypassApps, selectedUids))
|
||||
} else {
|
||||
// v6 disabled: blackhole native v6 egress for the captured apps so they
|
||||
// fall back to v4-through-proxy, matching what a v4-only VpnService does.
|
||||
append(buildV6Blackhole(appUid, perAppEnabled, bypassApps, selectedUids))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* hev-socks5-tunnel YAML config. hev creates the tun device named [TUN] itself, assigns it
|
||||
* the tun addresses, and forwards everything it receives to the core's SOCKS inbound on
|
||||
* loopback (TCP + UDP). MTU is taken from the existing VPN MTU setting. v6 is only given a
|
||||
* tun address when IPv6 is enabled; whether v6 actually flows in is decided separately by
|
||||
* the v6 route into [TABLE].
|
||||
*/
|
||||
private fun buildHevConfig(socksPort: Int, ipv6: Boolean): String {
|
||||
val v4 = AppConfig.ROOT_TUN_ADDR_V4.substringBefore("/")
|
||||
val v6 = AppConfig.ROOT_TUN_ADDR_V6.substringBefore("/")
|
||||
return buildString {
|
||||
appendLine("tunnel:")
|
||||
appendLine(" name: '$TUN'")
|
||||
appendLine(" mtu: ${SettingsManager.getVpnMtu()}")
|
||||
appendLine(" multi-queue: true")
|
||||
appendLine(" ipv4: '$v4'")
|
||||
if (ipv6) appendLine(" ipv6: '$v6'")
|
||||
appendLine("socks5:")
|
||||
appendLine(" port: $socksPort")
|
||||
appendLine(" address: '${AppConfig.LOOPBACK}'")
|
||||
appendLine(" udp: 'udp'")
|
||||
appendLine(" tcp-fastopen: true")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mangle OUTPUT marking chain (ipv4/ipv6). Mirrors VpnService's capture behavior:
|
||||
* - all-apps (no per-app): mark EVERY remaining uid (incl uid 0 + all system uids), so
|
||||
* nothing is missed;
|
||||
* - bypass mode: the selected apps go fully direct, everything else is captured;
|
||||
* - proxy mode: only the selected apps are captured.
|
||||
*/
|
||||
private fun buildMangleMarking(
|
||||
cmd: String,
|
||||
appUid: Int,
|
||||
perAppEnabled: Boolean,
|
||||
bypassApps: Boolean,
|
||||
selectedUids: List<String>,
|
||||
): String {
|
||||
val allowMode = perAppEnabled && !bypassApps
|
||||
val bypassSelected = perAppEnabled && bypassApps && selectedUids.isNotEmpty()
|
||||
return buildString {
|
||||
appendLine("$cmd -t mangle -N $CHAIN 2>/dev/null || true")
|
||||
appendLine("$cmd -t mangle -F $CHAIN")
|
||||
// the app's own core traffic (the real outbound) must not loop back into the tun.
|
||||
// The $FWMARK RETURN is kept defensively (hev itself only talks to loopback, which
|
||||
// the 127.0.0.0/8 bypass below already RETURNs).
|
||||
appendLine("$cmd -t mangle -A $CHAIN -m mark --mark $FWMARK -j RETURN")
|
||||
appendLine("$cmd -t mangle -A $CHAIN -m owner --uid-owner $appUid -j RETURN")
|
||||
// bypass mode: selected apps go fully direct (incl their DNS)
|
||||
if (bypassSelected) {
|
||||
selectedUids.forEach { appendLine("$cmd -t mangle -A $CHAIN -m owner --uid-owner $it -j RETURN") }
|
||||
}
|
||||
// Route DNS through the core for ALL modes, with no uid filter. On Android the
|
||||
// DNS query is sent by netd (a shared system uid) on behalf of the app, not under
|
||||
// the app's own uid, so it can't be attributed to a selected uid via owner-match.
|
||||
// This MUST also run before the LAN-bypass RETURNs below, otherwise a query to a
|
||||
// LAN/router resolver (192.168.x / 10.x) would be returned direct and resolved by
|
||||
// the local ISP resolver (DNS leak + CDN mis-resolution, e.g. Instagram media).
|
||||
// The MARK survives a later RETURN, so the marked query still routes into the tun.
|
||||
appendLine("$cmd -t mangle -A $CHAIN -p udp --dport 53 -j MARK --set-xmark $MARK")
|
||||
appendLine("$cmd -t mangle -A $CHAIN -p tcp --dport 53 -j MARK --set-xmark $MARK")
|
||||
// keep LAN / private destinations direct (per-family CIDR list)
|
||||
val cidrs = if (cmd == "ip6tables") bypassCidrsV6 else bypassCidrs
|
||||
cidrs.forEach { appendLine("$cmd -t mangle -A $CHAIN -d $it -j RETURN") }
|
||||
if (allowMode) {
|
||||
// Proxy ONLY the explicitly selected apps. If nothing resolved (e.g. the
|
||||
// selected packages failed to resolve to uids at early boot), mark nothing
|
||||
// instead of falling through to the catch-all below: a fail-open here would
|
||||
// tunnel every unselected app — both a privacy leak and the "per-app proxies
|
||||
// everything after a reboot" bug.
|
||||
selectedUids.forEach { appendLine("$cmd -t mangle -A $CHAIN -m owner --uid-owner $it -j MARK --set-xmark $MARK") }
|
||||
} else {
|
||||
// all-apps mode (per-app off) or bypass mode: capture EVERY remaining uid
|
||||
// (incl uid 0 + system uids)
|
||||
appendLine("$cmd -t mangle -A $CHAIN -j MARK --set-xmark $MARK")
|
||||
}
|
||||
appendLine("$cmd -t mangle -D OUTPUT -j $CHAIN 2>/dev/null || true")
|
||||
appendLine("$cmd -t mangle -A OUTPUT -j $CHAIN")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blackhole native IPv6 egress for the captured app population when IPv6 is NOT routed
|
||||
* into the tun. A v4-only VpnService has no v6 route, so the kernel rejects apps' v6 and
|
||||
* they fall back to IPv4; Root mode has to reproduce that explicitly, otherwise v6-capable
|
||||
* apps reach destinations natively, bypassing the proxy / leaking. REJECT (not DROP) gives
|
||||
* an instant failure so happy-eyeballs falls back to v4 without a timeout.
|
||||
*
|
||||
* Exemptions mirror the v4 chain: the tun2socks helper (fwmark), the app's own core (uid),
|
||||
* loopback, link-local / multicast (NDP/RA/MLD) and ULA/LAN destinations. Per-app selection
|
||||
* is honored: in bypass mode the bypassed apps keep native v6; in proxy mode only the
|
||||
* selected apps lose v6 (everything else stays fully direct).
|
||||
*/
|
||||
private fun buildV6Blackhole(
|
||||
appUid: Int,
|
||||
perAppEnabled: Boolean,
|
||||
bypassApps: Boolean,
|
||||
selectedUids: List<String>,
|
||||
): String {
|
||||
val chain = AppConfig.ROOT_V6_CHAIN
|
||||
val allowMode = perAppEnabled && !bypassApps
|
||||
val bypassSelected = perAppEnabled && bypassApps && selectedUids.isNotEmpty()
|
||||
val reject = "-j REJECT --reject-with icmp6-adm-prohibited"
|
||||
return buildString {
|
||||
appendLine("ip6tables -t filter -N $chain 2>/dev/null || true")
|
||||
appendLine("ip6tables -t filter -F $chain")
|
||||
// never touch the helper, the core, loopback, NDP/link-local/multicast or LAN
|
||||
appendLine("ip6tables -t filter -A $chain -m mark --mark $FWMARK -j RETURN")
|
||||
appendLine("ip6tables -t filter -A $chain -m owner --uid-owner $appUid -j RETURN")
|
||||
appendLine("ip6tables -t filter -A $chain -o lo -j RETURN")
|
||||
bypassCidrsV6.forEach { appendLine("ip6tables -t filter -A $chain -d $it -j RETURN") }
|
||||
// bypass mode: bypassed apps keep their native v6
|
||||
if (bypassSelected) {
|
||||
selectedUids.forEach { appendLine("ip6tables -t filter -A $chain -m owner --uid-owner $it -j RETURN") }
|
||||
}
|
||||
if (allowMode) {
|
||||
// proxy mode: only the selected apps lose v6 (so they fall back to v4-via-proxy).
|
||||
// None resolved -> reject nothing, mirroring the v4 chain's fail-closed handling.
|
||||
selectedUids.forEach { appendLine("ip6tables -t filter -A $chain -m owner --uid-owner $it $reject") }
|
||||
} else {
|
||||
// all-apps / bypass: reject everyone left
|
||||
appendLine("ip6tables -t filter -A $chain $reject")
|
||||
}
|
||||
appendLine("ip6tables -t filter -D OUTPUT -j $chain 2>/dev/null || true")
|
||||
appendLine("ip6tables -t filter -A OUTPUT -j $chain")
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------- LAN / tethering sharing
|
||||
|
||||
/**
|
||||
* Route Wi-Fi-hotspot / USB-tethered clients through the tun as well (ipv4).
|
||||
* Best-effort: wrapped in `set +e` so a failure here never breaks the working proxy.
|
||||
* Mirrors Magic_V2Ray's hotspot rules (FORWARD accept, DNS DNAT, source-based policy
|
||||
* routing for private client ranges, MSS clamp).
|
||||
*/
|
||||
private fun buildLanShareSetup(captureDeviceTraffic: Boolean, ipv6: Boolean): String {
|
||||
val fwd = AppConfig.ROOT_FWD_CHAIN
|
||||
val dnsChain = AppConfig.ROOT_DNS_CHAIN
|
||||
val v6fwd = AppConfig.ROOT_V6_FWD_CHAIN
|
||||
val v6pre = AppConfig.ROOT_V6_PRE_CHAIN
|
||||
// Use the app's configured remote DNS (first plain IPv4) as the DNAT target for
|
||||
// tethered clients; fall back to the default when it's a DoH/DoT/IPv6 value that
|
||||
// can't be a DNAT target.
|
||||
val dns = SettingsManager.getRemoteDnsServers()
|
||||
.firstOrNull { Utils.isPureIpAddress(it) && !it.contains(":") }
|
||||
?: AppConfig.ROOT_LAN_DNS
|
||||
val lanCidrs = listOf("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
|
||||
return buildString {
|
||||
appendLine("set +e")
|
||||
appendLine("echo 1 > /proc/sys/net/ipv4/ip_forward 2>/dev/null || true")
|
||||
// forward traffic to/from the tun
|
||||
appendLine("iptables -N $fwd 2>/dev/null || true")
|
||||
appendLine("iptables -F $fwd")
|
||||
appendLine("iptables -A $fwd -i $TUN -j ACCEPT")
|
||||
appendLine("iptables -A $fwd -o $TUN -j ACCEPT")
|
||||
appendLine("iptables -D FORWARD -j $fwd 2>/dev/null || true")
|
||||
appendLine("iptables -I FORWARD -j $fwd")
|
||||
// clamp MSS to avoid TLS fragmentation overhead through the tunnel
|
||||
appendLine("iptables -t mangle -D FORWARD -o $TUN -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1350 2>/dev/null || true")
|
||||
appendLine("iptables -t mangle -A FORWARD -o $TUN -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1350")
|
||||
// hijack tethered clients' DNS into a dedicated chain so it resolves through the
|
||||
// tunnel; the chain keeps teardown independent of the resolver IP
|
||||
appendLine("iptables -t nat -N $dnsChain 2>/dev/null || true")
|
||||
appendLine("iptables -t nat -F $dnsChain")
|
||||
lanCidrs.forEach {
|
||||
appendLine("iptables -t nat -A $dnsChain ! -i $TUN -d $it -p udp --dport 53 -j DNAT --to $dns")
|
||||
}
|
||||
appendLine("iptables -t nat -D PREROUTING -j $dnsChain 2>/dev/null || true")
|
||||
appendLine("iptables -t nat -A PREROUTING -j $dnsChain")
|
||||
// policy routing: return-path via main, LAN direct, the rest via the tun table
|
||||
appendLine("ip rule add iif lo goto 6000 pref 5000 2>/dev/null || true")
|
||||
appendLine("ip rule add iif $TUN lookup main suppress_prefixlength 0 pref 5010 2>/dev/null || true")
|
||||
appendLine("ip rule add iif $TUN goto 6000 pref 5020 2>/dev/null || true")
|
||||
appendLine("ip rule add to 10.0.0.0/8 lookup main pref 5025 2>/dev/null || true")
|
||||
appendLine("ip rule add to 172.16.0.0/12 lookup main pref 5026 2>/dev/null || true")
|
||||
appendLine("ip rule add to 192.168.0.0/16 lookup main pref 5027 2>/dev/null || true")
|
||||
appendLine("ip rule add from 10.0.0.0/8 lookup $TABLE pref 5030 2>/dev/null || true")
|
||||
appendLine("ip rule add from 172.16.0.0/12 lookup $TABLE pref 5040 2>/dev/null || true")
|
||||
appendLine("ip rule add from 192.168.0.0/16 lookup $TABLE pref 5050 2>/dev/null || true")
|
||||
appendLine("ip rule add nop pref 6000 2>/dev/null || true")
|
||||
|
||||
// ---------------------------------------------------------- IPv6 clients
|
||||
// Tethered/hotspot clients get a native (RA-assigned) global IPv6. The IPv4 rules
|
||||
// above don't touch it, so it egresses the upstream interface directly, bypassing
|
||||
// the proxy = IPv6 leak. Handle it explicitly (mirrors vincentng295/Magic_V2Ray
|
||||
// cae4f7f): route it through the tun when v6 is enabled, reject it when it isn't.
|
||||
appendLine("ip6tables -N $v6fwd 2>/dev/null || true")
|
||||
appendLine("ip6tables -F $v6fwd")
|
||||
appendLine("ip6tables -D FORWARD -j $v6fwd 2>/dev/null || true")
|
||||
appendLine("ip6tables -I FORWARD -j $v6fwd")
|
||||
if (ipv6) {
|
||||
// When the device itself isn't capturing v6 (VPN-mode sharing) the tun table
|
||||
// has no v6 default and the tun has no v6 address — add them so marked client
|
||||
// v6 has somewhere to go. In Root mode the device-capture block already did.
|
||||
if (!captureDeviceTraffic) {
|
||||
appendLine("ip -6 addr add ${AppConfig.ROOT_TUN_ADDR_V6} dev $TUN 2>/dev/null || true")
|
||||
appendLine("ip -6 route replace default dev $TUN table $TABLE 2>/dev/null || true")
|
||||
appendLine("ip -6 rule add fwmark $MARK table $TABLE priority $PRIORITY 2>/dev/null || true")
|
||||
}
|
||||
// allow forwarding to/from the tun
|
||||
appendLine("ip6tables -A $v6fwd -i $TUN -j ACCEPT")
|
||||
appendLine("ip6tables -A $v6fwd -o $TUN -j ACCEPT")
|
||||
// mark forwarded (non-locally-sourced) client v6 into the tun table. DNS first
|
||||
// so a query to a LAN/router resolver is still tunneled (MARK survives RETURN);
|
||||
// keep loopback, link-local (NDP/RA) and ULA/multicast direct.
|
||||
appendLine("ip6tables -t mangle -N $v6pre 2>/dev/null || true")
|
||||
appendLine("ip6tables -t mangle -F $v6pre")
|
||||
appendLine("ip6tables -t mangle -A $v6pre ! -i $TUN -p udp --dport 53 -j MARK --set-xmark $MARK")
|
||||
appendLine("ip6tables -t mangle -A $v6pre ! -i $TUN -p tcp --dport 53 -j MARK --set-xmark $MARK")
|
||||
bypassCidrsV6.forEach { appendLine("ip6tables -t mangle -A $v6pre ! -i $TUN -d $it -j RETURN") }
|
||||
appendLine("ip6tables -t mangle -A $v6pre ! -i $TUN -j MARK --set-xmark $MARK")
|
||||
appendLine("ip6tables -t mangle -D PREROUTING -j $v6pre 2>/dev/null || true")
|
||||
appendLine("ip6tables -t mangle -A PREROUTING -j $v6pre")
|
||||
// fail closed: any forwarded v6 that wasn't marked into the tun (e.g. the
|
||||
// addrtype match is unavailable, or marking failed) is rejected rather than
|
||||
// leaked straight out the upstream interface.
|
||||
appendLine("ip6tables -A $v6fwd -j REJECT --reject-with icmp6-no-route")
|
||||
} else {
|
||||
// v6 disabled: reject forwarded clients' native v6 so it can't leak past the
|
||||
// proxy (the device's own v6 is blackholed separately in OUTPUT).
|
||||
appendLine("ip6tables -A $v6fwd -j REJECT --reject-with icmp6-no-route")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- teardown
|
||||
|
||||
private fun buildTeardown(context: Context): String {
|
||||
val runDir = File(context.filesDir, AppConfig.ROOT_RUNTIME_DIR)
|
||||
val pidFile = File(runDir, "tun2socks.pid").absolutePath
|
||||
val oomGuardPid = File(runDir, "oomguard.pid").absolutePath
|
||||
val corePid = Process.myPid()
|
||||
return buildString {
|
||||
// mangle (TUN2SOCKS), both families
|
||||
for (cmd in listOf("iptables", "ip6tables")) {
|
||||
appendLine("$cmd -t mangle -D OUTPUT -j $CHAIN 2>/dev/null || true")
|
||||
appendLine("$cmd -t mangle -F $CHAIN 2>/dev/null || true")
|
||||
appendLine("$cmd -t mangle -X $CHAIN 2>/dev/null || true")
|
||||
}
|
||||
// IPv6 blackhole chain (only set up when v6 is disabled; harmless if absent)
|
||||
appendLine("ip6tables -t filter -D OUTPUT -j ${AppConfig.ROOT_V6_CHAIN} 2>/dev/null || true")
|
||||
appendLine("ip6tables -t filter -F ${AppConfig.ROOT_V6_CHAIN} 2>/dev/null || true")
|
||||
appendLine("ip6tables -t filter -X ${AppConfig.ROOT_V6_CHAIN} 2>/dev/null || true")
|
||||
// routing rule + table
|
||||
appendLine("ip rule del fwmark $MARK table $TABLE priority $PRIORITY 2>/dev/null || true")
|
||||
appendLine("ip -6 rule del fwmark $MARK table $TABLE priority $PRIORITY 2>/dev/null || true")
|
||||
appendLine("ip route flush table $TABLE 2>/dev/null || true")
|
||||
appendLine("ip -6 route flush table $TABLE 2>/dev/null || true")
|
||||
// LAN / tethering sharing (always cleaned, harmless if it was never set up)
|
||||
appendLine("iptables -D FORWARD -j ${AppConfig.ROOT_FWD_CHAIN} 2>/dev/null || true")
|
||||
appendLine("iptables -F ${AppConfig.ROOT_FWD_CHAIN} 2>/dev/null || true")
|
||||
appendLine("iptables -X ${AppConfig.ROOT_FWD_CHAIN} 2>/dev/null || true")
|
||||
appendLine("iptables -t mangle -D FORWARD -o $TUN -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1350 2>/dev/null || true")
|
||||
appendLine("iptables -t nat -D PREROUTING -j ${AppConfig.ROOT_DNS_CHAIN} 2>/dev/null || true")
|
||||
appendLine("iptables -t nat -F ${AppConfig.ROOT_DNS_CHAIN} 2>/dev/null || true")
|
||||
appendLine("iptables -t nat -X ${AppConfig.ROOT_DNS_CHAIN} 2>/dev/null || true")
|
||||
// IPv6 LAN-sharing chains (forward accept/reject + forwarded-client marking)
|
||||
appendLine("ip6tables -D FORWARD -j ${AppConfig.ROOT_V6_FWD_CHAIN} 2>/dev/null || true")
|
||||
appendLine("ip6tables -F ${AppConfig.ROOT_V6_FWD_CHAIN} 2>/dev/null || true")
|
||||
appendLine("ip6tables -X ${AppConfig.ROOT_V6_FWD_CHAIN} 2>/dev/null || true")
|
||||
appendLine("ip6tables -t mangle -D PREROUTING -j ${AppConfig.ROOT_V6_PRE_CHAIN} 2>/dev/null || true")
|
||||
appendLine("ip6tables -t mangle -F ${AppConfig.ROOT_V6_PRE_CHAIN} 2>/dev/null || true")
|
||||
appendLine("ip6tables -t mangle -X ${AppConfig.ROOT_V6_PRE_CHAIN} 2>/dev/null || true")
|
||||
for (pref in listOf(5000, 5010, 5020, 5025, 5026, 5027, 5030, 5040, 5050, 6000)) {
|
||||
appendLine("ip rule del pref $pref 2>/dev/null || true")
|
||||
}
|
||||
// tun device down + helper process
|
||||
appendLine("ip link set dev $TUN down 2>/dev/null || true")
|
||||
appendLine("[ -f '$pidFile' ] && kill \$(cat '$pidFile') 2>/dev/null || true")
|
||||
appendLine("rm -f '$pidFile'")
|
||||
// stop the OOM re-pin loop and restore the core process's LMK priority
|
||||
appendLine("[ -f '$oomGuardPid' ] && kill \$(cat '$oomGuardPid') 2>/dev/null || true")
|
||||
appendLine("rm -f '$oomGuardPid'")
|
||||
appendLine("echo 0 > /proc/$corePid/oom_score_adj 2>/dev/null || true")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.v2ray.ang.root
|
||||
|
||||
import android.content.Context
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Minimal root command runner backed by the `su` binary.
|
||||
*
|
||||
* Scripts are written to the app's private root runtime dir and executed with
|
||||
* `su -c sh <file>` so shell quoting stays simple. stderr is merged into stdout to
|
||||
* avoid pipe-buffer deadlocks.
|
||||
*/
|
||||
object RootShell {
|
||||
|
||||
data class Result(val code: Int, val output: String) {
|
||||
val success: Boolean get() = code == 0
|
||||
}
|
||||
|
||||
/** Write [script] to `<filesDir>/root/<name>` and run it as root. */
|
||||
fun runScript(context: Context, name: String, script: String): Result {
|
||||
val dir = File(context.filesDir, AppConfig.ROOT_RUNTIME_DIR).apply { mkdirs() }
|
||||
val file = File(dir, name).apply {
|
||||
writeText(script)
|
||||
setExecutable(true, false)
|
||||
}
|
||||
return exec("sh ${file.absolutePath}")
|
||||
}
|
||||
|
||||
fun exec(command: String, timeoutSeconds: Long = 30): Result {
|
||||
return try {
|
||||
val process = ProcessBuilder("su", "-c", command)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
val output = process.inputStream.bufferedReader().use { it.readText() }
|
||||
val finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
|
||||
if (!finished) {
|
||||
process.destroy()
|
||||
LogUtil.e(AppConfig.TAG, "RootShell: timed out: $command")
|
||||
return Result(-1, output)
|
||||
}
|
||||
val result = Result(process.exitValue(), output)
|
||||
if (!result.success) {
|
||||
LogUtil.w(AppConfig.TAG, "RootShell: '$command' exited ${result.code}: ${output.trim()}")
|
||||
}
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "RootShell: failed to run '$command'", e)
|
||||
Result(-1, e.message ?: e.javaClass.simpleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.contracts.ServiceControl
|
||||
import com.v2ray.ang.core.CoreServiceManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.root.RootProxyManager
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
/**
|
||||
* Foreground service for the root (system-wide) run modes. Unlike [CoreVpnService] it
|
||||
* does not use Android VpnService — traffic is routed by iptables instead
|
||||
* (see [RootProxyManager]).
|
||||
*
|
||||
* The in-process core is started first (so its listener is up and the foreground
|
||||
* notification is posted promptly), then the root routing rules are installed off the
|
||||
* main thread. On teardown the rules are removed before the core stops.
|
||||
*/
|
||||
class CoreRootService : Service(), ServiceControl {
|
||||
|
||||
private var setupJob: Job? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
LogUtil.i(AppConfig.TAG, "StartCore-Root: Service created")
|
||||
CoreServiceManager.serviceControl = SoftReference(this)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
LogUtil.i(AppConfig.TAG, "StartCore-Root: command received")
|
||||
|
||||
// Start the in-process core first (this also posts the foreground notification),
|
||||
// then install the root routing off the main thread.
|
||||
if (!CoreServiceManager.startCoreLoop(null)) {
|
||||
LogUtil.e(AppConfig.TAG, "StartCore-Root: core failed to start")
|
||||
stopService()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
setupJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
if (!RootProxyManager.start(this@CoreRootService)) {
|
||||
LogUtil.e(AppConfig.TAG, "StartCore-Root: failed to start root mode, stopping")
|
||||
stopService()
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// Wait for any in-flight async setup to finish before tearing down. The rules are
|
||||
// installed off the main thread and can take seconds (the setup script waits for the
|
||||
// tun to appear); if a stop arrives during that window, teardown would run first and
|
||||
// the setup would then re-install the rules + tun pointing at a now-dead core,
|
||||
// blackholing all traffic until the next start/stop cycle clears it.
|
||||
runBlocking { setupJob?.cancelAndJoin() }
|
||||
// Remove routing rules BEFORE stopping the core so traffic is never redirected
|
||||
// to a dead listener. Synchronous on purpose — leaving rules behind breaks the net.
|
||||
RootProxyManager.stop(this)
|
||||
CoreServiceManager.stopCoreLoop()
|
||||
}
|
||||
|
||||
override fun getService(): Service = this
|
||||
|
||||
override fun startService() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun vpnProtect(socket: Int): Boolean = true
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, SettingsManager.getLocale())
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import com.v2ray.ang.core.CoreServiceManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.NotificationManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.root.RootLanSharing
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
@@ -134,6 +135,9 @@ class CoreVpnService : VpnService(), ServiceControl {
|
||||
stopAllService()
|
||||
return
|
||||
}
|
||||
|
||||
// Start LAN sharing if enabled in settings
|
||||
RootLanSharing.startClientSharing(this)
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
@@ -362,6 +366,8 @@ class CoreVpnService : VpnService(), ServiceControl {
|
||||
tun2SocksService?.stopTun2Socks()
|
||||
tun2SocksService = null
|
||||
|
||||
RootLanSharing.stopClientSharing(this)
|
||||
|
||||
CoreServiceManager.stopCoreLoop()
|
||||
|
||||
if (isForced) {
|
||||
|
||||
@@ -61,6 +61,45 @@ class DialerNativeService : IDialerService {
|
||||
private const val UNARY_BODY_WAIT_TIMEOUT_MS = 15_000L
|
||||
private val TOKEN_REGEX = Regex("""/websocket\?token=([^"'\s]+)""")
|
||||
private val METHODS_WITHOUT_BODY = setOf("GET", "HEAD")
|
||||
private val HEADERS_BLACKLIST = hashSetOf(
|
||||
// AI suggest:
|
||||
"host",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
"content-encoding",
|
||||
"connection",
|
||||
"upgrade",
|
||||
"sec-websocket-key",
|
||||
"sec-websocket-version",
|
||||
"sec-websocket-protocol",
|
||||
"Timing-Allow-Origin",
|
||||
// xray
|
||||
"Set-Cookie",
|
||||
"Cookie",
|
||||
"Origin",
|
||||
"Sec-CH-UA",
|
||||
"Sec-CH-UA-Mobile",
|
||||
"Sec-CH-UA-Platform",
|
||||
"DNT",
|
||||
"User-Agent",
|
||||
"Accept-Language",
|
||||
"Cache-Control",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"Sec-Fetch-Site",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-User",
|
||||
"Sec-Fetch-Dest",
|
||||
"Referer",
|
||||
"Accept",
|
||||
"Priority",
|
||||
"Pragma",
|
||||
"Access-Control-Allow-Origin",
|
||||
"Access-Control-Allow-Credentials",
|
||||
"Access-Control-Allow-Methods",
|
||||
"Access-Control-Allow-Headers",
|
||||
"Access-Control-Expose-Headers",
|
||||
"Access-Control-Max-Age",
|
||||
)
|
||||
}
|
||||
|
||||
@Volatile
|
||||
@@ -630,6 +669,11 @@ class DialerNativeService : IDialerService {
|
||||
requestBuilder.header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
task.extra.referrer?.takeIf { it.isNotBlank() }
|
||||
?.let { requestBuilder.header("Referer", it) }
|
||||
val taskHeaders = task.extra.headers.filterKeys { key ->
|
||||
val lowerKey = key.lowercase()
|
||||
!HEADERS_BLACKLIST.any { blackKey -> blackKey.equals(lowerKey, ignoreCase = true) }
|
||||
}
|
||||
taskHeaders.forEach { (key, value) -> requestBuilder.header(key, value) }
|
||||
|
||||
val method = task.method.uppercase()
|
||||
val methodAllowsBody = method !in METHODS_WITHOUT_BODY
|
||||
@@ -663,7 +707,7 @@ class DialerNativeService : IDialerService {
|
||||
val extra: Extra
|
||||
) {
|
||||
data class Extra(
|
||||
// val headers: Map<String, String> = emptyMap(),
|
||||
val headers: Map<String, String> = emptyMap(),
|
||||
// val cookies: Map<String, String> = emptyMap(),
|
||||
val protocols: List<String> = emptyList(),
|
||||
val referrer: String? = null
|
||||
@@ -679,7 +723,7 @@ class DialerNativeService : IDialerService {
|
||||
|
||||
val streamResponse = root.optBoolean("streamResponse", false)
|
||||
val extraObject = root.optJSONObject("extra")
|
||||
// val headers = extraObject.optStringMap("headers")
|
||||
val headers = extraObject.optStringMap("headers")
|
||||
// val cookies = extraObject.optStringMap("cookies")
|
||||
val referrer = extraObject?.optString("referrer")?.takeIf { it.isNotBlank() }
|
||||
val protocols = extraObject.optProtocols()
|
||||
@@ -689,7 +733,7 @@ class DialerNativeService : IDialerService {
|
||||
url = url,
|
||||
streamResponse = streamResponse,
|
||||
extra = Extra(
|
||||
// headers = headers,
|
||||
headers = headers,
|
||||
// cookies = cookies,
|
||||
protocols = protocols,
|
||||
referrer = referrer
|
||||
@@ -698,16 +742,16 @@ class DialerNativeService : IDialerService {
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
// private fun JSONObject?.optStringMap(name: String): Map<String, String> {
|
||||
// val child = this?.optJSONObject(name) ?: return emptyMap()
|
||||
// val map = LinkedHashMap<String, String>()
|
||||
// val iter = child.keys()
|
||||
// while (iter.hasNext()) {
|
||||
// val key = iter.next()
|
||||
// map[key] = child.optString(key)
|
||||
// }
|
||||
// return map
|
||||
// }
|
||||
private fun JSONObject?.optStringMap(name: String): Map<String, String> {
|
||||
val child = this?.optJSONObject(name) ?: return emptyMap()
|
||||
val map = LinkedHashMap<String, String>()
|
||||
val iter = child.keys()
|
||||
while (iter.hasNext()) {
|
||||
val key = iter.next()
|
||||
map[key] = child.optString(key)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private fun JSONObject?.optProtocols(): List<String> {
|
||||
val raw = this?.opt("protocol") ?: return emptyList()
|
||||
|
||||
@@ -4,7 +4,12 @@ import android.content.Context
|
||||
import com.v2ray.ang.core.CoreConfigManager
|
||||
import com.v2ray.ang.core.CoreNativeManager
|
||||
import com.v2ray.ang.dto.RealPingEvent
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.SpeedtestManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -76,6 +81,21 @@ class RealPingWorkerService(
|
||||
|
||||
private fun startRealPing(guid: String): Long {
|
||||
val retFailure = -1L
|
||||
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure
|
||||
if (!config.configType.isComplexType()
|
||||
&& config.configType != EConfigType.HYSTERIA2
|
||||
&& config.server.isNotNullEmpty()
|
||||
&& config.serverPort?.toIntOrNull() != null
|
||||
) {
|
||||
val url = config.server.orEmpty()
|
||||
val port = config.serverPort.orEmpty().toInt()
|
||||
val tcpTime = SpeedtestManager.socketConnectTime(url, port, 1000)
|
||||
if (tcpTime <= -1L) {
|
||||
return retFailure
|
||||
}
|
||||
}
|
||||
|
||||
val configResult = CoreConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||
if (!configResult.status) {
|
||||
return retFailure
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityBackupBinding
|
||||
import com.v2ray.ang.databinding.DialogWebdavBinding
|
||||
import com.v2ray.ang.dto.WebDavConfig
|
||||
import com.v2ray.ang.dto.entities.WebDavConfig
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
|
||||
@@ -17,8 +17,9 @@ import com.v2ray.ang.R
|
||||
import com.v2ray.ang.contracts.MainAdapterListener
|
||||
import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
@@ -225,6 +226,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
private fun removeServerSub(guid: String, position: Int) {
|
||||
mainViewModel.removeServer(guid)
|
||||
adapter.removeServerSub(guid, position)
|
||||
ownerActivity.refreshGroupTabTitles()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,9 +271,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
}
|
||||
|
||||
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
|
||||
val isCustom = profile.configType == EConfigType.CUSTOM
|
||||
|| profile.configType == EConfigType.POLICYGROUP
|
||||
|| profile.configType == EConfigType.PROXYCHAIN
|
||||
val isCustom = profile.configType.isComplexType()
|
||||
|
||||
val (shareOptions, skip) = if (more) {
|
||||
val options = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
|
||||
|
||||
@@ -56,9 +56,7 @@ abstract class HelperBaseActivity : BaseActivity() {
|
||||
mimeType: String = "*/*",
|
||||
onResult: (Uri?) -> Unit
|
||||
) {
|
||||
checkAndRequestPermission(PermissionType.READ_STORAGE) {
|
||||
fileChooser.launch(mimeType, onResult)
|
||||
}
|
||||
fileChooser.launch(mimeType, onResult)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
@@ -18,6 +21,10 @@ import com.v2ray.ang.viewmodel.LogcatViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||
@@ -45,6 +52,55 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun shareLogcat() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val logText = viewModel.getAll().joinToString("\n")
|
||||
|
||||
val result = try {
|
||||
val shareDir = File(cacheDir, "shared_logs").apply {
|
||||
mkdirs()
|
||||
}
|
||||
|
||||
shareDir.listFiles()?.forEach { it.delete() }
|
||||
|
||||
val timestamp = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US).format(Date())
|
||||
val logFile = File(shareDir, "v2rayNG_logcat_$timestamp.txt")
|
||||
logFile.writeText(logText, Charsets.UTF_8)
|
||||
|
||||
val uri = FileProvider.getUriForFile(
|
||||
this@LogcatActivity,
|
||||
"${packageName}.cache",
|
||||
logFile
|
||||
)
|
||||
|
||||
uri to logFile.name
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
toast(e.localizedMessage ?: e.toString())
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_STREAM, result.first)
|
||||
putExtra(Intent.EXTRA_SUBJECT, result.second)
|
||||
putExtra(Intent.EXTRA_TITLE, result.second)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
clipData = ClipData.newUri(contentResolver, result.second, result.first)
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
getString(R.string.logcat_share)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_logcat, menu)
|
||||
|
||||
@@ -78,6 +134,11 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share_all -> {
|
||||
shareLogcat()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.clear_all -> {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
viewModel.clearLogcat()
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
@@ -76,23 +77,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
binding.viewPager.isUserInputEnabled = true
|
||||
|
||||
// setup navigation drawer
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
||||
)
|
||||
binding.drawerLayout.addDrawerListener(toggle)
|
||||
toggle.syncState()
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
})
|
||||
setupNavigationDrawer()
|
||||
|
||||
binding.fab.setOnClickListener { handleFabAction() }
|
||||
binding.layoutTest.setOnClickListener { handleLayoutTestClick() }
|
||||
@@ -106,6 +91,31 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNavigationDrawer() {
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this,
|
||||
binding.drawerLayout,
|
||||
binding.toolbar,
|
||||
R.string.navigation_drawer_open,
|
||||
R.string.navigation_drawer_close
|
||||
)
|
||||
binding.drawerLayout.addDrawerListener(toggle)
|
||||
toggle.syncState()
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupViewModel() {
|
||||
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
|
||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
||||
@@ -131,6 +141,26 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
binding.viewPager.setCurrentItem(targetIndex, false)
|
||||
|
||||
binding.tabGroup.isVisible = groups.size > 1
|
||||
refreshGroupTabTitles(true)
|
||||
}
|
||||
|
||||
fun refreshGroupTabTitles(refreshAll: Boolean = false) {
|
||||
val groupsToRefresh = if (refreshAll || mainViewModel.subscriptionId.isEmpty()) {
|
||||
groupPagerAdapter.groups
|
||||
} else {
|
||||
groupPagerAdapter.groups.filter { it.id == mainViewModel.subscriptionId }
|
||||
}
|
||||
|
||||
groupsToRefresh.forEach { group ->
|
||||
if (group.id.isEmpty()) {
|
||||
return@forEach
|
||||
}
|
||||
val tabIndex = groupPagerAdapter.groups.indexOfFirst { it.id == group.id }
|
||||
if (tabIndex >= 0) {
|
||||
val count = MmkvManager.decodeServerList(group.id).size
|
||||
binding.tabGroup.getTabAt(tabIndex)?.text = "${group.remarks} ($count)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFabAction() {
|
||||
@@ -164,6 +194,11 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
toast(R.string.title_file_chooser)
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN && MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING)) {
|
||||
checkAndRequestPermission(PermissionType.ACCESS_LOCAL_NETWORK) {}
|
||||
}
|
||||
|
||||
CoreServiceManager.startVService(this)
|
||||
}
|
||||
|
||||
@@ -304,12 +339,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
true
|
||||
}
|
||||
|
||||
R.id.ping_all -> {
|
||||
toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
|
||||
mainViewModel.testAllTcping()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.real_ping_all -> {
|
||||
toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
|
||||
mainViewModel.testAllRealPing()
|
||||
@@ -416,6 +445,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
count > 0 -> {
|
||||
toast(getString(R.string.title_import_config_count, count))
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
}
|
||||
|
||||
countSub > 0 -> setupGroupTab()
|
||||
@@ -471,6 +501,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
if (result.configCount > 0) {
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
}
|
||||
hideLoading()
|
||||
}
|
||||
@@ -500,6 +531,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
val ret = mainViewModel.removeAllServer()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
toast(getString(R.string.title_del_config_count, ret))
|
||||
hideLoading()
|
||||
}
|
||||
@@ -519,6 +551,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
val ret = mainViewModel.removeDuplicateServer()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
toast(getString(R.string.title_del_duplicate_config_count, ret))
|
||||
hideLoading()
|
||||
}
|
||||
@@ -538,6 +571,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
val ret = mainViewModel.removeInvalidServer()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
toast(getString(R.string.title_del_config_count, ret))
|
||||
hideLoading()
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import com.v2ray.ang.R
|
||||
import com.v2ray.ang.contracts.MainAdapterListener
|
||||
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
||||
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.ServersCache
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ServersCache
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
@@ -58,7 +59,7 @@ class MainRecyclerAdapter(
|
||||
//Name address
|
||||
holder.itemMainBinding.tvName.text = profile.remarks
|
||||
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
||||
holder.itemMainBinding.tvType.text = profile.configType.name
|
||||
holder.itemMainBinding.tvType.text = getProtocolDescription(profile)
|
||||
|
||||
//TestResult
|
||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||
@@ -140,6 +141,35 @@ class MainRecyclerAdapter(
|
||||
return subRemarks?.toString() ?: ""
|
||||
}
|
||||
|
||||
private fun getProtocolDescription(profile: ProfileItem): String {
|
||||
if (profile.configType.isComplexType()) {
|
||||
return profile.configType.name
|
||||
}
|
||||
|
||||
val parts = mutableListOf<String>()
|
||||
parts.add(profile.configType.name)
|
||||
|
||||
// Transport: hide tcp or blank
|
||||
profile.network?.let { net ->
|
||||
if (net.isNotBlank() && !net.equals("tcp", ignoreCase = true)) {
|
||||
parts.add(net)
|
||||
}
|
||||
}
|
||||
|
||||
// Security: hide blank or tls
|
||||
profile.security?.let { sec ->
|
||||
if (sec.isNotBlank()) {
|
||||
if (profile.insecure == true && sec.equals("tls", ignoreCase = true)) {
|
||||
parts.add("$sec insecure") // TODO
|
||||
} else {
|
||||
parts.add(sec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.joinToString(" / ")
|
||||
}
|
||||
|
||||
fun removeServerSub(guid: String, position: Int) {
|
||||
val idx = data.indexOfFirst { it.guid == guid }
|
||||
if (idx >= 0) {
|
||||
@@ -203,4 +233,4 @@ class MainRecyclerAdapter(
|
||||
|
||||
override fun onItemDismiss(position: Int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityBypassListBinding
|
||||
import com.v2ray.ang.dto.AppInfo
|
||||
import com.v2ray.ang.dto.UrlContentRequest
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.extension.v2RayApplication
|
||||
@@ -189,12 +190,25 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
|
||||
val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
var content = HttpUtil.getUrlContent(url, 5000)
|
||||
var content = HttpUtil.getUrlContent(
|
||||
UrlContentRequest(
|
||||
url = url,
|
||||
timeout = 5000
|
||||
)
|
||||
)
|
||||
if (content.isNullOrEmpty()) {
|
||||
val proxyUsername = SettingsManager.getSocksUsername()
|
||||
val proxyPassword = SettingsManager.getSocksPassword()
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: ""
|
||||
content = HttpUtil.getUrlContent(
|
||||
UrlContentRequest(
|
||||
url = url,
|
||||
timeout = 5000,
|
||||
httpPort = httpPort,
|
||||
proxyUsername = proxyUsername,
|
||||
proxyPassword = proxyPassword
|
||||
)
|
||||
) ?: ""
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
//LogUtil.i(AppConfig.TAG, content)
|
||||
|
||||
@@ -11,11 +11,10 @@ import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
||||
import com.v2ray.ang.dto.RulesetItem
|
||||
import com.v2ray.ang.dto.entities.RulesetItem
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -78,9 +77,7 @@ class RoutingEditActivity : BaseActivity() {
|
||||
* The dropdown button triggers showing the full list without typing.
|
||||
*/
|
||||
private fun setupOutboundTagInput() {
|
||||
val profileRemarks = MmkvManager.decodeAllServerList()
|
||||
.mapNotNull { id -> MmkvManager.decodeServerConfig(id)?.remarks }
|
||||
.filter { it.isNotBlank() }
|
||||
val profileRemarks = SettingsManager.getProfileRemarks()
|
||||
|
||||
val suggestions = (BUILTIN_OUTBOUND_TAGS.toList() + profileRemarks).distinct()
|
||||
val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions)
|
||||
|
||||
@@ -7,20 +7,21 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.v2ray.ang.AppConfig
|
||||
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_MTU
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
@@ -28,10 +29,14 @@ import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.CertificateFingerprintManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ServerActivity : BaseActivity() {
|
||||
|
||||
@@ -141,7 +146,10 @@ class ServerActivity : BaseActivity() {
|
||||
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
||||
private val et_ech_config_list: EditText? by lazy { findViewById(R.id.et_ech_config_list) }
|
||||
private val container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) }
|
||||
private val et_verify_peer_cert_by_name: EditText? by lazy { findViewById(R.id.et_verify_peer_cert_by_name) }
|
||||
private val container_verify_peer_cert_by_name: LinearLayout? by lazy { findViewById(R.id.lay_verify_peer_cert_by_name) }
|
||||
private val et_pinned_ca256: EditText? by lazy { findViewById(R.id.et_pinned_ca256) }
|
||||
private val btn_pinned_ca256_action: Button? by lazy { findViewById(R.id.btn_pinned_ca256_action) }
|
||||
private val container_pinned_ca256: LinearLayout? by lazy { findViewById(R.id.lay_pinned_ca256) }
|
||||
private val layout_browser_dialer: LinearLayout? by lazy { findViewById(R.id.layout_browser_dialer) }
|
||||
private val sp_browser_dialer_mode: Spinner? by lazy { findViewById(R.id.sp_browser_dialer_mode) }
|
||||
@@ -296,6 +304,7 @@ class ServerActivity : BaseActivity() {
|
||||
container_spider_x,
|
||||
container_mldsa65_verify,
|
||||
container_ech_config_list,
|
||||
container_verify_peer_cert_by_name,
|
||||
container_pinned_ca256
|
||||
).forEach { it?.visibility = View.GONE }
|
||||
}
|
||||
@@ -308,6 +317,7 @@ class ServerActivity : BaseActivity() {
|
||||
container_alpn,
|
||||
container_allow_insecure,
|
||||
container_ech_config_list,
|
||||
container_verify_peer_cert_by_name,
|
||||
container_pinned_ca256
|
||||
).forEach { it?.visibility = View.VISIBLE }
|
||||
listOf(
|
||||
@@ -328,6 +338,7 @@ class ServerActivity : BaseActivity() {
|
||||
container_alpn,
|
||||
container_allow_insecure,
|
||||
container_ech_config_list,
|
||||
container_verify_peer_cert_by_name,
|
||||
container_pinned_ca256
|
||||
).forEach { it?.visibility = View.GONE }
|
||||
listOf(
|
||||
@@ -344,6 +355,9 @@ class ServerActivity : BaseActivity() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
btn_pinned_ca256_action?.setOnClickListener {
|
||||
fetchPinnedCA256ForCurrentConfig()
|
||||
}
|
||||
if (config != null) {
|
||||
bindingServer(config)
|
||||
} else {
|
||||
@@ -411,6 +425,7 @@ class ServerActivity : BaseActivity() {
|
||||
sp_allow_insecure?.setSelection(allowinsecure)
|
||||
}
|
||||
et_ech_config_list?.text = Utils.getEditable(config.echConfigList)
|
||||
et_verify_peer_cert_by_name?.text = Utils.getEditable(config.verifyPeerCertByName)
|
||||
et_pinned_ca256?.text = Utils.getEditable(config.pinnedCA256)
|
||||
} else if (config.security == REALITY) {
|
||||
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
||||
@@ -614,11 +629,12 @@ class ServerActivity : BaseActivity() {
|
||||
val spiderX = et_spider_x?.text?.toString()
|
||||
val mldsa65Verify = et_mldsa65_verify?.text?.toString()
|
||||
val echConfigList = et_ech_config_list?.text?.toString()
|
||||
val verifyPeerCertByName = et_verify_peer_cert_by_name?.text?.toString()
|
||||
val pinnedCA256 = et_pinned_ca256?.text?.toString()
|
||||
|
||||
val allowInsecure =
|
||||
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
|
||||
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE)
|
||||
false
|
||||
} else {
|
||||
allowinsecures[allowInsecureField].toBoolean()
|
||||
}
|
||||
@@ -633,9 +649,51 @@ class ServerActivity : BaseActivity() {
|
||||
config.spiderX = spiderX
|
||||
config.mldsa65Verify = mldsa65Verify
|
||||
config.echConfigList = echConfigList
|
||||
config.verifyPeerCertByName = verifyPeerCertByName
|
||||
config.pinnedCA256 = pinnedCA256
|
||||
}
|
||||
|
||||
private fun fetchPinnedCA256ForCurrentConfig() {
|
||||
val config = buildCurrentProfileForCertificateFetch() ?: return
|
||||
|
||||
lifecycleScope.launch {
|
||||
btn_pinned_ca256_action?.isEnabled = false
|
||||
try {
|
||||
val sha256 = withContext(Dispatchers.IO) {
|
||||
CertificateFingerprintManager.fetchForManualFill(config)
|
||||
}
|
||||
if (sha256.isNullOrBlank()) {
|
||||
toast(R.string.toast_fetch_cert_sha256_failed)
|
||||
} else {
|
||||
et_pinned_ca256?.text = Utils.getEditable(sha256)
|
||||
toastSuccess(R.string.toast_fetch_cert_sha256_success)
|
||||
}
|
||||
} finally {
|
||||
btn_pinned_ca256_action?.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCurrentProfileForCertificateFetch(): ProfileItem? {
|
||||
if (TextUtils.isEmpty(et_address.text.toString())) {
|
||||
toast(R.string.server_lab_address)
|
||||
return null
|
||||
}
|
||||
|
||||
val configType = MmkvManager.decodeServerConfig(editGuid)?.configType ?: createConfigType
|
||||
if (configType != EConfigType.HYSTERIA2 && Utils.parseInt(et_port.text.toString()) <= 0) {
|
||||
toast(R.string.server_lab_port)
|
||||
return null
|
||||
}
|
||||
|
||||
val config = ProfileItem.create(configType)
|
||||
saveCommon(config)
|
||||
saveStreamSettings(config)
|
||||
saveTls(config)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
private fun transportTypes(network: String?): Array<out String> {
|
||||
return when (network) {
|
||||
NetworkType.TCP.type -> {
|
||||
|
||||
@@ -10,7 +10,7 @@ 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.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityServerGroupBinding
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.extension.toast
|
||||
|
||||
@@ -11,8 +11,9 @@ import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.contracts.BaseAdapterListener
|
||||
import com.v2ray.ang.databinding.ActivityServerProxyChainBinding
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
@@ -55,18 +56,13 @@ class ServerProxyChainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun loadAvailableRemarks() {
|
||||
allRemarks = MmkvManager.decodeAllServerList()
|
||||
.asSequence()
|
||||
.mapNotNull { guid -> MmkvManager.decodeServerConfig(guid) }
|
||||
.filter { profile ->
|
||||
profile.configType != EConfigType.CUSTOM
|
||||
&& profile.configType != EConfigType.POLICYGROUP
|
||||
&& profile.configType != EConfigType.PROXYCHAIN
|
||||
}
|
||||
.map { it.remarks.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.distinct()
|
||||
.toList()
|
||||
allRemarks = SettingsManager.getProfileRemarks(
|
||||
excludeConfigTypes = setOf(
|
||||
EConfigType.CUSTOM,
|
||||
EConfigType.POLICYGROUP,
|
||||
EConfigType.PROXYCHAIN,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupRecycler() {
|
||||
@@ -114,10 +110,7 @@ class ServerProxyChainActivity : BaseActivity() {
|
||||
|
||||
val invalidMembers = chainMembers.filter { member ->
|
||||
val profile = SettingsManager.getServerViaRemarks(member)
|
||||
profile == null
|
||||
|| profile.configType == EConfigType.CUSTOM
|
||||
|| profile.configType == EConfigType.POLICYGROUP
|
||||
|| profile.configType == EConfigType.PROXYCHAIN
|
||||
profile == null || profile.configType.isComplexType()
|
||||
}
|
||||
if (invalidMembers.isNotEmpty()) {
|
||||
toast(getString(R.string.server_proxy_chain_members_invalid, invalidMembers.joinToString(", ")))
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
@@ -9,9 +10,12 @@ import androidx.preference.PreferenceFragmentCompat
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.VPN
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.helper.MmkvPreferenceDataStore
|
||||
import com.v2ray.ang.root.RootManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -40,8 +44,11 @@ class SettingsActivity : BaseActivity() {
|
||||
private val fragmentPackets by lazy { findPreference<ListPreference>(AppConfig.PREF_FRAGMENT_PACKETS) }
|
||||
private val fragmentLength by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_LENGTH) }
|
||||
private val fragmentInterval by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_INTERVAL) }
|
||||
private val fragmentMaxSplit by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_MAXSPLIT) }
|
||||
|
||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||
private val enableRootMode by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_ROOT_MODE_ENABLE) }
|
||||
private val lanSharing by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_ROOT_LAN_SHARING) }
|
||||
|
||||
private val hevTunLogLevel by lazy { findPreference<ListPreference>(AppConfig.PREF_HEV_TUNNEL_LOGLEVEL) }
|
||||
private val hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) }
|
||||
@@ -96,6 +103,7 @@ class SettingsActivity : BaseActivity() {
|
||||
updateMode(valueStr)
|
||||
true
|
||||
}
|
||||
|
||||
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
||||
|
||||
useHevTun?.setOnPreferenceChangeListener { _, newValue ->
|
||||
@@ -112,6 +120,33 @@ class SettingsActivity : BaseActivity() {
|
||||
updateDynamicSocksPort(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
|
||||
enableRootMode?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue == true && !RootManager.cachedRoot()) {
|
||||
lifecycleScope.launch {
|
||||
if (checkAndRequestRoot()) {
|
||||
enableRootMode?.isChecked = true
|
||||
}
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
lanSharing?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue == true && !RootManager.cachedRoot()) {
|
||||
lifecycleScope.launch {
|
||||
if (checkAndRequestRoot()) {
|
||||
lanSharing?.isChecked = true
|
||||
}
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initPreferenceSummaries() {
|
||||
@@ -160,6 +195,15 @@ class SettingsActivity : BaseActivity() {
|
||||
preferenceScreen?.let { traverse(it) }
|
||||
}
|
||||
|
||||
private suspend fun checkAndRequestRoot(): Boolean {
|
||||
val hasRoot = RootManager.refresh()
|
||||
if (!isAdded) return false
|
||||
if (!hasRoot) {
|
||||
context?.toastError(R.string.toast_root_required)
|
||||
}
|
||||
return hasRoot
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
updateHevTunSettings(MmkvManager.decodeSettingsBool(AppConfig.PREF_USE_HEV_TUNNEL, true))
|
||||
@@ -243,6 +287,7 @@ class SettingsActivity : BaseActivity() {
|
||||
fragmentPackets?.isEnabled = enabled
|
||||
fragmentLength?.isEnabled = enabled
|
||||
fragmentInterval?.isEnabled = enabled
|
||||
fragmentMaxSplit?.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun updateDynamicSocksPort(enabled: Boolean) {
|
||||
|
||||
@@ -4,12 +4,16 @@ import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.ImageButton
|
||||
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
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
@@ -33,6 +37,7 @@ class SubEditActivity : BaseActivity() {
|
||||
//setContentView(binding.root)
|
||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_sub_setting))
|
||||
|
||||
setupProfileRemarkInputs()
|
||||
SettingsChangeManager.makeSetupGroupTab()
|
||||
val subItem = MmkvManager.decodeSubscription(editSubId)
|
||||
if (subItem != null) {
|
||||
@@ -73,6 +78,37 @@ class SubEditActivity : BaseActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setupProfileRemarkInputs() {
|
||||
val suggestions = SettingsManager.getProfileRemarks(
|
||||
excludeConfigTypes = setOf(
|
||||
EConfigType.CUSTOM,
|
||||
EConfigType.POLICYGROUP,
|
||||
EConfigType.PROXYCHAIN,
|
||||
)
|
||||
)
|
||||
|
||||
setupProfileRemarkInput(binding.etPreProfile, binding.btnPreProfileDropdown, suggestions)
|
||||
setupProfileRemarkInput(binding.etNextProfile, binding.btnNextProfileDropdown, suggestions)
|
||||
}
|
||||
|
||||
private fun setupProfileRemarkInput(
|
||||
input: AutoCompleteTextView,
|
||||
dropdownButton: ImageButton,
|
||||
suggestions: List<String>
|
||||
) {
|
||||
val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions)
|
||||
input.setAdapter(adapter)
|
||||
input.threshold = 0
|
||||
|
||||
dropdownButton.setOnClickListener {
|
||||
input.requestFocus()
|
||||
input.showDropDown()
|
||||
}
|
||||
input.setOnClickListener {
|
||||
input.showDropDown()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* save server config
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.contracts.BaseAdapterListener
|
||||
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
||||
import com.v2ray.ang.dto.AssetUrlItem
|
||||
import com.v2ray.ang.dto.entities.AssetUrlItem
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
|
||||
@@ -8,7 +8,7 @@ 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
|
||||
import com.v2ray.ang.dto.entities.AssetUrlItem
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.v2ray.ang.util
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.dto.UrlContentRequest
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -108,20 +109,15 @@ object HttpUtil {
|
||||
* @param httpPort The HTTP port to use.
|
||||
* @return The content of the URL as a string.
|
||||
*/
|
||||
fun getUrlContent(
|
||||
url: String,
|
||||
timeout: Int,
|
||||
httpPort: Int = 0,
|
||||
proxyUsername: String? = null,
|
||||
proxyPassword: String? = null
|
||||
): String? {
|
||||
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
|
||||
fun getUrlContent(request: UrlContentRequest): String? {
|
||||
val url = request.url ?: return null
|
||||
val client = buildOkHttpClient(request.timeout, request.httpPort, request.proxyUsername, request.proxyPassword, followRedirects = true)
|
||||
val requestBuilder = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.header("Connection", "close")
|
||||
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
|
||||
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
|
||||
if (request.httpPort != 0 && !request.proxyUsername.isNullOrBlank() && !request.proxyPassword.isNullOrBlank()) {
|
||||
requestBuilder.header("Proxy-Authorization", Credentials.basic(request.proxyUsername, request.proxyPassword))
|
||||
}
|
||||
try {
|
||||
client.newCall(requestBuilder.build()).execute().use { response ->
|
||||
@@ -147,25 +143,18 @@ object HttpUtil {
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun getUrlContentWithUserAgent(
|
||||
url: String?,
|
||||
userAgent: String?,
|
||||
timeout: Int = 15000,
|
||||
httpPort: Int = 0,
|
||||
proxyUsername: String? = null,
|
||||
proxyPassword: String? = null
|
||||
): String {
|
||||
var currentUrl = url
|
||||
fun getUrlContentWithUserAgent(request: UrlContentRequest): String {
|
||||
var currentUrl = request.url
|
||||
var redirects = 0
|
||||
val maxRedirects = 3
|
||||
|
||||
while (redirects++ < maxRedirects) {
|
||||
if (currentUrl == null) continue
|
||||
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = false)
|
||||
val finalUserAgent = if (userAgent.isNullOrBlank()) {
|
||||
val client = buildOkHttpClient(request.timeout, request.httpPort, request.proxyUsername, request.proxyPassword, followRedirects = false)
|
||||
val finalUserAgent = if (request.userAgent.isNullOrBlank()) {
|
||||
"v2rayNG/${BuildConfig.VERSION_NAME}"
|
||||
} else {
|
||||
userAgent
|
||||
request.userAgent
|
||||
}
|
||||
val requestBuilder = Request.Builder()
|
||||
.url(currentUrl)
|
||||
@@ -175,8 +164,8 @@ object HttpUtil {
|
||||
|
||||
applyEmbeddedBasicAuthHeader(currentUrl, requestBuilder)
|
||||
|
||||
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
|
||||
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
|
||||
if (request.httpPort != 0 && !request.proxyUsername.isNullOrBlank() && !request.proxyPassword.isNullOrBlank()) {
|
||||
requestBuilder.header("Proxy-Authorization", Credentials.basic(request.proxyUsername, request.proxyPassword))
|
||||
}
|
||||
|
||||
client.newCall(requestBuilder.build()).execute().use { response ->
|
||||
@@ -271,20 +260,17 @@ object HttpUtil {
|
||||
}
|
||||
|
||||
fun downloadToFile(
|
||||
url: String,
|
||||
targetFile: File,
|
||||
timeout: Int = 15000,
|
||||
httpPort: Int = 0,
|
||||
proxyUsername: String? = null,
|
||||
proxyPassword: String? = null
|
||||
request: UrlContentRequest,
|
||||
targetFile: File
|
||||
): Boolean {
|
||||
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
|
||||
val url = request.url ?: return false
|
||||
val client = buildOkHttpClient(request.timeout, request.httpPort, request.proxyUsername, request.proxyPassword, followRedirects = true)
|
||||
val requestBuilder = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.header("Connection", "close")
|
||||
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
|
||||
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
|
||||
if (request.httpPort != 0 && !request.proxyUsername.isNullOrBlank() && !request.proxyPassword.isNullOrBlank()) {
|
||||
requestBuilder.header("Proxy-Authorization", Credentials.basic(request.proxyUsername, request.proxyPassword))
|
||||
}
|
||||
|
||||
return try {
|
||||
|
||||
@@ -35,6 +35,23 @@ object JsonUtil {
|
||||
return gson.fromJson(src, cls)
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parses a JSON string into an object of the specified class.
|
||||
* Returns null if parsing fails instead of throwing an exception.
|
||||
*
|
||||
* @param src The JSON string to parse.
|
||||
* @param cls The class of the object to parse into.
|
||||
* @return The parsed object, or null if parsing fails.
|
||||
*/
|
||||
fun <T> fromJsonSafe(src: String, cls: Class<T>): T? {
|
||||
return try {
|
||||
gson.fromJson(src, cls)
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to parse JSON", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object to its pretty-printed JSON representation.
|
||||
*
|
||||
|
||||
@@ -14,24 +14,21 @@ import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.GroupMapItem
|
||||
import com.v2ray.ang.dto.ServersCache
|
||||
import com.v2ray.ang.dto.SubscriptionCache
|
||||
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
||||
import com.v2ray.ang.dto.TestServiceMessage
|
||||
import com.v2ray.ang.dto.entities.ServersCache
|
||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
import com.v2ray.ang.extension.matchesPattern
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.SpeedtestManager
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Collections
|
||||
@@ -45,7 +42,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||
val updateListAction by lazy { MutableLiveData<Int>() }
|
||||
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
||||
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
||||
|
||||
/**
|
||||
* 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):
|
||||
@@ -63,8 +59,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
*/
|
||||
override fun onCleared() {
|
||||
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
SpeedtestManager.closeAllTcpSockets()
|
||||
LogUtil.i(AppConfig.TAG, "Main ViewModel is cleared")
|
||||
super.onCleared()
|
||||
}
|
||||
@@ -177,32 +171,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the TCP ping for all servers.
|
||||
*/
|
||||
fun testAllTcping() {
|
||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
SpeedtestManager.closeAllTcpSockets()
|
||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||
|
||||
val serversCopy = serversCache.toList()
|
||||
for (item in serversCopy) {
|
||||
item.profile.let { outbound ->
|
||||
val serverAddress = outbound.server
|
||||
val serverPort = outbound.serverPort
|
||||
if (serverAddress != null && serverPort != null) {
|
||||
tcpingTestScope.launch {
|
||||
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
|
||||
launch(Dispatchers.Main) {
|
||||
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
||||
updateListAction.value = getPosition(item.guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the real ping for all servers.
|
||||
*/
|
||||
@@ -296,16 +264,28 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
/**
|
||||
* Removes duplicate servers.
|
||||
* Excludes servers with complex types (Custom, PolicyGroup, or ProxyChain) from duplicate comparison.
|
||||
* @return The number of removed servers.
|
||||
*/
|
||||
fun removeDuplicateServer(): Int {
|
||||
val serversCacheCopy = serversCache.toList().toMutableList()
|
||||
val deleteServer = mutableListOf<String>()
|
||||
|
||||
serversCacheCopy.forEachIndexed { index, sc ->
|
||||
val profile = sc.profile
|
||||
// Skip if this profile has a complex config type
|
||||
if (profile.configType.isComplexType()) {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
serversCacheCopy.forEachIndexed { index2, sc2 ->
|
||||
if (index2 > index) {
|
||||
val profile2 = sc2.profile
|
||||
// Skip if the second profile has a complex config type
|
||||
if (profile2.configType.isComplexType()) {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
if (profile == profile2 && !deleteServer.contains(sc2.guid)) {
|
||||
deleteServer.add(sc2.guid)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.v2ray.ang.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.v2ray.ang.dto.RulesetItem
|
||||
import com.v2ray.ang.dto.entities.RulesetItem
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.v2ray.ang.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.v2ray.ang.dto.SubscriptionCache
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.v2ray.ang.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.AssetUrlCache
|
||||
import com.v2ray.ang.dto.AssetUrlItem
|
||||
import com.v2ray.ang.dto.UrlContentRequest
|
||||
import com.v2ray.ang.dto.entities.AssetUrlCache
|
||||
import com.v2ray.ang.dto.entities.AssetUrlItem
|
||||
import com.v2ray.ang.extension.concatUrl
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
@@ -92,7 +93,18 @@ class UserAssetViewModel : ViewModel() {
|
||||
val targetTemp = File(extDir, item.remarks + "_temp")
|
||||
val target = File(extDir, item.remarks)
|
||||
try {
|
||||
if (HttpUtil.downloadToFile(item.url, targetTemp, 15000, httpPort, proxyUsername, proxyPassword)) {
|
||||
if (
|
||||
HttpUtil.downloadToFile(
|
||||
UrlContentRequest(
|
||||
url = item.url,
|
||||
timeout = 15000,
|
||||
httpPort = httpPort,
|
||||
proxyUsername = proxyUsername,
|
||||
proxyPassword = proxyPassword
|
||||
),
|
||||
targetTemp
|
||||
)
|
||||
) {
|
||||
targetTemp.renameTo(target)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -209,12 +209,33 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sub_setting_pre_profile" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_pre_profile"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/sub_setting_pre_profile_tip"
|
||||
android:inputType="text" />
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp8">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/et_pre_profile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:completionThreshold="0"
|
||||
android:hint="@string/sub_setting_pre_profile_tip"
|
||||
android:inputType="text"
|
||||
android:imeOptions="actionDone"
|
||||
android:paddingEnd="40dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_pre_profile_dropdown"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/sub_setting_pre_profile"
|
||||
android:src="@drawable/ic_arrow_drop_down" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -229,12 +250,33 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sub_setting_next_profile" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_next_profile"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/sub_setting_pre_profile_tip"
|
||||
android:inputType="text" />
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp8">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/et_next_profile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:completionThreshold="0"
|
||||
android:hint="@string/sub_setting_pre_profile_tip"
|
||||
android:inputType="text"
|
||||
android:imeOptions="actionDone"
|
||||
android:paddingEnd="40dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_next_profile_dropdown"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/sub_setting_next_profile"
|
||||
android:src="@drawable/ic_arrow_drop_down" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp8"
|
||||
android:entries="@array/allowinsecures"
|
||||
android:nextFocusDown="@+id/et_ech_config_list"/>
|
||||
android:nextFocusDown="@+id/et_verify_peer_cert_by_name"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
@@ -138,6 +138,27 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lay_verify_peer_cert_by_name"
|
||||
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_verify_peer_cert_by_name" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_verify_peer_cert_by_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:nextFocusDown="@+id/et_ech_config_list" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lay_pinned_ca256"
|
||||
android:layout_width="match_parent"
|
||||
@@ -157,6 +178,14 @@
|
||||
android:inputType="text"
|
||||
android:nextFocusDown="@+id/et_public_key" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_pinned_ca256_action"
|
||||
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:text="@string/pinned_ca256_action_fetch" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -88,6 +88,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_pinned_ca256_action"
|
||||
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:text="@string/pinned_ca256_action_fetch" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -17,4 +17,9 @@
|
||||
android:icon="@drawable/ic_copy"
|
||||
android:title="@string/logcat_copy"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/share_all"
|
||||
android:icon="@drawable/ic_share_24dp"
|
||||
android:title="@string/logcat_share"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -90,10 +90,6 @@
|
||||
android:icon="@drawable/ic_share_24dp"
|
||||
android:title="@string/title_export_all"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/ping_all"
|
||||
android:title="@string/title_ping_all_server"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/real_ping_all"
|
||||
android:title="@string/title_real_ping_all_server"
|
||||
|
||||
@@ -118,7 +118,11 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">فشل نسخ الملف، يرجى استخدام مدير الملفات</string>
|
||||
@@ -220,9 +224,6 @@
|
||||
<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>
|
||||
<string name="summary_pref_allow_insecure">عند TLS، الافتراضي هو السماح غير الآمن</string>
|
||||
|
||||
<string name="title_pref_socks_port">منفذ بروكسي Local</string>
|
||||
<string name="title_pref_enable_local_proxy">تفعيل الوكيل المحلي</string>
|
||||
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
|
||||
@@ -286,6 +287,7 @@
|
||||
<string name="title_logcat">Logcat</string>
|
||||
<string name="logcat_copy">نسخ</string>
|
||||
<string name="logcat_clear">مسح</string>
|
||||
<string name="logcat_share">مشاركة السجل</string>
|
||||
<string name="title_service_restart">إعادة تشغيل الخدمة</string>
|
||||
<string name="title_del_all_config">حذف جميع الإعدادات (قبل أول خطوة)</string>
|
||||
<string name="title_del_duplicate_config">حذف الإعدادات المكررة (2)</string>
|
||||
@@ -304,7 +306,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||
<string name="toast_invalid_update_interval">فاصل التحديث غير صالح. الحد الأدنى هو 15 دقيقة.</string>
|
||||
<string name="title_sub_update">تحديث الاشتراك (أول خطوة)</string>
|
||||
<string name="title_ping_all_server">Tcping لجميع الإعدادات</string>
|
||||
<string name="title_real_ping_all_server"> اختبر جميع الإعدادات (3)</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">الفرز حسب نتائج الاختبار (5)</string>
|
||||
@@ -367,6 +368,7 @@
|
||||
<string name="title_pref_fragment_length">طول الجزء (الحد الأدنى - الحد الأقصى)</string>
|
||||
<string name="title_pref_fragment_interval">فاصل الجزء (الحد الأدنى - الحد الأقصى)</string>
|
||||
<string name="title_pref_fragment_enabled">تفعيل الجزء</string>
|
||||
<string name="title_pref_fragment_maxsplit">ماكس سبليت الجزء</string>
|
||||
|
||||
<string name="update_check_for_update">Check for update</string>
|
||||
<string name="update_already_latest_version">Already on the latest version</string>
|
||||
|
||||
@@ -117,7 +117,11 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">ফাইল কপি ব্যর্থ, অনুগ্রহ করে ফাইল ম্যানেজার ব্যবহার করুন</string>
|
||||
@@ -220,9 +224,6 @@
|
||||
<string name="summary_pref_proxy_sharing_enabled">অন্যান্য ডিভাইসগুলি আপনার আইপি ঠিকানা ব্যবহার করে প্রক্সিতে সংযুক্ত হতে পারে, শুধুমাত্র বিশ্বস্ত নেটওয়ার্কে সক্রিয় করুন যাতে অনুমোদিত সংযোগ এড়ানো যায়</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">LAN থেকে সংযোগ অনুমোদন করুন, নিশ্চিত করুন যে আপনি একটি বিশ্বস্ত নেটওয়ার্কে আছেন</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">allowInsecure</string>
|
||||
<string name="summary_pref_allow_insecure">যখন TLS, ডিফল্টভাবে allowInsecure</string>
|
||||
|
||||
<string name="title_pref_socks_port">Local প্রক্সি পোর্ট</string>
|
||||
<string name="title_pref_enable_local_proxy">লোকাল প্রক্সি সক্ষম করুন</string>
|
||||
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
|
||||
@@ -286,6 +287,7 @@
|
||||
<string name="title_logcat">লগক্যাট</string>
|
||||
<string name="logcat_copy">কপি করুন</string>
|
||||
<string name="logcat_clear">স্পষ্ট করুন</string>
|
||||
<string name="logcat_share">লগ শেয়ার করুন</string>
|
||||
<string name="title_service_restart">সার্ভিস পুনরায় চালু করুন</string>
|
||||
<string name="title_del_all_config">সব কনফিগারেশন মুছে ফেলুন</string>
|
||||
<string name="title_del_duplicate_config">ডুপ্লিকেট কনফিগারেশন মুছে ফেলুন</string>
|
||||
@@ -304,7 +306,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||
<string name="toast_invalid_update_interval">আপডেটের বিরতি সঠিক নয়। সর্বনিম্ন ১৫ মিনিট।</string>
|
||||
<string name="title_sub_update">সাবস্ক্রিপশন আপডেট</string>
|
||||
<string name="title_ping_all_server">সব কনফিগারেশন TCPing</string>
|
||||
<string name="title_real_ping_all_server">সব কনফিগারেশন প্রকৃত বিলম্ব</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">টেস্ট ফলাফল দ্বারা সাজানো</string>
|
||||
@@ -366,6 +367,7 @@
|
||||
<string name="title_pref_fragment_length">ফ্র্যাগমেন্ট দৈর্ঘ্য (ন্যূনতম-সর্বাধিক)</string>
|
||||
<string name="title_pref_fragment_interval">ফ্র্যাগমেন্ট ইন্টারভ্যাল (ন্যূনতম-সর্বাধিক)</string>
|
||||
<string name="title_pref_fragment_enabled">ফ্র্যাগমেন্ট সক্রিয় করুন</string>
|
||||
<string name="title_pref_fragment_maxsplit">ফ্র্যাগমেন্ট ম্যাক্স স্প্লিট</string>
|
||||
|
||||
<string name="update_check_for_update">Check for update</string>
|
||||
<string name="update_already_latest_version">Already on the latest version</string>
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
<resources>
|
||||
<string name="app_widget_name">آلشت</string>
|
||||
<string name="app_tile_name">آلشت</string>
|
||||
<string name="app_tile_first_use">سی کرت ٱولی و کار بوردن ای ویژیی، ز برنومه سی ازاف کردن سرور استفاڌه کۊنین</string>
|
||||
<string name="app_tile_first_use">سی کرت ٱولی و کار بوردن ای ویژیی، ٱول ز من برنومه ی سرور ازاف کۊنین</string>
|
||||
<string name="navigation_drawer_open">گۊشیڌن نومگه کشاری</string>
|
||||
<string name="navigation_drawer_close">بستن نومگه کشاری</string>
|
||||
<string name="migration_success">مووفقیت من جاگورویی داده</string>
|
||||
<string name="action_stop_service">واڌاشتن خدمات</string>
|
||||
<string name="migration_fail">جاگورویی داده ٱنجوم نگرؽڌ</string>
|
||||
<string name="pull_down_to_refresh">سی وانۊ کردن، بکشینس بلم!</string>
|
||||
<string name="migration_success">جاگورویی داده یل وا مووفقیت ٱنجوم وابی!</string>
|
||||
<string name="action_stop_service">واڌاشتن سرویس</string>
|
||||
<string name="migration_fail">جاگورویی داده یل شکست خرد!</string>
|
||||
<string name="pull_down_to_refresh">سی وانۊ کردن، بلگه ن بلم بکشین!</string>
|
||||
|
||||
<!-- Notifications -->↓
|
||||
<string name="notification_action_stop_v2ray">واڌاشتن</string>
|
||||
<string name="toast_permission_denied">گرؽڌن موجوز مومکن نؽڌ</string>
|
||||
<string name="toast_permission_denied_notification">گرؽڌن موجوز وارسۊوی مومکن نؽڌ</string>
|
||||
<string name="notification_action_more">سی گرؽڌن دۉسمندیا بیشتر کیلیک کوݩ</string>
|
||||
<string name="toast_services_start">ره وستن خدمات</string>
|
||||
<string name="toast_services_stop">واڌاشتن خدمات</string>
|
||||
<string name="toast_services_success">ره وستن خدمات وا مووفقیت ٱنجوم وابی</string>
|
||||
<string name="toast_services_failure">ره وستن خدمات وا مووفقیت ٱنجوم نوابی</string>
|
||||
<string name="notification_action_more">سی دووسمندیا قلوه کیلیک کوݩ</string>
|
||||
<string name="toast_services_start">ره وستن سرویس</string>
|
||||
<string name="toast_services_stop">واڌاشتن سرویس</string>
|
||||
<string name="toast_services_success">ره وستن سرویس وا مووفقیت ٱنجوم وابی</string>
|
||||
<string name="toast_services_failure">ره وستن سرویس وا مووفقیت ٱنجوم نوابی</string>
|
||||
|
||||
<!--ServerActivity-->
|
||||
<string name="title_server">فایل کانفیگ</string>
|
||||
@@ -30,17 +30,17 @@
|
||||
<string name="menu_item_import_config_clipboard">و من ٱووردن کانفیگ ز کلیپ بورد</string>
|
||||
<string name="menu_item_import_config_local">و من ٱووردن کانفیگ ز مهلی</string>
|
||||
<string name="menu_item_import_config_policy_group">ٱووردن [بونکۊ سیاست]</string>
|
||||
<string name="menu_item_import_config_proxy_chain">Add [Proxy chain]</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>
|
||||
<string name="menu_item_import_config_manually_socks">هؽل دستی[SOCKS]</string>
|
||||
<string name="menu_item_import_config_manually_http">هؽل دستی[HTTP]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">هؽل دستی[Trojan]</string>
|
||||
<string name="menu_item_import_config_manually_wireguard">هؽل دستی[Wireguard]</string>
|
||||
<string name="menu_item_import_config_manually_hysteria2">هؽل دستی[Hysteria2]</string>
|
||||
<string name="menu_item_import_config_proxy_chain">ٱووردن [زنجیره پروکسی]</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>
|
||||
<string name="menu_item_import_config_manually_socks">ٱووردن [SOCKS]</string>
|
||||
<string name="menu_item_import_config_manually_http">ٱووردن [HTTP]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">ٱووردن [Trojan]</string>
|
||||
<string name="menu_item_import_config_manually_wireguard">ٱووردن [Wireguard]</string>
|
||||
<string name="menu_item_import_config_manually_hysteria2">ٱووردن [Hysteria2]</string>
|
||||
<string name="del_config_comfirm">پاک بۊ؟</string>
|
||||
<string name="del_invalid_config_comfirm">پؽش ز پاک کردن کانفیگ نا موئتبر واجۊری کوݩ! پاک کردن کانفیگن قوۊل اکۊنی؟</string>
|
||||
<string name="del_invalid_config_comfirm">پؽش ز پاک کردن، پینگ بگرین! الن اخۊین پاک کۊنین؟</string>
|
||||
<string name="server_lab_remarks">نیشتنا</string>
|
||||
<string name="server_lab_address">نشۊوی</string>
|
||||
<string name="server_lab_port">پورت</string>
|
||||
@@ -49,7 +49,7 @@
|
||||
<string name="server_lab_security">ٱمنیت</string>
|
||||
<string name="server_lab_network">شبکه</string>
|
||||
<string name="server_lab_more_function">جاگورو</string>
|
||||
<string name="server_lab_head_type">نوء سر بلگ</string>
|
||||
<string name="server_lab_head_type">نوع هدر</string>
|
||||
<string name="server_lab_mode_type">هالت gRPC</string>
|
||||
<string name="server_lab_request_host">هاست</string>
|
||||
<string name="server_lab_request_host_http">هاست http</string>
|
||||
@@ -66,11 +66,11 @@
|
||||
<string name="server_lab_path_h2">تور H2</string>
|
||||
<string name="server_lab_path_quic">تور QUIC</string>
|
||||
<string name="server_lab_path_kcp">KCP seed</string>
|
||||
<string name="server_lab_path_grpc">نوم خدمات gRPC</string>
|
||||
<string name="server_lab_path_grpc">نوم سرویس gRPC</string>
|
||||
<string name="server_lab_stream_security">TLS</string>
|
||||
<string name="server_lab_stream_fingerprint" translatable="false">Fingerprint</string>
|
||||
<string name="server_lab_stream_alpn">Alpn</string>
|
||||
<string name="server_lab_allow_insecure">اجازه نا ٱمن</string>
|
||||
<string name="server_lab_allow_insecure">هشتن SSL نا ٱمن</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">نشۊوی</string>
|
||||
<string name="server_lab_port3">پورت</string>
|
||||
@@ -81,13 +81,13 @@
|
||||
<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_short_id">ShortID</string>
|
||||
<string name="server_lab_preshared_key">کیلیت پؽش اشتراکی (اختیاری)</string>
|
||||
<string name="server_lab_short_id">شناسه کۊتال</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">Mldsa65Verify</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_reserved">Reserved(اختیاری، وا کاما (,) ز یک جوڌا ابۊن)</string>
|
||||
<string name="server_lab_local_address">نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما (,) ز یک جوڌا ابۊن)</string>
|
||||
<string name="server_lab_local_mtu">Mtu(اختیاری، پؽش فرز 1420)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پؽش فرز هسته 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پؽش فرز هسته 50)</string>
|
||||
@@ -117,30 +117,34 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra خام JSON، قالوو: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask خام JSON، قالوو: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">نومگه کانفیگ Ech</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">تئیڌ گوواهی هومتا و ری نوم</string>
|
||||
<string name="server_lab_pinned_ca256">جا کلک گوواهی (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">گرؽڌن وو پور کردن جا کلک گوواهی</string>
|
||||
<string name="toast_fetch_cert_sha256_success">جا کلک گوواهی وا مووفقیت گرؽڌه وابی</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">گرؽڌن جا کلک گوواهی مووفق نبی</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">لف گیری فایل ٱنجوم نوابی، ز ی برنومه دؽوۉداری فایل هیاری بگرین</string>
|
||||
<string name="menu_item_add_asset">ازاف کردن دارایی</string>
|
||||
<string name="menu_item_add_file">ازاف کردن فایل</string>
|
||||
<string name="menu_item_add_url">ازاف کردن لینگ</string>
|
||||
<string name="menu_item_add_asset">ٱووردن دارایی</string>
|
||||
<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="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="menu_item_invert_selection">پسند چواسه</string>
|
||||
<string name="msg_enter_keywords">کلمه کلیدی ن بزنین</string>
|
||||
<string name="menu_item_invert_selection">چواسه کردن پسندا</string>
|
||||
<string name="msg_enter_keywords">کلمه یل کلیدی ن بزنین</string>
|
||||
<string name="switch_bypass_apps_mode">هالت دور زیڌن</string>
|
||||
<string name="menu_item_select_proxy_app">پسند خوتکار پروکسی برنومه</string>
|
||||
<string name="msg_downloading_content">موئتوا هونی دانلود ابۊن</string>
|
||||
<string name="menu_item_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">سامووا ب تفکیک برنومه</string>
|
||||
@@ -153,14 +157,14 @@
|
||||
<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>
|
||||
<string name="summary_pref_per_app_proxy">پوی وولاتی: برنومه پسند وابیڌه وا ی پروکسی منپیز ابۊ، برنومه پسند نوابیڌه موستقیمن منپیز ابۊ.\nهالت دور زیڌن: برنومه پسند وابیڌه موستقیمن منپیز ابۊ، برنومه پسند نوابیڌه وا ی پروکسی منپیز ابۊ.\nپسند خوتکار برنومه یل پروکسی من نومگه امکووݩ پزیر هڌ.</string>
|
||||
<string name="title_pref_is_booted">منپیز خوتکار مجال ره ونی</string>
|
||||
<string name="summary_pref_is_booted">مجال ره وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ.</string>
|
||||
|
||||
<string name="title_pref_auto_remove_invalid_after_test">پاک کردن خوتکار کانفیگ نا موئتبر بئڌ آزمایش</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">نتیجه یل آزمایش گاشڌ دییق نبۊن؛ کانفیگ پاک وابیڌه ن نتری وورگنی.</string>
|
||||
<string name="title_pref_auto_sort_after_test">ترتیب خوتکار بئڌ آزمایش</string>
|
||||
<string name="summary_pref_auto_sort_after_test">نتیجه یل آزمایش گاشڌ دییق نبۊن؛</string>
|
||||
<string name="title_pref_auto_remove_invalid_after_test">پاک کردن خوتکار کانفیگا نا موئتبر بئڌ پینگ</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">نتیجه یل پینگ گاشڌ دییق نبۊن؛ کانفیگا پاک وابیڌه ن نتری وورگنی.</string>
|
||||
<string name="title_pref_auto_sort_after_test">ترتیب خوتکار بئڌ پینگ گرؽڌن</string>
|
||||
<string name="summary_pref_auto_sort_after_test">نتیجه یل پینگ گاشڌ دییق نبۊن.</string>
|
||||
|
||||
<string name="title_mux_settings">سامووا Mux</string>
|
||||
<string name="title_pref_mux_enabled">ره وندن Mux</string>
|
||||
@@ -175,21 +179,21 @@
|
||||
</string-array>
|
||||
|
||||
<string name="title_pref_speed_enabled">ره وندن نشووݩ داڌن سورعت</string>
|
||||
<string name="summary_pref_speed_enabled">نشووݩ داڌن سورعت هیم سکویی من وارسۊویا. نماڌ وارسۊوی و ری و کار گرؽڌن کانفیگ آلشت ابۊ.</string>
|
||||
<string name="summary_pref_speed_enabled">نشووݩ داڌن سورعت هیم سکویی من وارسۊویا.\nنماڌ وارسۊوی و ری و کار گرؽڌن کانفیگ آلشت ابۊ.</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">ره وندن Sniffing</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="title_pref_sniffing_enabled">ره وندن تجزیه وو تئلیل کتنا (Sniffing)</string>
|
||||
<string name="summary_pref_sniffing_enabled">قپ ریت سی و در کشیڌن نوم دامنه واقعی ز کتنا (پؽش فرز رۊشن هڌ)</string>
|
||||
<string name="title_pref_route_only_enabled">ره وندن تینا تور جوستن routeOnly</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 پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین فعال بۊ)</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">دی ان اس مهلی نشۊویا آی پی جئلی ن اوورگنه (زل تر، ٱما گاشڌ من ی قرده ز برنومه یل کار نکونه)</string>
|
||||
|
||||
<string name="title_pref_ipv6_enabled">ره وندن IPv6</string>
|
||||
<string name="summary_pref_ipv6_enabled">نشۊویا IPv6 وو تورا IPv6 ن ز رابت VPN ره ونین</string>
|
||||
<string name="summary_pref_ipv6_enabled">نشۊویا وو تورا IPv6 ن و رابت VPN ازاف کۊنین</string>
|
||||
|
||||
<string name="title_pref_prefer_ipv6">ترجی داڌن IPv6</string>
|
||||
<string name="summary_pref_prefer_ipv6">مجال هل وو فسل نوم دامنه، نشۊویا IPv6 ن ترجی بڌین</string>
|
||||
@@ -211,26 +215,23 @@
|
||||
|
||||
<string name="title_pref_delay_test_url">نشۊوی اینترنتی آزمایش تئخیر واقعی</string>
|
||||
<string name="summary_pref_delay_test_url">نشۊوی اینترنتی</string>
|
||||
<string name="title_pref_real_ping_concurrency">هموورگی آزمایش تئخیر واقعی</string>
|
||||
<string name="title_pref_real_ping_concurrency">تعداد هوم زمووی تست تئخیر واقعی</string>
|
||||
|
||||
<string name="title_pref_ip_api_url">نشۊوی اینترنتی آزمایش دووسمندیا منپیز هیم سکویی</string>
|
||||
<string name="summary_pref_ip_api_url">نشۊوی اینترنتی</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">هشتن منپیزا ز شبکه مهلی</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>
|
||||
<string name="summary_pref_allow_insecure">مجال و کار بوردن TLS ب تۉر پؽش فرز، موجوز نا ٱمن فعال هڌ.</string>
|
||||
<string name="title_pref_proxy_sharing_enabled">یک رسۊوی پروکسی من شبکه مهلی LAN</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">پوی دسگایل ترن وا نشۊوی IP ایسا، ز ره socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل ائتماد ره ونده بۊ تا ز منپیز قیر موجاز جلو گری بۊ.</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">یک رسۊوی من شبکه مهلی ره وست؛ موتمعن بۊین ک من ی شبکه ٱمن هڌین.</string>
|
||||
|
||||
<string name="title_pref_socks_port">پورت پروکسی مهلی</string>
|
||||
<string name="title_pref_enable_local_proxy">ره وندن پروکسی مهلی</string>
|
||||
<string name="summary_pref_enable_local_proxy">هرسا قیرفعال بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو ورۊ رسۊوی یل اشتراک وو کارا جۊر یو نترن ز پروکسی استفاڌه کونن.</string>
|
||||
<string name="summary_pref_enable_local_proxy">هرسا ره نوسته بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو کارایی جۊر ورۊ رسۊوی یل اشتراک نترن پروکسی ن و کار گرن.</string>
|
||||
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
|
||||
<string name="summary_pref_socks_enable_udp">ائراز هۊویت socks5 مجال استفاڌه ز UDP کاملن ایمن نؽ</string>
|
||||
<string name="summary_pref_socks_port">پورت پروکسی مهلی</string>
|
||||
<string name="title_pref_dynamic_socks_port">ره وندن پورت پروکسی مهلی پۊیا</string>
|
||||
<string name="summary_pref_dynamic_socks_port">هر کرت ک و من ٱووڌنی وورکل ابۊ، ی پورت پروکسی مهلی تساڌۊفی وورکل کۊنین</string>
|
||||
<string name="title_pref_dynamic_socks_port">وورکل پورت موتقیر سی پروکسی مهلی</string>
|
||||
<string name="summary_pref_dynamic_socks_port">هر کرت ک و من ٱووڌنی وورکل ابۊ، ی پورت پروکسی مهلی تساڌۊفی وورکل اکونه.</string>
|
||||
<string name="title_pref_socks_username">نوم منتوری پروکسی مهلی (اختیاری)</string>
|
||||
<string name="summary_pref_socks_username">نوم منتوری</string>
|
||||
<string name="title_pref_socks_password">رزم پروکسی مهلی (اختیاری)</string>
|
||||
@@ -240,18 +241,18 @@
|
||||
<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="title_pref_start_scan_immediate">زی بنه پا اسکن کود QR</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="title_pref_double_column_display">ره وندن نشووݩ داڌن دو سۊتۊنی</string>
|
||||
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ره وستن، وا برنومه ن ز نۊ ره ونین.</string>
|
||||
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینوو ترین موئتوا بیشتری ن سیل کۊنین.\nسی ره وستن، وا برنومه ن ز نۊ ره ونین.</string>
|
||||
|
||||
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن فعال کۊنین</string>
|
||||
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن ره ونین</string>
|
||||
<string name="summary_pref_group_all_display">ی بلگه «پوی بونکۊ یل کانفیگ» ازاف کۊنین</string>
|
||||
<!-- AboutActivity -->
|
||||
<string name="title_pref_feedback">فشناڌن منشڌ</string>
|
||||
@@ -268,48 +269,54 @@
|
||||
<string name="title_pref_promotion">تبلیقات</string>
|
||||
|
||||
<string name="title_pref_auto_update_subscription">ورۊ کردن خوتکار اشتراکا</string>
|
||||
<string name="summary_pref_auto_update_subscription">اشتراکا خوتۉ ن و تۉر خوتکار وا فاسله زمۊوی من پس زمینه ورۊ کۊنین. ای ویژیی من پوی دسگایل گاشڌ همیشه کار نکونه</string>
|
||||
<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">بارت پؽش هل دامنه دری</string>
|
||||
<string name="title_mode">هالت</string>
|
||||
<string name="title_mode_help">سی دووسمندیا وو هیاری بیشتر، ری ای هؽل بزݩ</string>
|
||||
<string name="title_mode_help">سی دووسمندیا وو هیاری قلوه، ری ای هؽل بزݩ</string>
|
||||
<string name="title_language">زووݩ</string>
|
||||
<string name="title_ui_settings">سامووا رابت منتوری</string>
|
||||
<string name="title_pref_ui_mode_night">سامووا هالت رابت منتوری</string>
|
||||
<string name="title_pref_use_hev_tunnel">فعال کردن ویژیی Hev TUN</string>
|
||||
<string name="summary_pref_use_hev_tunnel">مجالی ک فعال بۊ، TUN ز hev-socks5-tunnel استفاڌه اکونه؛ ٱر فعال نبۊ، زهمو xray-core استفاڌه اکونه.</string>
|
||||
<string name="title_pref_use_hev_tunnel">و کار گرؽڌن ویژیی Hev TUN</string>
|
||||
<string name="summary_pref_use_hev_tunnel">مجالی ک ره ونده بۊ، TUN ایا hev-socks5-tunnel ن و کار اگره؛ ٱر ره نوۊفته بۊ، همو xray-core ن و کار اگره.</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">Hev Tun سئت گوزارشا</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">زمووݩ مندیر بیڌن خوندن/هؽل کردن Hev Tun (سانیه) (پؽش فرز tcp، udp 300،60)</string>
|
||||
<string name="title_mode_settings">سامووا هالت</string>
|
||||
<string name="title_root_mode_enabled">هالت رۊت ره وست (منتورووݩ رۊت)</string>
|
||||
<string name="summary_root_mode_enabled">ره وستن پروکسی شفاف سئت بلم وا امتیازا ریشه. ای هالت، VPN استاندارد سیستوم ٱندروید ن دور ازنه تا موستقیمن پوی ترافیک شبکه لمی ن وا دست بگره. بئڌ ره وندن، هالت معمۊلی ٱسلی، تور جۊری هر برنومه وو سامووا VPN مربۊته کاملن ناموعتبر ابۊن.</string>
|
||||
<string name="toast_root_required">دسرسی رۊت سی ای ویژیی لازوم هڌ</string>
|
||||
<string name="title_root_lan_sharing">یک رسۊوی LAN / تترینگ (منتورووݩ رۊت وابیڌه)</string>
|
||||
<string name="summary_root_lan_sharing">ترافیک هاتاسپات وایفای وو دسگا یل منپیز وابیڌه وا USB (تترینگ) ن ز تور VPN بفشنین.</string>
|
||||
|
||||
<string name="title_logcat">گوزارشا</string>
|
||||
<string name="logcat_copy">لف گیری</string>
|
||||
<string name="logcat_clear">روفتن</string>
|
||||
<string name="title_service_restart">ره وندن دووارته خدمات</string>
|
||||
<string name="logcat_share">یک رسۊوی گوزارش</string>
|
||||
<string name="title_service_restart">ره وندن دووارته سرویس</string>
|
||||
<string name="title_del_all_config">پاک کردن پوی کانفیگا بونکۊ سکویی</string>
|
||||
<string name="title_del_duplicate_config">پاک کردن کانفیگا تکراری بونکۊ سکویی</string>
|
||||
<string name="title_del_invalid_config">پاک کردن کانفیگا نا موئتبر بونکۊ سکویی</string>
|
||||
<string name="title_export_all">و در کشیڌن کانفیگا غیر سفارشی بونکۊ سکویی من کلیپ بورد</string>
|
||||
<string name="title_export_all">و در کشیڌن کانفیگا قیر سفارشی بونکۊ سکویی من کلیپ بورد</string>
|
||||
<string name="title_sub_setting">سامووا بونکۊ اشتراک</string>
|
||||
<string name="sub_setting_remarks">نیشتنا</string>
|
||||
<string name="sub_setting_url">نشۊوی اینترنتی اختیاری</string>
|
||||
<string name="sub_setting_user_agent">User Agent</string>
|
||||
<string name="sub_setting_filter">نوم موستعار فیلتر</string>
|
||||
<string name="sub_setting_enable">فعال بیڌن ورۊ کردن</string>
|
||||
<string name="sub_auto_update">فعال بیڌن ورۊ کردن خوتکار</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="toast_invalid_update_interval">فاسله ورۊ کردن نا موئتبر هڌ. اقل مقدار 15 دؽقه هڌ.</string>
|
||||
<string name="toast_invalid_update_interval">فاسله ورۊ کردن نا موئتبر هڌ. هدقل مقدار 15 دؽقه هڌ.</string>
|
||||
<string name="title_sub_update">ورۊ کردن اشتراک بونکۊ سکویی</string>
|
||||
<string name="title_ping_all_server">Tcping کانفیگا بونکۊ سکویی</string>
|
||||
<string name="title_real_ping_all_server">تئخیر واقعی کانفیگا بونکۊ سکویی</string>
|
||||
<string name="title_user_asset_setting">فایلا بونچک جوقرافیایی</string>
|
||||
<string name="title_sort_by_test_results">ترتیب و ری نتیجه یل آزمایش</string>
|
||||
<string name="title_sort_by_test_results">ترتیب و ری نتیجه یل پینگ</string>
|
||||
<string name="title_filter_config">فیلتر کردن کانفیگا</string>
|
||||
<string name="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>
|
||||
@@ -322,23 +329,23 @@
|
||||
<string name="toast_fragment_not_available">نتره نما سکویی ن بجۊره</string>
|
||||
<string name="title_locate_selected_config">کانفیگ پسند وابیڌه ن بجۊرین</string>
|
||||
|
||||
<string name="tasker_start_service">ره وندن خدمات</string>
|
||||
<string name="tasker_start_service">ره وندن سرویس</string>
|
||||
<string name="tasker_setting_confirm">قوۊل</string>
|
||||
|
||||
<!-- RoutingSettingActivity -->
|
||||
<string name="routing_settings_domain_strategy">نشقه دامنه</string>
|
||||
<string name="routing_settings_title">سامووا تور جوستن</string>
|
||||
<string name="routing_settings_tips">وا کاما ز یک جوڌا ابۊن (,)؛ یکی ن بزنین: domain، ip یا process</string>
|
||||
<string name="routing_settings_tips">وا کاما (,) ز یک جوڌا ابۊن؛ یکی ن بزنین: domain، ip یا process</string>
|
||||
<string name="routing_settings_save">زفت کردن</string>
|
||||
<string name="routing_settings_delete">روفتن</string>
|
||||
<string name="routing_settings_rule_title">سامووا قانۉݩ تور جوستن</string>
|
||||
<string name="routing_settings_add_rule">ازاف کردن قانۉݩ</string>
|
||||
<string name="routing_settings_rule_title">سامووا قانووݩ تور جوستن</string>
|
||||
<string name="routing_settings_add_rule">ٱووردن قانووݩ</string>
|
||||
<string name="routing_settings_import_predefined_rulesets">و من ٱووردن قانووا</string>
|
||||
<string name="routing_settings_import_rulesets_tip">قانووایی ک هیم سکو هڌسۉݩ پاک ابۊن، هنی هم اخۊی پاکسۉݩ کۊنی؟</string>
|
||||
<string name="routing_settings_import_rulesets_tip">قانووایی ک هیم سکو هڌسووݩ پاک ابۊن، هنی هم اخۊی پاکسووݩ کۊنی؟</string>
|
||||
<string name="routing_settings_import_rulesets_from_clipboard">و من ٱووردن قانووا ز کلیپ بورد</string>
|
||||
<string name="routing_settings_import_rulesets_from_qrcode">و من ٱووردن قانووا ز QRcode</string>
|
||||
<string name="routing_settings_export_rulesets_to_clipboard">و در کشیڌن قانووا وو زفت من کلیپ بورد</string>
|
||||
<string name="routing_settings_locked">چفت هڌ، ای قانؤنن مجال و من ٱووردن ز پؽش سامووا زفت کۊنین</string>
|
||||
<string name="routing_settings_locked">چفت هڌ، ای قانووݩ ن مجال و من ٱووردن ز پؽش سامووا زفت کۊنین</string>
|
||||
<string name="routing_settings_domain" translatable="false">domain</string>
|
||||
<string name="routing_settings_ip" translatable="false">ip</string>
|
||||
<string name="routing_settings_port" translatable="false">port</string>
|
||||
@@ -359,13 +366,14 @@
|
||||
<string name="connection_not_connected">منپیز نؽڌ</string>
|
||||
<string name="connection_runing_task_left">تعداد وزیفه یل آزمایشی ک ره اۊفتانه: %s</string>
|
||||
|
||||
<string name="import_subscription_success">اشتراک وا موفقیت زفت زابی</string>
|
||||
<string name="import_subscription_success">اشتراک وا مووفقیت زفت وابی</string>
|
||||
<string name="import_subscription_failure">اشتراک زفت نوابی</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="title_pref_fragment_maxsplit">هدکسر تقسیم فرگمنت</string>
|
||||
|
||||
<string name="update_check_for_update">واجۊری سی ورۊ رسۊوی</string>
|
||||
<string name="update_already_latest_version">سکو نوسخه دیندایی پۊرنیڌه هڌ</string>
|
||||
@@ -377,12 +385,12 @@
|
||||
<string name="title_policy_group_type">نوع بونکۊ سیاست</string>
|
||||
<string name="title_policy_group_subscription_id">ز بونکۊ اشتراک</string>
|
||||
<string name="title_policy_group_subscription_filter">توزیهات فیلتر معمۊلی</string>
|
||||
<string name="server_proxy_chain_members">اعضای زنجیره پروکسی</string>
|
||||
<string name="server_proxy_chain_pick_members">اینجا بزنین تا عضو اِنتخاب اکه</string>
|
||||
<string name="server_proxy_chain_member_unselected">یه عضو اِنتخاب کۊنین</string>
|
||||
<string name="server_proxy_chain_members_unselected">لطفا سی هر ردیو زنجیره، remark اِنتخاب کۊنین</string>
|
||||
<string name="server_proxy_chain_members_insufficient">تعداد عضوها کافی نیس</string>
|
||||
<string name="server_proxy_chain_members_invalid">اعضای نامعتبر زنجیره: %1$s</string>
|
||||
<string name="server_proxy_chain_members">ٱئزا زنجیره پروکسی</string>
|
||||
<string name="server_proxy_chain_pick_members">سی پسند عوزو ایچو بزنین</string>
|
||||
<string name="server_proxy_chain_member_unselected">ی عوزو پسند کۊنین</string>
|
||||
<string name="server_proxy_chain_members_unselected">سی هر ردیف زنجیره remark پسند کۊنین</string>
|
||||
<string name="server_proxy_chain_members_insufficient">تعداد ٱئزا بس نؽ</string>
|
||||
<string name="server_proxy_chain_members_invalid">ٱئزا ناموئتبر زنجیره: %1$s</string>
|
||||
|
||||
<!-- BackupActivity -->
|
||||
<string name="title_configuration_backup_restore">لادراری گرؽڌن & وورگندن</string>
|
||||
@@ -447,7 +455,7 @@
|
||||
</string-array>
|
||||
<string-array name="policy_group_type">
|
||||
<item>کم ترین پینگ</item>
|
||||
<item>کم ترین بار(لود)</item>
|
||||
<item>کم ترین بار (لود)</item>
|
||||
<item>تساڌۊفی</item>
|
||||
<item>گردشی</item>
|
||||
</string-array>
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
<resources>
|
||||
<string name="app_widget_name">تعویض</string>
|
||||
<string name="app_tile_name">تعویض</string>
|
||||
<string name="app_tile_first_use">برای اولین بار از این ویژگی استفاده میکنید، لطفا از برنامه برای افزودن سرور استفاده کنید</string>
|
||||
<string name="app_tile_first_use">برای اولین استفاده از این قابلیت، لطفاً ابتدا از داخل برنامه یک سرور اضافه کنید</string>
|
||||
<string name="navigation_drawer_open">باز کردن منو کشویی</string>
|
||||
<string name="navigation_drawer_close">بستن منو کشویی</string>
|
||||
<string name="migration_success">موفقیت در انتقال داده</string>
|
||||
<string name="action_stop_service">توقف کردن سرویس</string>
|
||||
<string name="migration_fail">انتقال داده انجام نشد!</string>
|
||||
<string name="pull_down_to_refresh">لطفاً برای تازه کردن، پایین بکشید!</string>
|
||||
<string name="migration_success">انتقال دادهها با موفقیت انجام شد!</string>
|
||||
<string name="action_stop_service">توقف سرویس</string>
|
||||
<string name="migration_fail">انتقال دادهها با شکست مواجه شد!</string>
|
||||
<string name="pull_down_to_refresh">برای بروزرسانی، صفحه را به پایین بکشید!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_action_stop_v2ray">توقف</string>
|
||||
<string name="toast_permission_denied">دریافت مجوز امکان پذیر نیست</string>
|
||||
<string name="toast_permission_denied_notification">دریافت مجوز اعلان امکان پذیر نیست</string>
|
||||
<string name="notification_action_more">برای کسب اطلاعات بیشتر کلیک کنید</string>
|
||||
<string name="toast_permission_denied">امکان دریافت مجوز وجود ندارد</string>
|
||||
<string name="toast_permission_denied_notification">امکان دریافت مجوز ارسال اعلان وجود ندارد</string>
|
||||
<string name="notification_action_more">برای اطلاعات بیشتر کلیک کنید</string>
|
||||
<string name="toast_services_start">شروع خدمات</string>
|
||||
<string name="toast_services_stop">توقف خدمات</string>
|
||||
<string name="toast_services_success">شروع خدمات با موفقیت انجام شد</string>
|
||||
<string name="toast_services_failure">شروع خدمات با موفقیت انجام نشد!</string>
|
||||
<string name="toast_services_stop">توقف سرویس</string>
|
||||
<string name="toast_services_success">سرویس با موفقیت شروع شد</string>
|
||||
<string name="toast_services_failure">شروع سرویس با شکست مواجه شد</string>
|
||||
|
||||
<!--ServerActivity-->
|
||||
<string name="title_server">فایل کانفیگ</string>
|
||||
@@ -29,18 +29,18 @@
|
||||
<string name="menu_item_import_config_qrcode">کانفیگ را از QRcode وارد کنید</string>
|
||||
<string name="menu_item_import_config_clipboard">کانفیگ را از کلیپ بورد وارد کنید</string>
|
||||
<string name="menu_item_import_config_local">کانفیگ را از محلی وارد کنید</string>
|
||||
<string name="menu_item_import_config_policy_group">اضافه کردن [گروه خط مشی]</string>
|
||||
<string name="menu_item_import_config_proxy_chain">Add [Proxy chain]</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>
|
||||
<string name="menu_item_import_config_manually_socks">تایپ دستی[SOCKS]</string>
|
||||
<string name="menu_item_import_config_manually_http">تایپ دستی[HTTP]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">تایپ دستی[TROJAN]</string>
|
||||
<string name="menu_item_import_config_manually_wireguard">تایپ دستی[WIREGUARD]</string>
|
||||
<string name="menu_item_import_config_manually_hysteria2">تایپ دستی[HYSTERIA2]</string>
|
||||
<string name="del_config_comfirm">حذف شود؟</string>
|
||||
<string name="del_invalid_config_comfirm">لطفا قبل از حذف کانفیگ نامعتبر بررسی کنید! حذف کانفیگ را تایید می کنید؟</string>
|
||||
<string name="menu_item_import_config_policy_group">افزودن [گروه خط مشی]</string>
|
||||
<string name="menu_item_import_config_proxy_chain">افزودن [زنجیره پروکسی]</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>
|
||||
<string name="menu_item_import_config_manually_socks">افزودن [SOCKS]</string>
|
||||
<string name="menu_item_import_config_manually_http">افزودن [HTTP]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">افزودن [Trojan]</string>
|
||||
<string name="menu_item_import_config_manually_wireguard">افزودن [Wireguard]</string>
|
||||
<string name="menu_item_import_config_manually_hysteria2">افزودن [Hysteria2]</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_port">پورت</string>
|
||||
@@ -49,7 +49,7 @@
|
||||
<string name="server_lab_security">امنیت</string>
|
||||
<string name="server_lab_network">شبکه</string>
|
||||
<string name="server_lab_more_function">انتقال</string>
|
||||
<string name="server_lab_head_type">نوع سربرگ</string>
|
||||
<string name="server_lab_head_type">نوع هدر</string>
|
||||
<string name="server_lab_mode_type">حالت gRPC</string>
|
||||
<string name="server_lab_request_host">هاست</string>
|
||||
<string name="server_lab_request_host_http">هاست HTTP</string>
|
||||
@@ -57,7 +57,7 @@
|
||||
<string name="server_lab_request_host_httpupgrade">هاست HTTPUpgrade</string>
|
||||
<string name="server_lab_request_host_xhttp">هاست XHTTP</string>
|
||||
<string name="server_lab_request_host_h2">هاست H2</string>
|
||||
<string name="server_lab_request_host_quic">QUIC security</string>
|
||||
<string name="server_lab_request_host_quic">QUIC امنیت</string>
|
||||
<string name="server_lab_request_host_grpc">gRPC Authority</string>
|
||||
<string name="server_lab_path">مسیر</string>
|
||||
<string name="server_lab_path_ws">مسیر WS</string>
|
||||
@@ -65,12 +65,12 @@
|
||||
<string name="server_lab_path_xhttp">مسیر XHTTP</string>
|
||||
<string name="server_lab_path_h2">مسیر H2</string>
|
||||
<string name="server_lab_path_quic">مسیر QUIC</string>
|
||||
<string name="server_lab_path_kcp">KCP seed</string>
|
||||
<string name="server_lab_path_grpc">gRPC ServiceName</string>
|
||||
<string name="server_lab_path_kcp">KCP سید</string>
|
||||
<string name="server_lab_path_grpc">gRPC نام سرویس</string>
|
||||
<string name="server_lab_stream_security">TLS</string>
|
||||
<string name="server_lab_stream_fingerprint">اثرانگشت</string>
|
||||
<string name="server_lab_stream_alpn">Alpn</string>
|
||||
<string name="server_lab_allow_insecure">اعطای مجوز ناامن</string>
|
||||
<string name="server_lab_allow_insecure">مجوز SSL ناامن</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">نشانی</string>
|
||||
<string name="server_lab_port3">پورت</string>
|
||||
@@ -81,19 +81,19 @@
|
||||
<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_short_id">ShortID</string>
|
||||
<string name="server_lab_preshared_key">کلید پیشاشتراکی (اختیاری)</string>
|
||||
<string name="server_lab_short_id">شناسه کوتاه</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">Mldsa65Verify</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_mtu">MTU (اختیاری، پیشفرض 1420)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پیشفرض هسته 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پیشفرض هسته 50)</string>
|
||||
<string name="toast_success">با موفقیت انجام شد</string>
|
||||
<string name="toast_failure">شکست</string>
|
||||
<string name="toast_none_data">هیچ داده ای وجود ندارد</string>
|
||||
<string name="toast_success">موفقیتآمیز</string>
|
||||
<string name="toast_failure">شکست!</string>
|
||||
<string name="toast_none_data">دادهای یافت نشد</string>
|
||||
<string name="toast_incorrect_protocol">پروتکل نادرست</string>
|
||||
<string name="toast_decoding_failed">رمزگشایی انجام نشد</string>
|
||||
<string name="title_file_chooser">انتخاب فایل کانفیگ</string>
|
||||
@@ -101,8 +101,8 @@
|
||||
<string name="server_customize_config">کانفیگ سفارشی</string>
|
||||
<string name="toast_config_file_invalid">کانفیگ معتبر نیست</string>
|
||||
<string name="server_lab_content">محتوا</string>
|
||||
<string name="toast_none_data_clipboard">هیچ داده ای در کلیپ بورد وجود ندارد</string>
|
||||
<string name="toast_invalid_url">نشانی اینترنتی معتبر نیست</string>
|
||||
<string name="toast_none_data_clipboard">دادهای در کلیپبورد وجود ندارد</string>
|
||||
<string name="toast_invalid_url">نشانی اینترنتی نامعتبر است</string>
|
||||
<string name="toast_insecure_url_protocol">لطفاً از آدرس های اشتراک پروتکل HTTP ناامن استفاده نکنید</string>
|
||||
<string name="server_lab_need_inbound">اطمینان حاصل کنید که پورت ورودی با تنظیمات مطابقت دارد</string>
|
||||
<string name="toast_malformed_josn">کانفیگ درست نیست</string>
|
||||
@@ -117,32 +117,36 @@
|
||||
<string name="server_lab_xhttp_extra">خام JSON XHTTP Extra، قالب: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">لیست کانفیگ Ech</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">کپی فایل انجام نشد، لطفا از برنامه مدیریت فایل استفاده کنید</string>
|
||||
<string name="toast_asset_copy_failed">کپی فایل شکست خورد، لطفا از برنامه مدیریت فایل استفاده کنید</string>
|
||||
<string name="menu_item_add_file">افزودن فایل ها</string>
|
||||
<string name="menu_item_scan_qrcode">اسکن QRcode</string>
|
||||
<string name="title_url">URL</string>
|
||||
<string name="menu_item_download_file">دانلود فایل ها</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="menu_item_invert_selection">Invert selection</string>
|
||||
<string name="msg_enter_keywords">کلمه ی کلیدی را وارد نمایید</string>
|
||||
<string name="menu_item_invert_selection">معکوس کردن انتخابها</string>
|
||||
<string name="msg_enter_keywords">کلمات کلیدی را وارد کنید</string>
|
||||
<string name="switch_bypass_apps_mode">حالت دور زدن</string>
|
||||
<string name="menu_item_select_proxy_app">انتخاب خودکار پروکسی برنامه</string>
|
||||
<string name="msg_downloading_content">در حال دانلود محتوا</string>
|
||||
<string name="menu_item_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">تنظیمات به تفکیک برنامه</string>
|
||||
<string name="per_app_proxy_settings_enable">فعال کردن به تفکیک برنامه</string>
|
||||
<string name="per_app_proxy_settings_enable">فعالسازی به تفکیک برنامه</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">تنظیمات</string>
|
||||
@@ -154,10 +158,10 @@
|
||||
<string name="title_pref_is_booted">اتصال خودکار هنگام راه اندازی</string>
|
||||
<string name="summary_pref_is_booted">هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد.</string>
|
||||
|
||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||
<string name="title_pref_auto_remove_invalid_after_test">حذف خودکار کانفیگهای نامعتبر بعد از پینگ</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">نتایج پینگ ممکن است همیشه دقیق نباشند؛ کانفیگهای حذفشده قابل بازیابی نیستند.</string>
|
||||
<string name="title_pref_auto_sort_after_test">مرتبسازی خودکار بعد از پینگ گرفتن</string>
|
||||
<string name="summary_pref_auto_sort_after_test">نتایج آزمایش ممکن است کاملاً دقیق نباشند.</string>
|
||||
|
||||
<string name="title_mux_settings">تنظیمات MUX</string>
|
||||
<string name="title_pref_mux_enabled">فعال کردن MUX</string>
|
||||
@@ -175,8 +179,8 @@
|
||||
<string name="summary_pref_speed_enabled">نمایش سرعت فعلی در قسمت اعلان. \nآیکون اعلان بر اساس استفاده تغییر میکند.</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">فعال کردن تجزیه و تحلیل بسته ها (Sniffing)</string>
|
||||
<string name="summary_pref_sniffing_enabled">استفاده از تشخیص نام دامنه (Sniff) در بسته ها (به طور پیش فرض فعال است)</string>
|
||||
<string name="title_pref_route_only_enabled">فعال کردن دامنه فقط مسیر یابی (RouteOnly)</string>
|
||||
<string name="summary_pref_sniffing_enabled">تلاش برای استخراج نام دامنه واقعی از بستههای ترافیک (بهطور پیشفرض روشن است)</string>
|
||||
<string name="title_pref_route_only_enabled">فعالسازی فقط مسیر یابی (RouteOnly)</string>
|
||||
<string name="summary_pref_route_only_enabled">از نام دامنه (Snnifed) فقط برای مسیریابی استفاده کنید و آدرس مقصد را به عنوان IP ذخیره کنید.</string>
|
||||
|
||||
|
||||
@@ -214,33 +218,30 @@
|
||||
<string name="title_pref_ip_api_url">تست اطلاعات اتصال فعلی url</string>
|
||||
<string name="summary_pref_ip_api_url">Url</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">اجازه اتصالات از طریق شبکه محلی</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">سایر دستگاه ها می توانند با استفاده از آدرس آیپی شما برای استفاده از یک پروکسی محلی متصل شوند. فقط در یک شبکه قابل اعتماد برای جلوگیری از اتصالات غیرمجاز استفاده کنید.</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">اتصالات از طریق شبکه محلی را مجاز کنید، مطمئن شوید که در یک شبکه قابل اعتماد هستید.</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">اعطای مجوز ناامن</string>
|
||||
<string name="summary_pref_allow_insecure">هنگام استفاده از TLS، به طور پیش فرض مجوز ناامن فعال است.</string>
|
||||
<string name="title_pref_proxy_sharing_enabled">اشتراکگذاری پروکسی در شبکه محلی (LAN)</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">دستگاههای دیگر میتوانند با وارد کردن IP شما، از پروکسیتان استفاده کنند. برای جلوگیری از سوءاستفاده، فقط در شبکههای امن و قابل اعتماد این گزینه را روشن کنید.</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">اشتراکگذاری در شبکه محلی فعال شد؛ مطمئن شوید در یک شبکه امن هستید.</string>
|
||||
|
||||
<string name="title_pref_socks_port">پورت پروکسی محلی</string>
|
||||
<string name="title_pref_enable_local_proxy">فعالسازی پروکسی محلی</string>
|
||||
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
|
||||
<string name="summary_pref_enable_local_proxy">اگر غیرفعال باشد، هیچ پروکسی SOCKS در دسترس نخواهد بود و کارهایی مثل آپدیت اشتراکها نمیتوانند از پروکسی استفاده کنند.</string>
|
||||
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
|
||||
<string name="summary_pref_socks_enable_udp">احراز هویت socks5 هنگام استفاده از UDP کاملاً ایمن نیست</string>
|
||||
<string name="summary_pref_socks_port">پورت پروکسی محلی</string>
|
||||
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
|
||||
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
|
||||
<string name="title_pref_socks_username">Local proxy username (optional)</string>
|
||||
<string name="summary_pref_socks_username">Username</string>
|
||||
<string name="title_pref_socks_password">Local proxy password (optional)</string>
|
||||
<string name="summary_pref_socks_password">Password</string>
|
||||
<string name="title_pref_dynamic_socks_port">تولید پورت متغیر برای پروکسی محلی</string>
|
||||
<string name="summary_pref_dynamic_socks_port">با هر بار ایجاد یک کانکشن ورودی، یک پورت تصادفی تولید میکند.</string>
|
||||
<string name="title_pref_socks_username">نام کاربری پروکسی محلی (اختیاری)</string>
|
||||
<string name="summary_pref_socks_username">نام کاربری</string>
|
||||
<string name="title_pref_socks_password">رمز عبور پروکسی محلی (اختیاری)</string>
|
||||
<string name="summary_pref_socks_password">رمز عبور</string>
|
||||
|
||||
<string name="title_pref_local_dns_port">پورت DNS محلی</string>
|
||||
<string name="summary_pref_local_dns_port">پورت DNS محلی</string>
|
||||
|
||||
<string name="title_pref_confirm_remove">تایید حذف فایل کانفیگ</string>
|
||||
<string name="summary_pref_confirm_remove">آیا برای حذف فایل کانفیگ نیاز به تایید دوم توسط کاربر است</string>
|
||||
<string name="title_pref_confirm_remove">تایید حذف کانفیگ</string>
|
||||
<string name="summary_pref_confirm_remove">آیا برای حذف کانفیگ نیاز به تایید دوم توسط کاربر است</string>
|
||||
|
||||
<string name="title_pref_start_scan_immediate">فورا اسکن را شروع کن</string>
|
||||
<string name="title_pref_start_scan_immediate">شروع فوری اسکن QR کد</string>
|
||||
<string name="summary_pref_start_scan_immediate">دوربین را برای اسکن بلافاصله در هنگام راه اندازی باز کنید، در غیر این صورت می توانید کد را اسکن کنید یا عکسی را در نوار ابزار انتخاب کنید.</string>
|
||||
|
||||
<string name="title_pref_append_http_proxy">پروکسی HTTP را به VPN اضافه کنید</string>
|
||||
@@ -275,14 +276,15 @@
|
||||
<string name="title_language">زبان</string>
|
||||
<string name="title_ui_settings">تنظیمات رابط کاربری</string>
|
||||
<string name="title_pref_ui_mode_night">تنظیمات حالت رابط کاربری</string>
|
||||
<string name="title_pref_use_hev_tunnel">Enable Hev TUN Feature</string>
|
||||
<string name="summary_pref_use_hev_tunnel">When enabled, TUN will use hev-socks5-tunnel; otherwise, it will use xray-core.</string>
|
||||
<string name="title_pref_use_hev_tunnel">استفاده از قابلیت Hev TUN</string>
|
||||
<string name="summary_pref_use_hev_tunnel">اگر فعال باشد، تونل VPN از hev-socks5-tunnel استفاده میکند؛ در غیر این صورت از xray-core استفاده خواهد شد.</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">سطح گزارشات Hev Tun</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">زمان انتظار خواندن/نوشتن Hev Tun (ثانیه) (پیشفرض tcp,udp 300،60)</string>
|
||||
|
||||
<string name="title_logcat">گزارشات</string>
|
||||
<string name="logcat_copy">کپی</string>
|
||||
<string name="logcat_clear">پاک کردن</string>
|
||||
<string name="logcat_share">اشتراکگذاری گزارش</string>
|
||||
<string name="title_service_restart">راهاندازی مجدد خدمات</string>
|
||||
<string name="title_del_all_config">حذف تمام کانفیگ های گروه فعلی</string>
|
||||
<string name="title_del_duplicate_config">حذف کانفیگ های تکراری گروه فعلی</string>
|
||||
@@ -301,23 +303,22 @@
|
||||
<string name="sub_setting_pre_profile_tip">لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است</string>
|
||||
<string name="toast_invalid_update_interval">بازه بروزرسانی نامعتبر است. حداقل مقدار 15 دقیقه است.</string>
|
||||
<string name="title_sub_update">بهروزرسانی گروه فعلی اشتراک</string>
|
||||
<string name="title_ping_all_server">TCPING کانفیگ های گروه فعلی</string>
|
||||
<string name="title_real_ping_all_server">تاخیر واقعی کانفیگ های گروه فعلی</string>
|
||||
<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">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>
|
||||
<string name="title_import_config_count">وارد کردن %d کانفیگ</string>
|
||||
<string name="title_export_config_count">صادر کردن %d کانفیگ</string>
|
||||
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
|
||||
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||
<string name="title_update_subscription_no_subscription">No subscriptions</string>
|
||||
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||
<string name="toast_fragment_not_available">Unable to locate current view</string>
|
||||
<string name="title_locate_selected_config">Locate the selected config</string>
|
||||
<string name="title_update_subscription_result">بروزرسانی %1$d کانفیگ انجام شد (%2$d موفق، %3$d شکست خورده، %4$d نادیده گرفته شده)</string>
|
||||
<string name="title_update_subscription_no_subscription">هیچ اشتراکی وجود ندارد</string>
|
||||
<string name="toast_server_not_found_in_group">سرور انتخابشده در گروه فعلی پیدا نشد</string>
|
||||
<string name="toast_fragment_not_available">موقعیت صفحه فعلی پیدا نشد</string>
|
||||
<string name="title_locate_selected_config">پیدا کردن کانفیگ متصلشده در لیست</string>
|
||||
|
||||
<string name="tasker_start_service">شروع خدمات</string>
|
||||
<string name="tasker_setting_confirm">تایید</string>
|
||||
@@ -363,6 +364,7 @@
|
||||
<string name="title_pref_fragment_length">طول بسته های فرگمنت (حداقل-حداکثر)</string>
|
||||
<string name="title_pref_fragment_interval">فاصله بین بسته های فرگمنت (حداقل-حداکثر)</string>
|
||||
<string name="title_pref_fragment_enabled">فعال کردن فرگمنت</string>
|
||||
<string name="title_pref_fragment_maxsplit">حداکثر تقسیم فرگمنت</string>
|
||||
|
||||
<string name="update_check_for_update">بررسی به روز رسانی</string>
|
||||
<string name="update_already_latest_version">در حال حاضر آخرین نسخه نصب شده است</string>
|
||||
@@ -388,7 +390,7 @@
|
||||
<string name="title_configuration_share">اشتراک گذاری پیکربندی</string>
|
||||
<string name="title_webdav_config_setting">تنظیمات WebDAV</string>
|
||||
<string name="title_webdav_config_setting_unknown">لطفا ابتدا WebDAV را پیکربندی کنید.</string>
|
||||
<string name="title_webdav_url">WebDAV server URL</string>
|
||||
<string name="title_webdav_url">لینک سرور WebDAV</string>
|
||||
<string name="title_webdav_user">یوزرنیم</string>
|
||||
<string name="title_webdav_pass">پسورد</string>
|
||||
<string name="title_webdav_remote_path">دایرکتوری راه دور (اختیاری)</string>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<string name="menu_item_import_config_qrcode">Импорт из QR-кода</string>
|
||||
<string name="menu_item_import_config_clipboard">Импорт из буфера обмена</string>
|
||||
<string name="menu_item_import_config_local">Импорт из файла</string>
|
||||
<string name="menu_item_import_config_policy_group">Добавить группу политик</string>
|
||||
<string name="menu_item_import_config_policy_group">Добавить политику группы</string>
|
||||
<string name="menu_item_import_config_proxy_chain">Добавить цепочку прокси</string>
|
||||
<string name="menu_item_import_config_manually_vmess">Добавить VMess</string>
|
||||
<string name="menu_item_import_config_manually_vless">Добавить VLESS</string>
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="server_lab_stream_security">TLS</string>
|
||||
<string name="server_lab_stream_fingerprint">Отпечаток</string>
|
||||
<string name="server_lab_stream_alpn">ALPN</string>
|
||||
<string name="server_lab_allow_insecure">Разрешать небезопасные</string>
|
||||
<string name="server_lab_allow_insecure">Разрешать небезопасные соединения</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">Адрес</string>
|
||||
<string name="server_lab_port3">Порт</string>
|
||||
@@ -117,7 +117,14 @@
|
||||
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">Необработанный JSON FinalMask, формат: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">ECHConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Проверять сертификат узла по имени</string>
|
||||
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
|
||||
<string name="server_lab_browser_dialer">Использовать переадресацию браузера</string>
|
||||
<string name="server_lab_browser_dialer_tip">Поддерживаются только исходящие соединения XHTTP (packet-up) и WS; настройки, связанные с TLS, могут быть проигнорированы или конфликтовать</string>
|
||||
<string name="toast_allow_insecure_deprecated">ВНИМАНИЕ: пропуск проверки сертификата («Разрешать небезопасные соединения») будет отключён в августе 2026 года. Пожалуйста, используйте отпечатки сертификатов как можно скорее. Функция будет недоступна после истечения срока действия.</string>
|
||||
<string name="pinned_ca256_action_fetch">Получить и заполнить отпечаток сертификата</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Отпечаток сертификата успешно получен</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Отпечаток сертификата не получен</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string>
|
||||
@@ -222,9 +229,6 @@
|
||||
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">Разрешать небезопасные соединения</string>
|
||||
<string name="summary_pref_allow_insecure">Для TLS по умолчанию разрешены небезопасные соединения</string>
|
||||
|
||||
<string name="title_pref_socks_port">Порт локального прокси</string>
|
||||
<string name="title_pref_enable_local_proxy">Использовать локальный прокси</string>
|
||||
<string name="summary_pref_enable_local_proxy">Если отключено, SOCKS-прокси недоступен, поэтому обновления подписки и подобные действия не смогут использовать прокси</string>
|
||||
@@ -285,10 +289,17 @@
|
||||
<string name="summary_pref_use_hev_tunnel">Если включено, TUN будет использовать hev-socks5-tunnel; иначе будет использован xray-core</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">Подробность журнала HevTun</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">Ожидание чтения/записи HevTun (секунд, по умолчанию TCP,UDP 300,60)</string>
|
||||
<string name="title_mode_settings">Настройки режима</string>
|
||||
<string name="title_root_mode_enabled">Использовать root-режим</string>
|
||||
<string name="summary_root_mode_enabled">Низкоуровневое прозрачное проксирование с помощью привилегий root. Этот режим обходит стандартный VPN системы Android, чтобы напрямую управлять всем сетевым трафиком. После включения первоначальный обычный режим, маршрутизация отдельных приложений и связанные с ними настройки VPN станут полностью недействительными.</string>
|
||||
<string name="toast_root_required">Для этой функции требуется доступ с правами root</string>
|
||||
<string name="title_root_lan_sharing">Совместное использование LAN/точки доступа (root)</string>
|
||||
<string name="summary_root_lan_sharing">Маршрутизация точек доступа Wi-Fi и устройств, подключённых к USB, через VPN</string>
|
||||
|
||||
<string name="title_logcat">Журнал</string>
|
||||
<string name="logcat_copy">Копировать</string>
|
||||
<string name="logcat_clear">Очистить</string>
|
||||
<string name="logcat_share">Поделиться журналом</string>
|
||||
<string name="title_service_restart">Перезапуск службы</string>
|
||||
<string name="title_del_all_config">Удалить профили</string>
|
||||
<string name="title_del_duplicate_config">Удалить дубликаты профилей</string>
|
||||
@@ -307,7 +318,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">Профиль должен быть уникальным</string>
|
||||
<string name="toast_invalid_update_interval">Неправильный интервал обновления. Минимум 15 минут.</string>
|
||||
<string name="title_sub_update">Обновить подписку</string>
|
||||
<string name="title_ping_all_server">Проверить профили</string>
|
||||
<string name="title_real_ping_all_server">Проверить задержку профилей</string>
|
||||
<string name="title_user_asset_setting">Файлы ресурсов</string>
|
||||
<string name="title_sort_by_test_results">Сортировать по результатам теста</string>
|
||||
@@ -343,7 +353,7 @@
|
||||
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string>
|
||||
<string name="routing_settings_domain">Домен</string>
|
||||
<string name="routing_settings_ip">IP</string>
|
||||
<string name="routing_settings_process">Процесс (название пакета; поддерживается только при использовании Xray TUN, включённой функции routeOnly и ОС Android 10+)</string>
|
||||
<string name="routing_settings_process">Процесс (название пакета; поддерживается только при использовании Xray TUN, включённой функции «Домен только для маршрутизации» и ОС Android 10+)</string>
|
||||
<string name="routing_settings_port">Порт</string>
|
||||
<string name="routing_settings_protocol">Протокол</string>
|
||||
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
||||
@@ -369,6 +379,7 @@
|
||||
<string name="title_pref_fragment_length">Длина фрагмента (от — до)</string>
|
||||
<string name="title_pref_fragment_interval">Интервал фрагментов (от — до)</string>
|
||||
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string>
|
||||
<string name="title_pref_fragment_maxsplit">Максимальное число фрагментов</string>
|
||||
|
||||
<string name="update_check_for_update">Проверить обновление</string>
|
||||
<string name="update_already_latest_version">Установлена последняя версия</string>
|
||||
@@ -377,7 +388,7 @@
|
||||
<string name="update_check_pre_release">Искать предварительный выпуск</string>
|
||||
<string name="update_checking_for_update">Проверка обновления…</string>
|
||||
|
||||
<string name="title_policy_group_type">Тип группы политик</string>
|
||||
<string name="title_policy_group_type">Тип политики группы</string>
|
||||
<string name="title_policy_group_subscription_id">Из группы подписки</string>
|
||||
<string name="title_policy_group_subscription_filter">Название фильтра</string>
|
||||
|
||||
@@ -424,6 +435,12 @@
|
||||
<item>Прокси</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="browser_dialer_mode">
|
||||
<item>Отключено</item>
|
||||
<item>OkHttp</item>
|
||||
<item>WebView</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="ui_mode_night">
|
||||
<item>Как в системе</item>
|
||||
<item>Светлая</item>
|
||||
|
||||
@@ -117,7 +117,11 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">Không thể sao chép tệp tin, hãy dùng trình quản lý tệp!</string>
|
||||
@@ -220,9 +224,6 @@
|
||||
<string name="summary_pref_proxy_sharing_enabled">Các thiết bị khác trong cùng mạng LAN có thể kết nối đến SOCKS / HTTP proxy trên thiết bị của bạn. \nChỉ bật tính năng này trong các mạng đáng tin cậy để tránh kết nối trái phép.</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">Đang bật cho phép kết nối từ mạng LAN</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">Bỏ qua xác minh chứng chỉ</string>
|
||||
<string name="summary_pref_allow_insecure">Khi nhập những cấu hình có bảo mật TLS, mặc định sẽ không xác minh chứng chỉ.</string>
|
||||
|
||||
<string name="title_pref_socks_port">Cổng Proxy Local</string>
|
||||
<string name="title_pref_enable_local_proxy">Bật proxy cục bộ</string>
|
||||
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
|
||||
@@ -286,6 +287,7 @@
|
||||
<string name="title_logcat">Logcat</string>
|
||||
<string name="logcat_copy">Sao chép</string>
|
||||
<string name="logcat_clear">Xoá</string>
|
||||
<string name="logcat_share">Chia sẻ nhật ký</string>
|
||||
<string name="title_service_restart">Khởi động lại dịch vụ lõi</string>
|
||||
<string name="title_del_all_config">Xoá tất cả cấu hình</string>
|
||||
<string name="title_del_duplicate_config">Xoá cấu hình trùng lặp</string>
|
||||
@@ -304,7 +306,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||
<string name="toast_invalid_update_interval">Khoảng thời gian cập nhật không hợp lệ. Giá trị tối thiểu là 15 phút.</string>
|
||||
<string name="title_sub_update">Cập nhật các gói đăng ký</string>
|
||||
<string name="title_ping_all_server">Ping tất cả máy chủ</string>
|
||||
<string name="title_real_ping_all_server">Kiểm tra HTTP tất cả máy chủ</string>
|
||||
<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>
|
||||
@@ -368,6 +369,7 @@
|
||||
<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="title_pref_fragment_maxsplit">Fragment Max Split</string>
|
||||
|
||||
<string name="update_check_for_update">Check for update</string>
|
||||
<string name="update_already_latest_version">Already on the latest version</string>
|
||||
|
||||
@@ -117,9 +117,14 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
|
||||
<string name="server_lab_browser_dialer">启用浏览器转发</string>
|
||||
<string name="server_lab_browser_dialer_tip">仅支持 xhttp (packet-up) 和 ws。与优选域名冲突,utls, alpn, ech 等 TLS 设置将被忽略</string>
|
||||
<string name="toast_allow_insecure_deprecated">警告:将在 2026.8.1 移除跳过证书验证 "allowInsecure" ,请尽快改用证书指纹或修改服务端。否则到期后会无法使用。 </string>
|
||||
<string name="pinned_ca256_action_fetch">获取并填充指纹证书</string>
|
||||
<string name="toast_fetch_cert_sha256_success">获取指纹证书完成</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">获取证书指纹失败</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">失败, 请使用文件管理器</string>
|
||||
@@ -219,10 +224,7 @@
|
||||
<string name="summary_pref_proxy_sharing_enabled">其他设备可以使用 socks/http 协议通过您的 IP 地址连接到代理, 仅在受信任的网络中启用以避免未经授权的连接</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">允许来自局域网的连接,请确保处于受信网络</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">跳过证书验证 (allowInsecure)</string>
|
||||
<string name="summary_pref_allow_insecure">传输层安全选 tls 时,默认跳过证书验证 (allowInsecure)</string>
|
||||
|
||||
<string name="title_pref_socks_port">本地代理端口</string>
|
||||
<string name="title_pref_socks_port">本地代理端口</string>
|
||||
<string name="title_pref_enable_local_proxy">启用本地代理</string>
|
||||
<string name="summary_pref_enable_local_proxy">不开启时没有 socks 代理,无法通过代理更新订阅等</string>
|
||||
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
|
||||
@@ -282,10 +284,18 @@
|
||||
<string name="summary_pref_use_hev_tunnel">选择启用后 TUN 将使用 hev-socks5-tunnel 否则使用 xray-core</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">HevTun 日志级别</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">HevTun 读写超时(秒) (tcp,udp 默认 300,60)</string>
|
||||
<string name="title_mode_settings">模式设置</string>
|
||||
<string name="title_root_mode_enabled">启用 Root 模式 (仅限 Root 用户)</string>
|
||||
<string name="summary_root_mode_enabled">通过 Root 权限启用底层透明代理。此模式将不再通过 Android 常规系统 VPN 建立连接,而是直接接管系统底层所有网络流量。开启后,原有的常规模式、应用分流及相关 VPN 设置将直接失效。</string>
|
||||
<string name="toast_root_required">此功能需要 Root 权限</string>
|
||||
<string name="title_root_lan_sharing">局域网 / 热点网络共享 (仅限 Root 用户)</string>
|
||||
<string name="summary_root_lan_sharing">允许通过 Wi-Fi 热点和 USB 共享网络连接的设备走 VPN 代理流量</string>
|
||||
|
||||
|
||||
<string name="title_logcat">Logcat</string>
|
||||
<string name="logcat_copy">复制</string>
|
||||
<string name="logcat_clear">清除</string>
|
||||
<string name="logcat_share">分享日志</string>
|
||||
<string name="title_service_restart">服务重启</string>
|
||||
<string name="title_del_all_config">删除配置</string>
|
||||
<string name="title_del_duplicate_config">删除重复配置</string>
|
||||
@@ -304,7 +314,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">请确保配置别名存在并唯一</string>
|
||||
<string name="toast_invalid_update_interval">更新间隔无效,最小值为 15 分钟。</string>
|
||||
<string name="title_sub_update">更新订阅</string>
|
||||
<string name="title_ping_all_server">测试配置 Tcping</string>
|
||||
<string name="title_real_ping_all_server">测试配置真连接</string>
|
||||
<string name="title_user_asset_setting">资源文件</string>
|
||||
<string name="title_sort_by_test_results">按测试结果排序</string>
|
||||
@@ -369,6 +378,7 @@
|
||||
<string name="title_pref_fragment_length">分片包长(最小 - 最大)</string>
|
||||
<string name="title_pref_fragment_interval">分片间隔(最小 - 最大)</string>
|
||||
<string name="title_pref_fragment_enabled">启用分片(Fragment)</string>
|
||||
<string name="title_pref_fragment_maxsplit">分片最大分割数</string>
|
||||
|
||||
<string name="update_check_for_update">检查更新</string>
|
||||
<string name="update_already_latest_version">目前已是最新版本</string>
|
||||
|
||||
@@ -117,7 +117,12 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string>
|
||||
<string name="toast_allow_insecure_deprecated">警告:將在 2026.8.1 停用跳過憑證驗證 "allowInsecure" ,請盡快改用憑證指紋。到期後無法使用。 </string>
|
||||
<string name="pinned_ca256_action_fetch">獲取並填充指紋證書</string>
|
||||
<string name="toast_fetch_cert_sha256_success">獲取指紋證書完成</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">獲取證書指紋失敗</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">失敗,請使用檔案總管</string>
|
||||
@@ -218,9 +223,6 @@
|
||||
<string name="summary_pref_proxy_sharing_enabled">其他裝置可以使用 socks/http 協定透過您的 IP 位址連線到 Proxy,僅在受信任的網路中啟用以避免未經授權的連線</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">允許來自區域網路的連線,請確保處於受信網路</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">跳過憑證驗證 (allowInsecure)</string>
|
||||
<string name="summary_pref_allow_insecure">傳輸層安全選 tls 時,預設跳過憑證驗證 (allowInsecure)</string>
|
||||
|
||||
<string name="title_pref_socks_port">本地代理埠</string>
|
||||
<string name="title_pref_enable_local_proxy">啟用本地代理</string>
|
||||
<string name="summary_pref_enable_local_proxy">未開啟時沒有 socks 代理,無法透過代理更新訂閱等</string>
|
||||
@@ -281,10 +283,18 @@
|
||||
<string name="summary_pref_use_hev_tunnel">選擇啟用後 TUN 將使用 hev-socks5-tunnel 否則使用 xray-core</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">HevTun 日誌級別</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">HevTun 讀寫逾時(秒) (tcp,udp 預設 300,60)</string>
|
||||
<string name="title_mode_settings">模式設定</string>
|
||||
<string name="title_root_mode_enabled">啟用 Root 模式 (僅限 Root 使用者)</string>
|
||||
<string name="summary_root_mode_enabled">透過 Root 權限啟用底層透明代理。此模式將不再透過 Android 常規系統 VPN 建立連線,而是直接接管系統底層所有網路流量。開啟後,原有的常规模式、應用分流及相關 VPN 設定將直接失效。</string>
|
||||
<string name="toast_root_required">此功能需要 Root 權限</string>
|
||||
<string name="title_root_lan_sharing">區域網路 / 熱點網路共享 (僅限 Root 使用者)</string>
|
||||
<string name="summary_root_lan_sharing">允許透過 Wi-Fi 熱點和 USB 共享網路連線的裝置走 VPN 代理流量</string>
|
||||
|
||||
|
||||
<string name="title_logcat">Logcat</string>
|
||||
<string name="logcat_copy">複製</string>
|
||||
<string name="logcat_clear">清除</string>
|
||||
<string name="logcat_share">分享日誌</string>
|
||||
<string name="title_service_restart">重啟服務</string>
|
||||
<string name="title_del_all_config">刪除設定</string>
|
||||
<string name="title_del_duplicate_config">刪除重複設定</string>
|
||||
@@ -303,7 +313,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">请确保設定檔别名存在并唯一</string>
|
||||
<string name="toast_invalid_update_interval">更新間隔無效,最小值為 15 分鐘。</string>
|
||||
<string name="title_sub_update">更新訂閱</string>
|
||||
<string name="title_ping_all_server">偵測設定 Tcping</string>
|
||||
<string name="title_real_ping_all_server">偵測設定真延遲</string>
|
||||
<string name="title_user_asset_setting">資源檔案</string>
|
||||
<string name="title_sort_by_test_results">依偵測結果排序</string>
|
||||
@@ -367,6 +376,7 @@
|
||||
<string name="title_pref_fragment_length">分片包長(最小-最大)</string>
|
||||
<string name="title_pref_fragment_interval">分片間隔(最小-最大)</string>
|
||||
<string name="title_pref_fragment_enabled">啟用分片(Fragment)</string>
|
||||
<string name="title_pref_fragment_maxsplit">分片最大分割數</string>
|
||||
|
||||
<string name="update_check_for_update">檢查更新</string>
|
||||
<string name="update_already_latest_version">当前已是最新版本</string>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
|
||||
<string-array name="fragment_packets" translatable="false">
|
||||
<item>tlshello</item>
|
||||
<item>1-1</item>
|
||||
<item>1-2</item>
|
||||
<item>1-3</item>
|
||||
<item>1-5</item>
|
||||
|
||||
@@ -118,9 +118,14 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="server_lab_browser_dialer">Enable Browser Dialer</string>
|
||||
<string name="server_lab_browser_dialer_tip">Only supports xhttp (packet-up) and ws outbound; TLS-related settings may be ignored or conflict</string>
|
||||
<string name="toast_allow_insecure_deprecated">WARNING: Skipping certificate verification "allowInsecure" will be disabled in August 2026. Please use certificate fingerprints as soon as possible. This feature will not be available after its expiration.</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">File copy failed, please use File Manager</string>
|
||||
@@ -226,9 +231,6 @@
|
||||
<string name="summary_pref_proxy_sharing_enabled">Other devices can connect to proxy by your IP address through local proxy. Only enable in trusted networks to avoid unauthorized connections</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">Allow connections from the LAN. Make sure you are in a trusted network</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">allowInsecure</string>
|
||||
<string name="summary_pref_allow_insecure">When TLS is selected, allow insecure connections by default</string>
|
||||
|
||||
<string name="title_pref_socks_port">Local proxy port</string>
|
||||
<string name="title_pref_enable_local_proxy">Enable local proxy</string>
|
||||
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
|
||||
@@ -289,10 +291,17 @@
|
||||
<string name="summary_pref_use_hev_tunnel">When enabled, TUN will use hev-socks5-tunnel; otherwise, it will use xray-core.</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">Hev Tun Log Level</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">Hev Tun read/write timeout (seconds) (tcp,udp default 300,60)</string>
|
||||
<string name="title_mode_settings">Mode Settings</string>
|
||||
<string name="title_root_mode_enabled">Root mode enabled (Root Users)</string>
|
||||
<string name="summary_root_mode_enabled">Enable low-level transparent proxying via root privileges. This mode bypasses the standard Android system VPN to directly take over all underlying network traffic. Once enabled, the original regular mode, per-app routing, and related VPN settings will become completely invalid.</string>
|
||||
<string name="toast_root_required">Root access is required for this feature</string>
|
||||
<string name="title_root_lan_sharing">LAN / tethering sharing (Root Users)</string>
|
||||
<string name="summary_root_lan_sharing">Route Wi-Fi hotspot and USB-tethered devices through the VPN</string>
|
||||
|
||||
<string name="title_logcat">Logcat</string>
|
||||
<string name="logcat_copy">Copy</string>
|
||||
<string name="logcat_clear">Clear</string>
|
||||
<string name="logcat_share">Share log</string>
|
||||
<string name="title_service_restart">Service restart</string>
|
||||
<string name="title_del_all_config">Delete config</string>
|
||||
<string name="title_del_duplicate_config">Delete duplicate config</string>
|
||||
@@ -311,7 +320,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">The config remarks exist and are unique</string>
|
||||
<string name="toast_invalid_update_interval">Invalid update interval. Minimum is 15 minutes.</string>
|
||||
<string name="title_sub_update">Update subscription</string>
|
||||
<string name="title_ping_all_server">Tcping config</string>
|
||||
<string name="title_real_ping_all_server">Real delay config</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">Sorting by test results</string>
|
||||
@@ -374,6 +382,7 @@
|
||||
<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="title_pref_fragment_maxsplit">Fragment Max Split</string>
|
||||
|
||||
<string name="update_check_for_update">Check for update</string>
|
||||
<string name="update_already_latest_version">Already on the latest version</string>
|
||||
|
||||
@@ -144,11 +144,6 @@
|
||||
android:summary="@string/summary_pref_route_only_enabled"
|
||||
android:title="@string/title_pref_route_only_enabled" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="pref_allow_insecure"
|
||||
android:title="@string/title_pref_allow_insecure" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="true"
|
||||
android:key="pref_enable_local_proxy"
|
||||
@@ -269,6 +264,11 @@
|
||||
android:summary="10-20"
|
||||
android:title="@string/title_pref_fragment_interval" />
|
||||
|
||||
<EditTextPreference
|
||||
android:key="pref_fragment_maxsplit"
|
||||
android:summary="10"
|
||||
android:title="@string/title_pref_fragment_maxsplit" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="tlshello"
|
||||
android:entries="@array/fragment_packets"
|
||||
@@ -314,6 +314,10 @@
|
||||
android:summary="@string/summary_pref_ip_api_url"
|
||||
android:title="@string/title_pref_ip_api_url" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/title_mode_settings">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="VPN"
|
||||
android:entries="@array/mode_entries"
|
||||
@@ -322,6 +326,19 @@
|
||||
android:summary="%s"
|
||||
android:title="@string/title_mode" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="pref_root_mode_enabled"
|
||||
android:summary="@string/summary_root_mode_enabled"
|
||||
android:title="@string/title_root_mode_enabled" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="pref_root_lan_sharing"
|
||||
android:summary="@string/summary_root_lan_sharing"
|
||||
android:title="@string/title_root_lan_sharing" />
|
||||
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import android.util.Base64
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
agp = "9.2.1"
|
||||
desugarJdkLibs = "2.1.5"
|
||||
gradleLicensePlugin = "0.9.8"
|
||||
kotlin = "2.3.10"
|
||||
kotlin = "2.4.0"
|
||||
coreKtx = "1.17.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Thu Nov 14 12:42:51 BDT 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user