Compare commits

..
2 Commits
Author SHA1 Message Date
2dust c0e968ac34 up ver 2019-07-01 13:22:48 +08:00
2dust 29d3fb710b ii 2019-05-15 10:17:59 +08:00
515 changed files with 13767 additions and 37687 deletions

No files matched your search

-34
View File
@@ -1,34 +0,0 @@
---
name: v2rayNG程序问题
about: 创建一个报告来帮助我们改进
---
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
### 预期行为
描述你认为应该发生什么
### 实际行为
描述实际发生了什么
### 复现方法
1.
2.
3.
### 日志信息
<details>
通过`adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main`获取日志。请自行删减日志中可能出现的敏感信息。
如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
```
在这里粘贴日志
```
</details>
### 环境信息
### 额外信息(可选)
-5
View File
@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: V2Ray程序问题
url: https://github.com/v2fly/v2ray-core/
about: 如果您有V2Ray而非v2rayNG的问题,请至这个链接讨论。
-6
View File
@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
-162
View File
@@ -1,162 +0,0 @@
name: Build APK
on:
workflow_dispatch:
inputs:
release_tag:
required: false
type: string
push:
branches:
- master
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: 'recursive'
fetch-depth: '0'
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
log-accepted-android-sdk-licenses: false
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;29.0.14206865"
echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.14206865" >> $GITHUB_ENV
sed -i '10i\
\
ndkVersion = "29.0.14206865"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
- name: Restore cached libhevtun
id: cache-libhevtun-restore
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') }}
- name: Build libhevtun
if: steps.cache-libhevtun-restore.outputs.cache-hit != 'true'
run: |
bash compile-hevtun.sh
- name: Save libhevtun
if: steps.cache-libhevtun-restore.outputs.cache-hit != 'true'
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') }}
- name: Copy libhevtun
run: |
cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
- name: Fetch AndroidLibXrayLite tag
run: |
pushd AndroidLibXrayLite
CURRENT_TAG=$(git describe --tags --abbrev=0)
echo "Current tag in this repo: $CURRENT_TAG"
echo "CURRENT_TAG=$CURRENT_TAG" >> $GITHUB_ENV
popd
- name: Download libv2ray
uses: robinraju/release-downloader@v1.13
with:
repository: '2dust/AndroidLibXrayLite'
tag: ${{ env.CURRENT_TAG }}
fileName: 'libv2ray.aar'
out-file-path: V2rayNG/app/libs/
- name: Setup Java
uses: actions/setup-java@v5.7.0
with:
distribution: 'temurin'
java-version: '21'
- name: Decode Keystore
uses: timheuer/base64-to-file@v2.0.0
id: android_keystore
with:
fileName: "android_keystore.jks"
encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }}
- name: Build APK
run: |
cd ${{ github.workspace }}/V2rayNG
echo "sdk.dir=${ANDROID_HOME}" > local.properties
chmod 755 gradlew
./gradlew licenseFdroidReleaseReport
./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }}
- name: Upload arm64-v8a APK
uses: actions/upload-artifact@v7
if: ${{ success() }}
with:
name: arm64-v8a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
- name: Upload armeabi-v7a APK
uses: actions/upload-artifact@v7
if: ${{ success() }}
with:
name: armeabi-v7a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
- name: Upload x86 APK
uses: actions/upload-artifact@v7
if: ${{ success() }}
with:
name: x86-apk
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*x86*.apk
- name: Upload universal APK
uses: actions/upload-artifact@v7
if: ${{ success() }}
with:
name: universal-apk
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*universal*.apk
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
-16
View File
@@ -1,16 +0,0 @@
name: Validate Fastlane metadata
on:
workflow_dispatch:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Validate Fastlane Supply Metadata
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0
+10 -64
View File
@@ -1,66 +1,12 @@
# Ignore data and key store files
V2rayNG/app/src/main/res/layout/activity_inapp_buy.xml
V2rayNG/app/src/main/assets/geoip.dat
V2rayNG/app/src/main/assets/geosite.dat
V2rayNG/app/src/main/java/com/v2ray/ang/InappBuyActivity.java
V2rayNG/gradle/wrapper/gradle-wrapper.properties
V2rayNG/gradle/wrapper/gradle-wrapper.properties
*.aar
*.dat
*.jks
# Ignore output JSON file
V2rayNG/app/release/output.json
# Ignore IDE and build system directories
.idea/
.gradle/
*.iml
# Ignore local properties and DS_Store files
/local.properties
.DS_Store
# Ignore build directories and captures
/build
/captures
V2rayNG/app/build
V2rayNG/build
V2rayNG/local.properties
# Ignore APK and AAR files
*.apk
*.aar
# Ignore signing properties
signing.properties
# Ignore shared object files
*.so
# Ignore Google services JSON
V2rayNG/app/google-services.json
# Additional common Android/Java ignores
*.log
*.tmp
*.bak
*.swp
*.orig
*.class
*.jar
*.war
*.ear
# Ignore executable files
*.exe
*.dll
*.obj
*.o
*.pyc
*.pyo
# Ignore files from other IDEs
.vscode/
.classpath
.project
.settings/
*.sublime-workspace
*.sublime-project
# Ignore OS-specific files
Thumbs.db
.DS_Store
V2rayNG/gradle/wrapper/gradle-wrapper.properties
V2rayNG/gradle/wrapper/gradle-wrapper.properties
V2rayNG/app/release/output.json
-6
View File
@@ -1,6 +0,0 @@
[submodule "AndroidLibXrayLite"]
path = AndroidLibXrayLite
url = https://github.com/2dust/AndroidLibXrayLite
[submodule "hev-socks5-tunnel"]
path = hev-socks5-tunnel
url = https://github.com/heiher/hev-socks5-tunnel
+15 -26
View File
@@ -1,40 +1,29 @@
**v2rayNG 隐私权政策**
v2rayNG 隐私条款
本政策自2023年11月17日起施行
最后更新 2017-11-22
2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
1. 信息收集
**信息收集**
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
2. 信息共享
**信息共享**
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
3. 信息存留
**信息存留**
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
4. 信息泄露
**信息泄露**
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
**儿童隐私**
这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。
**条款修改**
我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。**
**联系我们**
如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。
5. 条款修改
我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
+3 -87
View File
@@ -1,90 +1,6 @@
# v2rayNG
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
</a>
[![API](https://img.shields.io/badge/API-24%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.4.0-blue.svg)](https://kotlinlang.org)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master)
[![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng)
[![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases)
[![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/v2rayn)
---
## 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.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 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 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)
### Geoip 与 Geosite
- 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)
更多内容请见我们的 [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)
+9
View File
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
*.apk
signing.properties
+2
View File
@@ -0,0 +1,2 @@
/build
/google-services.json
+129
View File
@@ -0,0 +1,129 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
buildToolsVersion '27.0.3'
compileOptions {
targetCompatibility = "8"
sourceCompatibility = "8"
}
defaultConfig {
applicationId "com.v2ray.ang"
minSdkVersion 17
targetSdkVersion Integer.parseInt("$targetSdkVer")
multiDexEnabled true
versionCode 210
versionName "1.0.1"
}
signingConfigs {
release {
storeFile file("../key.jks")
keyAlias 'ang'
keyPassword '123456'
storePassword '123456'
}
debug {
storeFile file("../key.jks")
keyAlias 'ang'
keyPassword '123456'
storePassword '123456'
}
}
buildTypes {
release {
minifyEnabled false
zipAlignEnabled false
shrinkResources false
signingConfig signingConfigs.release
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
zipAlignEnabled false
shrinkResources false
signingConfig signingConfigs.release
}
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
splits {
abi {
enable true
reset()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
universalApk true //generate an additional APK that contains all the ABIs
}
}
// map for the version code
project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
android.applicationVariants.all { variant ->
// assign different version code for each output
variant.outputs.each { output ->
output.versionCodeOverride =
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
1000000 + android.defaultConfig.versionCode
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
implementation project(':dpreference')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
// Android support library
implementation "com.android.support:support-v4:$supportLibVersion"
implementation "com.android.support:appcompat-v7:$supportLibVersion"
implementation "com.android.support:design:$supportLibVersion"
implementation "com.android.support:cardview-v7:$supportLibVersion"
implementation "com.android.support:preference-v7:$supportLibVersion"
implementation "com.android.support:recyclerview-v7:$supportLibVersion"
// DSL
implementation "org.jetbrains.anko:anko-sdk15:$ankoVersion"
implementation "org.jetbrains.anko:anko-support-v4:$ankoVersion"
implementation "org.jetbrains.anko:anko-appcompat-v7:$ankoVersion"
implementation "org.jetbrains.anko:anko-design:$ankoVersion"
implementation 'com.google.code.gson:gson:2.8.2'
implementation 'io.reactivex:rxjava:1.3.4'
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
implementation 'com.dinuscxj:recycleritemdecoration:1.0.0'
implementation 'io.reactivex:rxkotlin:0.60.0'
implementation 'me.dm7.barcodescanner:core:1.9.8'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
implementation 'com.beust:klaxon:3.0.1'
implementation 'com.android.support:multidex:1.0.3'
implementation(name: 'libv2ray', ext: 'aar')
//implementation(name: 'tun2socks', ext: 'aar')
}
buildscript {
repositories {
google()
jcenter()
maven { url 'https://maven.google.com' }
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion"
}
}
repositories {
flatDir {
dirs 'libs'
}
}
-210
View File
@@ -1,210 +0,0 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.jaredsburrows.license")
}
android {
namespace = "com.v2ray.ang"
compileSdk = 37
defaultConfig {
applicationId = "com.v2ray.ang"
minSdk = 24
targetSdk = 37
versionCode = 744
versionName = "2.3.4"
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
splits {
abi {
isEnable = true
reset()
if (!abiFilterList.isNullOrEmpty()) {
include(*abiFilterList.toTypedArray())
} else {
include(
"arm64-v8a",
"armeabi-v7a",
"x86_64",
"x86"
)
}
isUniversalApk = abiFilterList.isNullOrEmpty()
}
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
flavorDimensions.add("distribution")
productFlavors {
create("fdroid") {
dimension = "distribution"
applicationIdSuffix = ".fdroid"
buildConfigField("String", "DISTRIBUTION", "\"F-Droid\"")
}
create("playstore") {
dimension = "distribution"
buildConfigField("String", "DISTRIBUTION", "\"Play Store\"")
}
}
sourceSets {
getByName("main") {
jniLibs.srcDirs("libs")
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
applicationVariants.all {
val variant = this
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
if (isFdroid) {
val versionCodes =
mapOf(
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
)
variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
val abi = output.getFilter("ABI") ?: "universal"
output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk"
if (versionCodes.containsKey(abi)) {
output.versionCodeOverride =
(100 * variant.versionCode + versionCodes[abi]!!).plus(5000000)
} else {
return@forEach
}
}
} else {
val versionCodes =
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
val abi = if (output.getFilter("ABI") != null)
output.getFilter("ABI")
else
"universal"
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
if (versionCodes.containsKey(abi)) {
output.versionCodeOverride =
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
} else {
return@forEach
}
}
}
}
buildFeatures {
buildConfig = true
compose = true
}
androidResources {
generateLocaleConfig = true
localeFilters += listOf(
"en",
"zh-rCN",
"zh-rTW",
"vi",
"ru",
"fa",
"ar",
"bn",
"bqi-rIR"
)
}
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
dependencies {
// Core Libraries
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
// AndroidX Core Libraries
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
// Compose Libraries
implementation(platform(libs.androidx.compose.bom))
androidTestImplementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.lifecycle.runtime.compose)
implementation(libs.coil.compose)
debugImplementation(libs.androidx.compose.ui.tooling)
// Data and Storage Libraries
implementation(libs.mmkv.static)
implementation(libs.gson)
implementation(libs.okhttp)
// Reactive and Utility Libraries
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
// QR Code: CameraX + ZXing
implementation(libs.camerax.core)
implementation(libs.camerax.camera2)
implementation(libs.camerax.lifecycle)
implementation(libs.camerax.compose)
implementation(libs.core) // zxing core
// AndroidX Lifecycle and Architecture Components
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.runtime.ktx)
// Background Task Libraries
implementation(libs.work.runtime.ktx)
implementation(libs.work.multiprocess)
// Reorderable list
implementation(libs.reorderable)
// Testing Libraries
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
testImplementation(libs.org.mockito.mockito.inline)
testImplementation(libs.mockito.kotlin)
coreLibraryDesugaring(libs.desugar.jdk.libs)
}
+48 -8
View File
@@ -1,10 +1,14 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# By default, the flags in this file are appended to flags specified
# in G:\android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
@@ -12,10 +16,46 @@
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
-dontwarn org.apache.commons.**
-keep class org.apache.commons.** { *;}
# Disable debug info output
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String,int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
static void throwUninitializedPropertyAccessException(java.lang.String);
}
-dontwarn org.jetbrains.anko.internals.**
-keep class org.jetbrains.anko.internals.** { *;}
-dontwarn rx.internal.util.unsafe.**
-keep class rx.internal.util.unsafe.** { *;}
-dontwarn app.dinus.**
-keep class app.dinus.** { *;}
-keepclassmembers class ** {
@com.hwangjr.rxbus.annotation.Subscribe public *;
@com.hwangjr.rxbus.annotation.Produce public *;
}
-keep class libv2ray.** { *;}
@@ -0,0 +1,13 @@
package com.v2ray.ang;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="app_name" type="string">v2rayNG (DEV)</item>
</resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">v2rayNG (F-Droid)</string>
</resources>
@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:enabled="true"
android:icon="@drawable/ic_qu_switch_24dp"
android:shortcutDisabledMessage="@string/app_widget_name"
android:shortcutId="shortcuts_switch"
android:shortcutLongLabel="@string/app_widget_name"
android:shortcutShortLabel="@string/app_widget_name">
<!--suppress AndroidDomInspection -->
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.v2ray.ang.ui.shortcut.ScSwitchActivity"
android:targetPackage="com.v2ray.ang.fdroid" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/ic_qu_scan_24dp"
android:shortcutDisabledMessage="@string/menu_item_import_config_qrcode"
android:shortcutId="shortcuts_scan"
android:shortcutLongLabel="@string/menu_item_import_config_qrcode"
android:shortcutShortLabel="@string/menu_item_import_config_qrcode">
<!--suppress AndroidDomInspection -->
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.v2ray.ang.ui.shortcut.ScScannerActivity"
android:targetPackage="com.v2ray.ang.fdroid" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/ic_qu_start_24dp"
android:shortcutDisabledMessage="@string/toast_services_start"
android:shortcutId="shortcuts_start"
android:shortcutLongLabel="@string/toast_services_start"
android:shortcutShortLabel="@string/toast_services_start">
<!--suppress AndroidDomInspection -->
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.v2ray.ang.ui.shortcut.ScStartActivity"
android:targetPackage="com.v2ray.ang.fdroid" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/ic_qu_stop_24dp"
android:shortcutDisabledMessage="@string/toast_services_stop"
android:shortcutId="shortcuts_stop"
android:shortcutLongLabel="@string/toast_services_stop"
android:shortcutShortLabel="@string/toast_services_stop">
<!--suppress AndroidDomInspection -->
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.v2ray.ang.ui.shortcut.ScStopActivity"
android:targetPackage="com.v2ray.ang.fdroid" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>
+54 -293
View File
@@ -1,75 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MissingLeanbackLauncher">
package="com.v2ray.ang">
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="true" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
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.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
<uses-permission android:name="android.permission.CAMERA" />
<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.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="com.android.vending.BILLING" />
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
<application
android:name=".AngApplication"
android:allowBackup="true"
android:banner="@mipmap/ic_banner"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/AppThemeDayNight.NoActionBar"
android:usesCleartextTraffic="true">
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
android:theme="@style/AppTheme">
<activity
android:name=".ui.main.MainActivity"
android:exported="true"
android:name=".ui.MainActivity"
android:theme="@style/AppTheme.NoActionBar"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
@@ -80,285 +43,83 @@
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".ui.server.ServerVmessActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
android:name=".ui.ServerActivity"
android:windowSoftInputMode="stateUnchanged" />
<activity
android:name=".ui.server.ServerVlessActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
android:name=".ui.Server2Activity"
android:windowSoftInputMode="stateUnchanged" />
<activity
android:name=".ui.server.ServerShadowsocksActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
android:name=".ui.Server3Activity"
android:windowSoftInputMode="stateUnchanged" />
<activity
android:name=".ui.server.ServerSocksActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
android:name=".ui.Server4Activity"
android:windowSoftInputMode="stateUnchanged" />
<activity android:name=".ui.SettingsActivity" />
<activity android:name=".ui.PerAppProxyActivity" />
<activity android:name=".ui.ScannerActivity" />
<activity android:name=".InappBuyActivity" />
<activity android:name=".ui.LogcatActivity" />
<activity
android:name=".ui.server.ServerHttpActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
<activity
android:name=".ui.server.ServerTrojanActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
<activity
android:name=".ui.server.ServerWireguardActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
<activity
android:name=".ui.server.ServerHysteria2Activity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
<activity
android:name=".ui.server.ServerCustomConfigActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
<activity
android:name=".ui.server.ServerGroupActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
<activity
android:name=".ui.server.ServerProxyChainActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged|adjustResize" />
<activity
android:name=".ui.settings.SettingsActivity"
android:exported="false" />
<activity
android:name=".ui.perappproxy.PerAppProxyActivity"
android:exported="false" />
<activity
android:name=".ui.apppicker.AppPickerActivity"
android:exported="false" />
<activity
android:name=".ui.ScannerActivity"
android:exported="false" />
<activity
android:name=".ui.logcat.LogcatActivity"
android:exported="false" />
<activity
android:name=".ui.routing.RoutingSettingActivity"
android:exported="false" />
<activity
android:name=".ui.routing.RoutingEditActivity"
android:windowSoftInputMode="stateUnchanged|adjustResize"
android:exported="false" />
<activity
android:name=".ui.subscription.SubSettingActivity"
android:exported="false" />
<activity
android:name=".ui.userasset.UserAssetActivity"
android:exported="false" />
<activity
android:name=".ui.userasset.UserAssetUrlActivity"
android:exported="false" />
android:name=".ui.RoutingSettingsActivity"
android:windowSoftInputMode="stateUnchanged" />
<activity android:name=".ui.SubSettingActivity" />
<activity
android:name=".ui.subscription.SubEditActivity"
android:windowSoftInputMode="stateUnchanged|adjustResize"
android:exported="false" />
<activity
android:name=".ui.shortcut.ScScannerActivity"
android:exported="false" />
<activity
android:name=".ui.shortcut.ScSwitchActivity"
android:excludeFromRecents="true"
android:exported="false"
android:process=":RunSoLibV2RayDaemon"
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
<activity
android:name=".ui.shortcut.ScStartActivity"
android:excludeFromRecents="true"
android:exported="false"
android:process=":RunSoLibV2RayDaemon"
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
<activity
android:name=".ui.shortcut.ScStopActivity"
android:excludeFromRecents="true"
android:exported="false"
android:process=":RunSoLibV2RayDaemon"
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
<activity
android:name=".ui.UrlSchemeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="v2rayng" />
<data android:host="install-config" />
<data android:host="install-sub" />
</intent-filter>
</activity>
<activity
android:name=".ui.checkupdate.CheckUpdateActivity"
android:exported="false" />
<activity
android:name=".ui.backup.BackupActivity"
android:exported="false" />
<activity
android:name=".ui.AboutActivity"
android:exported="false" />
<activity android:name=".ui.SubEditActivity" />
<activity android:name=".ui.ScScannerActivity" />
<activity android:name=".ui.ScSwitchActivity" />
<service
android:name=".service.CoreVpnService"
android:name=".service.V2RayVpnService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse"
android:label="@string/app_name"
android:permission="android.permission.BIND_VPN_SERVICE"
android:process=":RunSoLibV2RayDaemon"
tools:ignore="VpnServicePolicy">
android:process=":RunSoLibV2RayDaemon">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true" />
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn" />
</service>
<service
android:name=".service.CoreProxyOnlyService"
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>
<!--<receiver android:name=".receiver.WidgetProvider">-->
<!--<meta-data-->
<!--android:name="android.appwidget.provider"-->
<!--android:resource="@xml/app_widget_provider" />-->
<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"
android:foregroundServiceType="specialUse"
android:process=":RunSoLibV2RayDaemon">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="test" />
</service>
<service
android:name=".service.SubscriptionUpdateService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:process=":RunSoLibV2RayDaemon">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="test" />
</service>
<receiver
android:name=".receiver.WidgetProvider"
android:exported="true"
android:process=":RunSoLibV2RayDaemon">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/app_widget_provider" />
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="${applicationId}.action.widget.click" />
<action android:name="${applicationId}.action.activity" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BootReceiver"
android:exported="true"
android:label="BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<!--<intent-filter>-->
<!--<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
<!--<action android:name="com.v2ray.ang.action.widget.click" />-->
<!--</intent-filter>-->
<!--</receiver>-->
<service
android:name=".service.QSTileService"
android:exported="true"
android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name"
android:icon="@drawable/ic_v"
android:label="@string/app_tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:process=":RunSoLibV2RayDaemon">
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="tile" />
</service>
<!-- =====================Tasker===================== -->
<activity
android:name=".ui.shortcut.TaskerActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher">
android:name=".ui.TaskerActivity"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
<receiver
android:name=".receiver.TaskerReceiver"
android:exported="true"
android:process=":RunSoLibV2RayDaemon"
tools:ignore="ExportedReceiver">
<receiver android:name=".receiver.TaskerReceiver">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter>
</receiver>
<!-- =====================Tasker===================== -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<service
android:name="androidx.work.multiprocess.RemoteWorkManagerService"
android:exported="false"
android:process=":bg" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.cache"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/cache_paths" />
</provider>
</application>
</manifest>
</manifest>
@@ -0,0 +1,144 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import android.os.Bundle;
/**
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
* This service provides the following features:
* 1. Provides a new API to get details of in-app items published for the app including
* price, type, title and description.
* 2. The purchase flow is synchronous and purchase information is available immediately
* after it completes.
* 3. Purchase information of in-app purchases is maintained within the Google Play system
* till the purchase is consumed.
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
* in-app items are consumable and thereafter can be purchased again.
* 5. An API to get current purchases of the user immediately. This will not contain any
* consumed purchases.
*
* All calls will give a response code with the following possible values
* RESULT_OK = 0 - success
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
* RESULT_ERROR = 6 - Fatal error during the API action
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
*/
interface IInAppBillingService {
/**
* Checks support for the requested billing API version, package and in-app type.
* Minimum API version supported by this interface is 3.
* @param apiVersion the billing version which the app is using
* @param packageName the package name of the calling app
* @param type type of the in-app item being purchased "inapp" for one-time purchases
* and "subs" for subscription.
* @return RESULT_OK(0) on success, corresponding result code on failures
*/
int isBillingSupported(int apiVersion, String packageName, String type);
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
* with a list JSON strings containing the productId, price, title and description.
* This API can be called with a maximum of 20 SKUs.
* @param apiVersion billing API version that the Third-party is using
* @param packageName the package name of the calling app
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
* "title : "Example Title", "description" : "This is an example description" }'
*/
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
/**
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
* the type, a unique purchase token and an optional developer payload.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type the type of the in-app item ("inapp" for one-time purchases
* and "subs" for subscription).
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
* TODO: change this to app-specific keys.
*/
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
String developerPayload);
/**
* Returns the current SKUs owned by the user of the type and package name specified along with
* purchase information and a signature of the data to be validated.
* This will return all SKUs that have been purchased in V3 and managed items purchased using
* V1 and V2 that have not been consumed.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param type the type of the in-app items being requested
* ("inapp" for one-time purchases and "subs" for subscription).
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
/**
* Consume the last purchase of the given SKU. This will result in this item being removed
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return 0 if consumption succeeded. Appropriate error values for failures.
*/
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
}
@@ -1,141 +0,0 @@
[
{
"remarks": "绕过bittorrent",
"outboundTag": "direct",
"protocol": [
"bittorrent"
]
},
{
"remarks": "阻断udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "代理Google",
"outboundTag": "proxy",
"domain": [
"geosite:google"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "代理海外公共DNSIP",
"outboundTag": "proxy",
"ip": [
"1.1.1.1",
"1.0.0.1",
"2606:4700:4700::1111",
"2606:4700:4700::1001",
"1.1.1.2",
"1.0.0.2",
"2606:4700:4700::1112",
"2606:4700:4700::1002",
"1.1.1.3",
"1.0.0.3",
"2606:4700:4700::1113",
"2606:4700:4700::1003",
"8.8.8.8",
"8.8.4.4",
"2001:4860:4860::8888",
"2001:4860:4860::8844",
"94.140.14.14",
"94.140.15.15",
"2a10:50c0::ad1:ff",
"2a10:50c0::ad2:ff",
"94.140.14.15",
"94.140.15.16",
"2a10:50c0::bad1:ff",
"2a10:50c0::bad2:ff",
"94.140.14.140",
"94.140.14.141",
"2a10:50c0::1:ff",
"2a10:50c0::2:ff",
"208.67.222.222",
"208.67.220.220",
"2620:119:35::35",
"2620:119:53::53",
"208.67.222.123",
"208.67.220.123",
"2620:119:35::123",
"2620:119:53::123",
"9.9.9.9",
"149.112.112.112",
"2620:fe::9",
"2620:fe::fe",
"9.9.9.11",
"149.112.112.11",
"2620:fe::11",
"2620:fe::fe:11",
"9.9.9.10",
"149.112.112.10",
"2620:fe::10",
"2620:fe::fe:10",
"77.88.8.8",
"77.88.8.1",
"2a02:6b8::feed:0ff",
"2a02:6b8:0:1::feed:0ff",
"77.88.8.88",
"77.88.8.2",
"2a02:6b8::feed:bad",
"2a02:6b8:0:1::feed:bad",
"77.88.8.7",
"77.88.8.3",
"2a02:6b8::feed:a11",
"2a02:6b8:0:1::feed:a11"
]
},
{
"remarks": "代理海外公共DNS域名",
"outboundTag": "proxy",
"domain": [
"domain:cloudflare-dns.com",
"domain:one.one.one.one",
"domain:dns.google",
"domain:adguard-dns.com",
"domain:opendns.com",
"domain:umbrella.com",
"domain:quad9.net",
"domain:yandex.net"
]
},
{
"remarks": "代理IP",
"outboundTag": "proxy",
"ip": [
"geoip:facebook",
"geoip:fastly",
"geoip:google",
"geoip:netflix",
"geoip:telegram",
"geoip:twitter"
]
},
{
"remarks": "代理GFW",
"outboundTag": "proxy",
"domain": [
"geosite:gfw",
"geosite:greatfire"
]
},
{
"remarks": "最终直连",
"port": "0-65535",
"outboundTag": "direct"
}
]
@@ -1,27 +0,0 @@
[
{
"remarks": "阻断udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "最终代理",
"port": "0-65535",
"outboundTag": "proxy"
}
]
@@ -1,95 +0,0 @@
[
{
"remarks": "阻断udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "代理Google",
"outboundTag": "proxy",
"domain": [
"geosite:google"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "绕过中国公共DNSIP",
"outboundTag": "direct",
"ip": [
"223.5.5.5",
"223.6.6.6",
"2400:3200::1",
"2400:3200:baba::1",
"119.29.29.29",
"1.12.12.12",
"120.53.53.53",
"2402:4e00::",
"2402:4e00:1::",
"180.76.76.76",
"2400:da00::6666",
"114.114.114.114",
"114.114.115.115",
"114.114.114.119",
"114.114.115.119",
"114.114.114.110",
"114.114.115.110",
"180.184.1.1",
"180.184.2.2",
"101.226.4.6",
"218.30.118.6",
"123.125.81.6",
"140.207.198.6",
"1.2.4.8",
"210.2.4.8",
"52.80.66.66",
"117.50.22.22",
"2400:7fc0:849e:200::4",
"2404:c2c0:85d8:901::4",
"117.50.10.10",
"52.80.52.52",
"2400:7fc0:849e:200::8",
"2404:c2c0:85d8:901::8",
"117.50.60.30",
"52.80.60.30"
]
},
{
"remarks": "绕过中国公共DNS域名",
"outboundTag": "direct",
"domain": [
"domain:alidns.com",
"domain:doh.pub",
"domain:dot.pub",
"domain:360.cn",
"domain:onedns.net"
]
},
{
"remarks": "绕过中国IP",
"outboundTag": "direct",
"ip": [
"geoip:cn"
]
},
{
"remarks": "绕过中国域名",
"outboundTag": "direct",
"domain": [
"geosite:cn"
]
}
]
@@ -1,37 +0,0 @@
[
{
"remarks": "Block udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "Direct LAN IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "Direct LAN domains",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "Bypass Iran domains",
"outboundTag": "direct",
"domain": [
"domain:ir",
"geosite:category-ir"
]
},
{
"remarks": "Bypass Iran IP",
"outboundTag": "direct",
"ip": [
"geoip:ir"
]
}
]
@@ -1,43 +0,0 @@
[
{
"remarks": "Bypass bittorrent",
"outboundTag": "direct",
"protocol": [
"bittorrent"
]
},
{
"remarks": "Block udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "Direct LAN IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "Direct LAN domains",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "Bypass Russia domains",
"outboundTag": "direct",
"domain": [
"geosite:category-ru"
]
},
{
"remarks": "Bypass Russia IP",
"outboundTag": "direct",
"ip": [
"geoip:ru"
]
}
]
File diff suppressed because it is too large. Load diff
@@ -1,413 +0,0 @@
amanita_design.samorost3.gp
android
au.com.shiftyjelly.pocketcasts
bbc.mobile.news.ww
be.mygod.vpnhotspot
ch.protonmail.android
cm.aptoide.pt
co.wanqu.android
com.alphainventor.filemanager
com.amazon.kindle
com.amazon.mshop.android.shopping
com.android.chrome
com.android.providers.downloads
com.android.providers.downloads.ui
com.android.providers.telephony
com.android.settings
com.android.vending
com.android6park.m6park
com.apkpure.aegon
com.apkupdater
com.app.pornhub
com.arthurivanets.owly
com.asahi.tida.tablet
com.authy.authy
com.avmovie
com.ballistiq.artstation
com.binance.dev
com.bitly.app
com.brave.browser
com.brave.browser_beta
com.breel.wallpapers18
com.bvanced.android.youtube
com.chrome.beta
com.chrome.canary
com.chrome.dev
com.cl.newt66y
com.cradle.iitc_mobile
org.exarhteam.iitc_mobile
com.cygames.shadowverse
com.dcard.freedom
com.devhd.feedly
com.devolver.reigns2
com.discord
com.downloader.video.tumblr
com.driverbrowser
com.dropbox.android
com.duolingo
com.duckduckgo.mobile.android
com.dv.adm
com.estrongs.android.pop
com.estrongs.android.pop.pro
com.evernote
com.facebook.katana
com.facebook.lite
com.facebook.mlite
com.facebook.orca
com.facebook.services
com.facebook.system
com.fastaccess.github
com.felixfilip.scpae
com.fireproofstudios.theroom4
com.firstrowria.pushnotificationtester
com.flyersoft.moonreaderp
com.fooview.android.fooview
com.fvd.eversync
com.gameloft.android.anmp.glofta8hm
com.gameloft.android.anmp.glofta9hm
com.gianlu.aria2app
com.github.yeriomin.yalpstore
com.google.android.apps.adm
com.google.android.apps.books
com.google.android.apps.docs
com.google.android.apps.docs.editors.sheets
com.google.android.apps.fitness
com.google.android.apps.googleassistant
com.google.android.apps.googlevoice
com.google.android.apps.hangoutsdialer
com.google.android.apps.inbox
com.google.android.apps.magazines
com.google.android.apps.maps
com.google.android.apps.nbu.files
com.google.android.apps.paidtasks
com.google.android.apps.pdfviewer
com.google.android.apps.photos
com.google.android.apps.plus
com.google.android.apps.translate
com.google.android.gm
com.google.android.gms
com.google.android.gms.setup
com.google.android.googlequicksearchbox
com.google.android.gsf
com.google.android.gsf.login
com.google.android.ims
com.google.android.inputmethod.latin
com.google.android.instantapps.supervisor
com.google.android.keep
com.google.android.music
com.google.android.ogyoutube
com.google.android.partnersetup
com.google.android.play.games
com.google.android.street
com.google.android.syncadapters.calendar
com.google.android.syncadapters.contacts
com.google.android.talk
com.google.android.tts
com.google.android.videos
com.google.android.youtube
com.google.ar.lens
com.hochan.coldsoup
com.ifttt.ifttt
com.imgur.mobile
com.innologica.inoreader
com.instagram.android
com.instagram.lite
com.instapaper.android
com.jarvanh.vpntether
com.kapp.youtube.final
com.klinker.android.twitter_l
com.lastpass.lpandroid
com.linecorp.linelite
com.lingodeer
com.ltnnews.news
com.mediapods.tumbpods
com.mgoogle.android.gms
com.microsoft.emmx
com.microsoft.office.powerpoint
com.microsoft.skydrive
com.mixplorer
com.msd.consumerchinese
com.msd.professionalchinese
com.mss2011c.sharehelper
com.netflix.mediaclient
com.newin.nplayer.pro
com.nianticlabs.ingress.prime.qa
com.nianticproject.ingress
com.ninefolders.hd3
com.ninegag.android.app
com.nintendo.zara
com.nytimes.cn
com.oasisfeng.island
com.ocnt.liveapp.hw
com.orekie.search
com.patreon.android
com.paypal.android.p2pmobile
com.perol.asdpl.pixivez
com.pinterest
com.popularapp.periodcalendar
com.popularapp.videodownloaderforinstagram
com.pushbullet.android
com.quoord.tapatalkpro.activity
com.quora.android
com.rayark.cytus2
com.rayark.implosion
com.rayark.pluto
com.reddit.frontpage
com.resilio.sync
com.rhmsoft.edit
com.rubenmayayo.reddit
com.sec.android.app.sbrowser
com.sec.android.app.sbrowser.beta
com.shanga.walli
com.simplehabit.simplehabitapp
com.slack
com.snaptube.premium
com.sololearn
com.sonelli.juicessh
com.sparkslab.dcardreader
com.spotify.music
com.tencent.huatuo
com.termux
com.teslacoilsw.launcher
com.theinitium.news
com.thomsonreuters.reuters
com.thunkable.android.hritvik00.freenom
com.topjohnwu.magisk
com.tripadvisor.tripadvisor
com.tumblr
com.twitter.android
com.u91porn
com.u9porn
com.ubisoft.dance.justdance2015companion
com.udn.news
com.utopia.pxview
com.valvesoftware.android.steam.community
com.vanced.manager
com.vanced.android.youtube
com.vanced.android.apps.youtube.music
com.mgoogle.android.gms
com.vimeo.android.videoapp
com.vivaldi.browser
com.vivaldi.browser.snapshot
com.vkontakte.android
com.whatsapp
com.wire
com.wuxiangai.refactor
com.xda.labs
com.xvideos.app
com.yahoo.mobile.client.android.superapp
com.yandex.browser
com.yandex.browser.beta
com.yandex.browser.alpha
com.z28j.feel
com.zhiliaoapp.musically
con.medium.reader
de.apkgrabber
de.robv.android.xposed.installer
dk.tacit.android.foldersync.full
es.rafalense.telegram.themes
es.rafalense.themes
flipboard.app
fm.moon.app
fr.gouv.etalab.mastodon
github.tornaco.xposedmoduletest
idm.internet.download.manager
idm.internet.download.manager.plus
io.github.javiewer
io.github.skyhacker2.magnetsearch
io.va.exposed
it.mvilla.android.fenix2
jp.bokete.app.android
jp.naver.line.android
jp.pxv.android
luo.speedometergpspro
m.cna.com.tw.App
mark.via.gp
me.tshine.easymark
net.teeha.android.url_shortener
net.tsapps.appsales
onion.fire
org.fdroid.fdroid
org.freedownloadmanager.fdm
org.kustom.widget
org.mozilla.fennec_aurora
org.mozilla.fenix
org.mozilla.fenix.nightly
org.mozilla.firefox
org.mozilla.firefox_beta
org.mozilla.focus
org.schabi.newpipe
org.telegram.messenger
org.telegram.messenger.web
org.telegram.multi
org.telegram.plus
org.thunderdog.challegram
org.torproject.android
org.torproject.torbrowser_alpha
org.wikipedia
org.xbmc.kodi
pl.zdunex25.updater
tv.twitch.android.app
tw.com.gamer.android.activecenter
videodownloader.downloadvideo.downloader
uk.co.bbc.learningenglish
com.ted.android
de.danoeh.antennapod
com.kiwibrowser.browser
nekox.messenger
com.nextcloud.client
com.aurora.store
com.aurora.adroid
chat.simplex.app
im.vector.app
network.loki.messenger
eu.siacs.conversations
xyz.nextalone.nagram
net.programmierecke.radiodroid2
im.fdx.v2ex
ml.docilealligator.infinityforreddit
com.bytemyth.ama
app.vanadium.browser
com.cakewallet.cake_wallet
org.purplei2p.i2pd
dk.tacit.android.foldersync.lite
com.nononsenseapps.feeder
com.m2049r.xmrwallet
com.paypal.android.p2pmobile
com.google.android.apps.googlevoice
com.readdle.spark
org.torproject.torbrowser
com.deepl.mobiletranslator
com.microsoft.bing
com.keylesspalace.tusky
com.ottplay.ottplay
ru.iptvremote.android.iptv.pro
jp.naver.line.android
com.xmflsct.app.tooot
com.forem.android
app.revanced.android.youtube
com.mgoogle.android.gms
com.pionex.client
vip.mytokenpocket
im.token.app
com.linekong.mars24
com.feixiaohao
com.aicoin.appandroid
com.binance.dev
com.kraken.trade
com.okinc.okex.gp
com.authy.authy
air.com.rosettastone.mobile.CoursePlayer
com.blizzard.bma
com.amazon.kindle
com.google.android.apps.fitness
net.tsapps.appsales
com.wemesh.android
com.google.android.apps.googleassistant
allen.town.focus.reader
me.hyliu.fluent_reader_lite
com.aljazeera.mobile
com.ft.news
de.marmaro.krt.ffupdater
myradio.radio.fmradio.liveradio.radiostation
com.google.earth
eu.kanade.tachiyomi.j2k
com.audials
com.microsoft.skydrive
com.mb.android.tg
com.melodis.midomiMusicIdentifier.freemium
com.foxnews.android
ch.threema.app
com.briarproject.briar.android
foundation.e.apps
com.valvesoftware.android.steam.friendsui
com.imback.yeetalk
so.onekey.app.wallet
com.xc3fff0e.xmanager
meditofoundation.medito
com.picol.client
com.streetwriters.notesnook
shanghai.panewsApp.com
org.coursera.android
com.positron_it.zlib
com.blizzard.messenger
com.javdb.javrocket
com.picacomic.fregata
com.fxl.chacha
me.proton.android.drive
com.lastpass.lpandroid
com.tradingview.tradingviewapp
com.deviantart.android.damobile
com.fusionmedia.investing
com.ewa.ewaapp
com.duolingo
com.hellotalk
io.github.huskydg.magisk
com.jsy.xpgbox
com.hostloc.app.hostloc
com.dena.pokota
com.vitorpamplona.amethyst
com.zhiliaoapp.musically
us.spotco.fennec_dos
com.fongmi.android.tv
com.pocketprep.android.itcybersecurity
com.cloudtv
com.glassdoor.app
com.indeed.android.jobsearch
com.linkedin.android
com.github.tvbox.osc.bh
com.example.douban
com.sipnetic.app
com.microsoft.rdc.androidx
org.zwanoo.android.speedtest
com.sonelli.juicessh
com.scmp.newspulse
org.lsposed.manager
mnn.Android
com.thomsonretuers.reuters
com.guardian
com.ttxapps.onesyncv2
org.fcitx.fcitx5.android.updater
com.tailscale.ipn
tw.nekomimi.nekogram
com.nexon.kartdrift
io.syncapps.lemmy_sync
com.seazon.feedme
com.readwise
de.spiritcroc.riotx
com.openai.chatgpt
io.changenow.changenow
com.poe.android
com.twingate
com.blinkslabs.blinkist.android
com.ichi2.anki
md.obsidian
com.musixmatch.android.lyrify
com.cyber.turbo
com.offsec.nethunter
me.ghui.v2er
com.samruston.twitter
org.adaway
org.swiftapps.swiftbackup
com.zerotier.one
com.quietmobile
com.instagram.barcelona
im.molly.app
com.rvx.android.youtube
com.deepl.mobiletranslator
com.qingsong.yingmi
com.lemurbrowser.exts
com.silverdev.dnartdroid
me.ash.reader
de.tutao.tutanota
dev.imranr.obtainium
com.getsomeheadspace.android
org.cromite.cromite
com.nutomic.syncthingandroid
com.bumble.app
com.cnn.mobile.android.phone
com.google.android.apps.authenticator2
com.microsoft.copilot
com.netflix.NGP.Storyteller
com.Slack
com.server.auditor.ssh.client
@@ -0,0 +1,196 @@
com.android.chrome
com.google.android.googlequicksearchbox
com.google.android.apps.photos
com.google.android.youtube
com.google.android.gm
com.google.android.apps.plus
com.android.vending
com.google.android.inputmethod.latin
com.google.android.apps.paidtasks
com.google.android.keep
com.google.android.gms.setup
com. google.android. apps.magazines
com.google.android.videos
com. google.android.gms
com.google.android.apps.books
com.google.android.music
com.google.android.play.games
com.google.android.gsf
com.google.android.gsf.login
com.app.pornhub
com.spotify.music
org.thunderdog.challegram
com.tumblr
com.twitter.android
com.xda.labs
com.kapp.youtube.final
com.google.android.ims
com.wire
mark.via.gp
com.downloader.video.tumblr
com.sololearn
com.cygames.shadowverse
com.felixfilip.scpae
amanita_design.samorost3.gp
com.devolver.reigns2
com.utopia.pxview
ch.protonmail.android
com.perol.asdpl.pixivez
com.pinterest
com.paypal.android.p2pmobile
com.arthurivanets.owly
com.rubenmayayo.reddit
com.rayark.cytus2
com.rayark.pluto
com.rayark.implosion
com.fireproofstudios.theroom4
com.netflix.mediaclient
com.instagram.android
com.google.android.apps.hangoutsdialer
com.google.android.talk
com.google.android.apps.plus
com.google.android.apps.pdfviewer
com.google.android.apps.magazines
com.google.android.apps.nbu.files
com.evernote
net.tsapps.appsales
com.google.android.apps.translate
com.google.ar.lens
com.google.android.apps.adm
com.google.android.apps.googleassistant
tw.com.gamer.android.activecenter
org.telegram.plus
com.brave.browser
com.breel.wallpapers18
com.teslacoilsw.launcher
com.lastpass.lpandroid
org.kustom.widget
com.fooview.android.fooview
com.google.android.apps.docs
com.google.android.apps.maps
com.facebook.services
com.facebook.system
com.facebook.katana
com.nianticlabs.ingress.prime.qa
com.vanced.android.youtube
com.nianticproject.ingress
com.quoord.tapatalkpro.activity
org.mozilla.firefox
com.reddit.frontpage
com.google.android.apps.fitness
au.com.shiftyjelly.pocketcasts
com.google.android.gms
com.android.providers.telephony
com.resilio.sync
com.google.android.apps.googlevoice
com.discord
com.cradle.iitc_mobile
be.mygod.vpnhotspot
com.alphainventor.filemanager
com.android.providers.downloads
com.apkpure.aegon
com.ballistiq.artstation
com.bitly.app
com.chrome.canary
com.chrome.dev
com.devhd.feedly
com.dropbox.android
com.estrongs.android.pop
com.estrongs.android.pop.pro
com.fastaccess.github
com.firstrowria.pushnotificationtester
com.fvd.eversync
com.gianlu.aria2app
com.github.yeriomin.yalpstore
com.google.android.apps.docs.editors.sheets
com.google.android.instantapps.supervisor
com.google.android.ogyoutube
com.google.android.partnersetup
com.google.android.syncadapters.calendar
com.google.android.syncadapters.contacts
com.google.android.tts
com.hochan.coldsoup
com.ifttt.ifttt
com.imgur.mobile
com.innologica.inoreader
com.instapaper.android
com.jarvanh.vpntether
com.mediapods.tumbpods
com.mgoogle.android.gms
com.microsoft.office.powerpoint
com.mixplorer
com.msd.consumerchinese
com.msd.professionalchinese
com.mss2011c.sharehelper
com.newin.nplayer.pro
com.oasisfeng.island
com.orekie.search
com.popularapp.videodownloaderforinstagram
com.pushbullet.android
com.rhmsoft.edit
com.slack
com.tencent.huatuo
com.termux
com.thunkable.android.hritvik00.freenom
com.topjohnwu.magisk
com.u91porn
com.u9porn
com.vimeo.android.videoapp
com.wuxiangai.refactor
com.yandex.browser
com.z28j.feel
de.robv.android.xposed.installer
dk.tacit.android.foldersync.full
es.rafalense.telegram.themes
es.rafalense.themes
flipboard.app
github.tornaco.xposedmoduletest
io.va.exposed
jp.pxv.android
me.tshine.easymark
net.teeha.android.url_shortener
onion.fire
org.fdroid.fdroid
org.mozilla.fennec_aurora
org.schabi.newpipe
org.telegram.messenger
org.torproject.android
org.xbmc.kodi
pl.zdunex25.updater
videodownloader.downloadvideo.downloader
com.quora.android
com.lingodeer
org.wikipedia
com.ninegag.android.app
com.duolingo
com.patreon.android
com.valvesoftware.android.steam.communimunity
co.wanqu.android
jp.bokete.app.android
com.vkontakte.android
com.amazon.mshop.android.shopping
com.ubisoft.dance.justdance2015companion
com.gameloft.android.anmp.glofta8hm
com.gameloft.android.anmp.glofta9hm
com.binance.dev
com.asahi.tida.tablet
com.theinitium.news
com.driverbrowser
com.thomsonreuters.reuters
com.nytimes.cn
com.android.providers.downloads.ui
com.avmovie
bbc.mobile.news.ww
org.mozilla.focus
io.github.javiewer
com.sonelli.juicessh
con.medium.reader
com.microsoft.skydrive
com.valvesoftware.android.steam.community
com.nintendo.zara
org.torproject.torbrowser_alpha
tv.twitch.android.app
com.shanga.walli
com.whatsapp
com.wire
com.simplehabit.simplehabitapp
+14 -11
View File
@@ -13,8 +13,8 @@
}
},
"system": {
"statsOutboundUplink": true,
"statsOutboundDownlink": true
"statsInboundUplink": true,
"statsInboundDownlink": true
}
},
"inbounds": [{
@@ -30,10 +30,17 @@
"enabled": true,
"destOverride": [
"http",
"tls",
"quic"
"tls"
]
}
},
{
"tag": "http",
"port": 10809,
"protocol": "http",
"settings": {
"userLevel": 8
}
}
],
"outbounds": [{
@@ -47,7 +54,7 @@
"users": [
{
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
"alterId": 0,
"alterId": 64,
"security": "auto",
"level": 8
}
@@ -74,11 +81,7 @@
},
{
"protocol": "freedom",
"streamSettings": {
"sockopt": {
"domainStrategy": "UseIP"
}
},
"settings": {},
"tag": "direct"
},
{
@@ -92,7 +95,7 @@
}
],
"routing": {
"domainStrategy": "AsIs",
"domainStrategy": "IPIfNonMatch",
"rules": []
},
"dns": {
@@ -1,119 +0,0 @@
{
"stats":{},
"log": {
"loglevel": "warning"
},
"policy":{
"levels": {
"8": {
"handshake": 4,
"connIdle": 300,
"uplinkOnly": 1,
"downlinkOnly": 1
}
},
"system": {
"statsOutboundUplink": true,
"statsOutboundDownlink": true
}
},
"inbounds": [{
"tag": "socks",
"port": 10808,
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": true,
"userLevel": 8
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls",
"quic"
]
}
},
{
"tag": "tun",
"protocol": "tun",
"settings": {
"name": "xray0",
"MTU": 1500,
"userLevel": 8
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls",
"quic"
]
}
}
],
"outbounds": [{
"tag": "proxy",
"protocol": "vmess",
"settings": {
"vnext": [
{
"address": "v2ray.cool",
"port": 10086,
"users": [
{
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
"alterId": 0,
"security": "auto",
"level": 8
}
]
}
],
"servers": [
{
"address": "v2ray.cool",
"method": "chacha20",
"ota": false,
"password": "123456",
"port": 10086,
"level": 8
}
]
},
"streamSettings": {
"network": "tcp"
},
"mux": {
"enabled": false
}
},
{
"protocol": "freedom",
"streamSettings": {
"sockopt": {
"domainStrategy": "UseIP"
}
},
"tag": "direct"
},
{
"protocol": "blackhole",
"tag": "block",
"settings": {
"response": {
"type": "http"
}
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": []
},
"dns": {
"hosts": {},
"servers": []
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 28 KiB

@@ -1,51 +0,0 @@
package com.v2ray.ang
import android.app.Application
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.work.Configuration
import androidx.work.WorkManager
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.handler.AppLocaleManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.ui.compose.ThemeManager
class AngApplication : Application() {
companion object {
lateinit var application: AngApplication
}
/**
* Attaches the base context to the application.
* @param base The base context.
*/
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base?.let(ContextCompat::getContextForLanguage))
application = this
}
private val workManagerConfiguration: Configuration = Configuration.Builder()
.setDefaultProcessName("${ANG_PACKAGE}:bg")
.build()
/**
* Initializes the application.
*/
override fun onCreate() {
super.onCreate()
MMKV.initialize(this)
AppLocaleManager.initialize(this)
// Initialize WorkManager with the custom configuration
WorkManager.initialize(this, workManagerConfiguration)
// Ensure critical preference defaults are present in MMKV early
SettingsManager.initApp(this)
// Initialize theme state from MMKV
ThemeManager.refresh()
}
}
@@ -1,347 +0,0 @@
package com.v2ray.ang
object AppConfig {
/** The application's package name. */
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
const val TAG = BuildConfig.APPLICATION_ID
/** Directory names used in the app's file system. */
const val DIR_ASSETS = "assets"
const val WEBDAV_BACKUP_DIR = "backups"
const val WEBDAV_BACKUP_FILE_NAME = "backup_ng.zip"
/** Legacy configuration keys. */
const val ANG_CONFIG = "ang_config"
// Default subscription ID for ungrouped servers
const val DEFAULT_SUBSCRIPTION_ID = "__default_subscription__"
/** Preferences mapped to MMKV storage. */
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled"
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
const val PREF_BYPASS_APPS = "pref_bypass_apps"
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
const val PREF_APPEND_HTTP_PROXY = "pref_append_http_proxy"
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
const val PREF_VPN_DNS = "pref_vpn_dns"
const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index"
const val PREF_VPN_MTU = "pref_vpn_mtu"
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
const val PREF_MUX_ENABLED = "pref_mux_enabled"
const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic"
const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled"
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 PREF_OBSERVATORY_LEAST_PING_INTERVAL = "pref_observatory_least_ping_interval"
const val PREF_OBSERVATORY_LEAST_LOAD_INTERVAL = "pref_observatory_least_load_interval"
const val PREF_OBSERVATORY_LEAST_LOAD_METHOD = "pref_observatory_least_load_method"
const val PREF_OBSERVATORY_LEAST_LOAD_SAMPLING = "pref_observatory_least_load_sampling"
const val PREF_OBSERVATORY_LEAST_LOAD_TIMEOUT = "pref_observatory_least_load_timeout"
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
const val SUBSCRIPTION_MIN_INTERVAL_MINUTES = 15L
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
const val PREF_DOUBLE_COLUMN_DISPLAY = "pref_double_column_display"
const val PREF_GROUP_ALL_DISPLAY = "pref_group_all_display"
const val PREF_LANGUAGE = "pref_language"
const val PREF_APP_LOCALE_MIGRATED = "pref_app_locale_migrated"
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
const val PREF_DYNAMIC_COLOR = "pref_dynamic_color"
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_ENABLE_LOCAL_PROXY = "pref_enable_local_proxy"
const val PREF_SOCKS_PORT = "pref_socks_port"
const val PREF_DYNAMIC_SOCKS_PORT = "pref_dynamic_socks_port"
const val PREF_SOCKS_USERNAME = "pref_socks_username"
const val PREF_SOCKS_PASSWORD = "pref_socks_password"
const val PREF_SOCKS_ENABLE_UDP = "pref_socks_enable_udp"
const val PREF_REMOTE_DNS = "pref_remote_dns"
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
const val PREF_DNS_HOSTS = "pref_dns_hosts"
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
const val PREF_IP_API_URL = "pref_ip_api_url"
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"
const val PREF_USE_HEV_TUNNEL = "pref_use_hev_tunnel_v2"
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
const val PREF_UPDATE_SUBSCRIPTION = "pref_update_subscription"
const val PREF_AUTO_TEST_AFTER_UPDATE_SUBSCRIPTION = "pref_auto_test_after_update_subscription"
const val PREF_AUTO_REMOVE_INVALID_AFTER_TEST = "pref_auto_remove_invalid_after_test"
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
const val PREF_REAL_PING_CONCURRENCY = "pref_real_ping_concurrency"
/** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
/** Protocol identifiers. */
const val PROTOCOL_FREEDOM = "freedom"
/** Broadcast actions. */
const val BROADCAST_ACTION_SERVICE = "$ANG_PACKAGE.action.service"
const val BROADCAST_ACTION_ACTIVITY = "$ANG_PACKAGE.action.activity"
const val BROADCAST_ACTION_WIDGET_CLICK = "$ANG_PACKAGE.action.widget.click"
/** Tasker extras. */
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
const val TASKER_DEFAULT_GUID = "Default"
/** Tags for different proxy modes. */
const val TAG_PROXY = "proxy"
const val TAG_DIRECT = "direct"
const val TAG_BLOCKED = "block"
const val TAG_FRAGMENT = "fragment"
const val TAG_DNS = "dns-module"
const val TAG_DOMESTIC_DNS = "domestic-dns"
const val TAG_BALANCER = "balancer-main"
const val TAG_BALANCER_PRE = "balancer"
/** Network-related constants. */
const val UPLINK = "uplink"
const val DOWNLINK = "downlink"
/** URLs for various resources. */
const val GITHUB_URL = "https://github.com"
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
const val APP_ISSUES_URL = "$APP_URL/issues"
const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
const val OBSERVATORY_LEAST_PING_INTERVAL = "3m"
const val OBSERVATORY_LEAST_LOAD_INTERVAL = "5m"
const val OBSERVATORY_LEAST_LOAD_METHOD = "HEAD"
const val OBSERVATORY_LEAST_LOAD_SAMPLING = "2"
const val OBSERVATORY_LEAST_LOAD_TIMEOUT = "30s"
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
const val IP_API_URL = "https://api.ip.sb/geoip"
/** DNS server addresses. */
const val DNS_PROXY = "https://cloudflare-dns.com/dns-query"
const val DNS_DIRECT = "223.5.5.5"
const val DNS_VPN = "1.1.1.1"
const val GEOSITE_PRIVATE = "geosite:private"
const val GEOSITE_CN = "geosite:cn"
const val GEOIP_PRIVATE = "geoip:private"
const val GEOIP_CN = "geoip:cn"
/** Geo data file names. */
const val GEOSITE_DAT = "geosite.dat"
const val GEOIP_DAT = "geoip.dat"
const val GEOIP_ONLY_CN_PRIVATE_DAT = "geoip-only-cn-private.dat"
const val GEOIP_ONLY_CN_PRIVATE_URL = "$GITHUB_RAW_URL/Loyalsoldier/geoip/release/$GEOIP_ONLY_CN_PRIVATE_DAT"
/** Ports and addresses for various services. */
const val PORT_LOCAL_DNS = "10853"
const val PORT_SOCKS = "10808"
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
const val WIREGUARD_LOCAL_MTU = "1420"
const val LOOPBACK = "127.0.0.1"
/** Message constants for communication. */
const val MSG_REGISTER_CLIENT = 1
const val MSG_STATE_RUNNING = 11
const val MSG_STATE_NOT_RUNNING = 12
const val MSG_UNREGISTER_CLIENT = 2
const val MSG_STATE_START = 3
const val MSG_STATE_START_SUCCESS = 31
const val MSG_STATE_START_FAILURE = 32
const val MSG_STATE_STOP = 4
const val MSG_STATE_STOP_SUCCESS = 41
const val MSG_STATE_RESTART = 5
const val MSG_MEASURE_DELAY = 6
const val MSG_MEASURE_DELAY_RESULT = 61
const val MSG_MEASURE_CONFIG_START = 7
const val MSG_MEASURE_CONFIG_CANCEL = 71
const val MSG_MEASURE_CONFIG_SUCCESS = 72
const val MSG_MEASURE_CONFIG_NOTIFY = 73
const val MSG_MEASURE_CONFIG_FINISH = 74
const val MSG_SUB_UPDATE_START = 8
const val MSG_SUB_UPDATE_CANCEL = 81
/** Notification channel IDs and names. */
// Use a new ID because Android does not let an app raise an existing channel's importance.
const val RAY_NG_CHANNEL_ID = "CORE_M_CH_ID_V2"
const val RAY_NG_CHANNEL_NAME = "Core Background Service"
/** Protocols Scheme **/
const val VMESS = "vmess://"
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://"
const val WIREGUARD = "wireguard://"
const val TUIC = "tuic://"
const val HYSTERIA = "hysteria://"
const val HYSTERIA2 = "hysteria2://"
const val HY2 = "hy2://"
const val V2RAYNFMTS = "v2rayn://"
/** Give a good name to this, IDK*/
const val VPN = "VPN"
const val VPN_MTU = 1500
/** Root (system-wide) mode runtime constants. */
const val ROOT_RUNTIME_DIR = "sys_cache"
const val ROOT_IPTABLES_CHAIN = "CORE_FILTER"
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 = "utun7788"
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 = "CORE_FWD" // FORWARD chain for LAN/tethering sharing
const val ROOT_DNS_CHAIN = "CORE_DNS" // nat chain for tethered-client DNS DNAT
const val ROOT_V6_CHAIN = "CORE6_FILTER" // ip6tables filter/OUTPUT chain: blackhole native IPv6 when it isn't tunneled
const val ROOT_V6_FWD_CHAIN = "CORE6_FWD" // ip6tables FORWARD chain: route or reject tethered clients' native IPv6
const val ROOT_V6_PRE_CHAIN = "CORE6_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"
// Google API rule constants
const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn"
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
// Android Private DNS constants
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
const val DEFAULT_SECURITY = "auto"
const val DEFAULT_LEVEL = 8
const val DEFAULT_NETWORK = "tcp"
const val TLS = "tls"
const val REALITY = "reality"
const val HEADER_TYPE_HTTP = "http"
const val UNIDENTIFIED_PACKAGE = "__unknown_app__"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_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_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_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
val ROUTED_IP_LIST = arrayListOf(
"0.0.0.0/5",
"8.0.0.0/7",
"11.0.0.0/8",
"12.0.0.0/6",
"16.0.0.0/4",
"32.0.0.0/3",
"64.0.0.0/2",
"128.0.0.0/3",
"160.0.0.0/5",
"168.0.0.0/6",
"172.0.0.0/12",
"172.32.0.0/11",
"172.64.0.0/10",
"172.128.0.0/9",
"173.0.0.0/8",
"174.0.0.0/7",
"176.0.0.0/4",
"192.0.0.0/9",
"192.128.0.0/11",
"192.160.0.0/13",
"192.169.0.0/16",
"192.170.0.0/15",
"192.172.0.0/14",
"192.176.0.0/12",
"192.192.0.0/10",
"193.0.0.0/8",
"194.0.0.0/7",
"196.0.0.0/6",
"200.0.0.0/5",
"208.0.0.0/4",
"240.0.0.0/4"
)
val PRIVATE_IP_LIST = arrayListOf(
"0.0.0.0/8",
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"224.0.0.0/4"
)
val GEO_FILES_SOURCES = arrayListOf(
"Loyalsoldier/v2ray-rules-dat",
"runetfreedom/russia-v2ray-rules-dat",
"Chocolate4U/Iran-v2ray-rules"
)
val BUILTIN_OUTBOUND_TAGS = setOf(
TAG_PROXY,
TAG_DIRECT,
TAG_BLOCKED,
)
val OBSERVATORY_DURATION_PATTERN = Regex("""[1-9]\d*(ms|s|m|h)""")
}
@@ -1,8 +0,0 @@
package com.v2ray.ang.contracts
import android.content.Context
interface IDialerService {
fun start(context: Context, dialerAddr: String)
fun stop()
}
@@ -1,38 +0,0 @@
package com.v2ray.ang.contracts
import android.app.Service
import android.net.Network
interface ServiceControl {
/**
* Gets the service instance.
* @return The service instance.
*/
fun getService(): Service
/**
* Starts the service.
*/
fun startService()
/**
* Stops the service.
*/
fun stopService()
/**
* Protects the VPN socket.
* @param socket The socket to protect.
* @return True if the socket is protected, false otherwise.
*/
fun vpnProtect(socket: Int): Boolean
/**
* Declares the networks the tunnel runs on top of.
* Only meaningful for the VPN service, the other run modes have no interface to report.
*
* @param networks The upstream networks, null to let the system pick.
* @return True if the networks were accepted.
*/
fun setUnderlyingNetworks(networks: Array<Network>?): Boolean = false
}
@@ -1,19 +0,0 @@
package com.v2ray.ang.contracts
/**
* Interface that defines the control operations for tun2socks implementations.
*
* This interface is implemented by different tunnel solutions like:
*/
interface Tun2SocksControl {
/**
* Starts the tun2socks process with the appropriate parameters.
* This initializes the VPN tunnel and connects it to the SOCKS proxy.
*/
fun startTun2Socks()
/**
* Stops the tun2socks process and cleans up resources.
*/
fun stopTun2Socks()
}
@@ -1,273 +0,0 @@
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.entities.ProfileItem
import com.v2ray.ang.enums.BalancerStrategyType
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
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
/**
* 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 {
/**
* 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, isCustom = true)
}
// 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
}
// Step 2: Resolve all non-builtin routing outbound tags.
val routingResolvedOutbounds = resolveRoutingOutbounds()
val resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds
val fallbackResolvedOutbounds = resolveFallbackOutbounds(resolvedOutbounds)
val routingDomainRules = collectRoutingDomainRulesForDns()
return CoreConfigContext(
context = context,
guid = guid,
resolvedOutbounds = resolvedOutbounds + fallbackResolvedOutbounds,
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 chainProfiles = resolveProxyChainProfiles(profile)
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
Pair(chainProfiles, type)
}
else -> {
val chainProfiles = resolveProxyChainProfilesFromGroup(profile)
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
Pair(chainProfiles, type)
}
}
return CoreConfigContext.ResolvedOutbound(
tag = tag,
profile = profile,
resolvedProfiles = resolvedProfiles,
resolvedType = resolvedType,
)
}
/**
* 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()
return serverList
.asSequence()
.mapNotNull { id -> MmkvManager.decodeServerConfig(id) }
.filter { profile ->
val subscriptionId = config.policyGroupSubscriptionId
if (subscriptionId.isNullOrBlank()) {
true
} else {
profile.subscriptionId == subscriptionId
}
}
.filter { profile ->
val filter = config.policyGroupFilter
if (filter.isNullOrBlank()) {
true
} else {
try {
Regex(filter).containsMatchIn(profile.remarks)
} catch (_: Exception) {
profile.remarks.contains(filter)
}
}
}
.filter { it.server.isNotNullEmpty() }
.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.remarks}'", e)
return listOf(config)
}
}
private fun resolveProxyChainProfiles(config: ProfileItem): List<ProfileItem> {
if (config.proxyChainProfiles.isNullOrBlank()) {
return listOf(config)
}
try {
return config.proxyChainProfiles.orEmpty().split(",")
.asSequence()
.mapNotNull { remark -> SettingsManager.getServerViaRemarks(remark) }
.filter { it.server.isNotNullEmpty() }
.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.remarks}'", e)
return listOf(config)
}
}
/**
* 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 (config.subscriptionId.isEmpty()) {
return listOf(config)
}
try {
val subItem = MmkvManager.decodeSubscription(config.subscriptionId) ?: return listOf(config)
val resolved = mutableListOf<ProfileItem>()
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 from group for '${config.remarks}'", e)
return listOf(config)
}
}
/**
* Collect enabled routing domain rules in original order for DNS segmentation.
*
* outbounds are normalized into three tags only: proxy / direct / block.
*/
private fun collectRoutingDomainRulesForDns(): List<CoreConfigContext.RoutingDomainRule> {
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return emptyList()
val result = mutableListOf<CoreConfigContext.RoutingDomainRule>()
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
}
result.add(
CoreConfigContext.RoutingDomainRule(
domain = rule.domain.orEmpty(),
outboundTag = normalizedOutboundTag
)
)
}
return result
}
/**
* Resolve and collect fallback outbounds from all POLICYGROUP nodes.
*
* Fallback targets must not overlap with already resolved tags or builtin tags.
*/
private fun resolveFallbackOutbounds(resolvedOutbounds: List<CoreConfigContext.ResolvedOutbound>): List<CoreConfigContext.ResolvedOutbound> {
return resolvedOutbounds
.asSequence()
.filter { it.resolvedType == CoreResolvedType.POLICYGROUP }
.filter { BalancerStrategyType.from(it.profile.policyGroupType).supportsObservatory && it.profile.policyGroupTestOutbounds != false }
.mapNotNull { it.profile.policyGroupFallbackTag }
.filter { it !in AppConfig.BUILTIN_OUTBOUND_TAGS && resolvedOutbounds.none { outbound -> outbound.tag == it } }
.distinct()
.mapNotNull { tag ->
SettingsManager.getServerViaRemarks(tag)
?.takeUnless { it.configType == EConfigType.CUSTOM || it.configType == EConfigType.POLICYGROUP }
?.let { resolveOutbound(tag, it) }
}
.toList()
}
}
File diff suppressed because it is too large. Load diff
@@ -1,100 +0,0 @@
package com.v2ray.ang.core
import android.content.Context
import com.v2ray.ang.AppConfig
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
import go.Seq
import libv2ray.CoreCallbackHandler
import libv2ray.CoreController
import libv2ray.Libv2ray
import java.util.concurrent.atomic.AtomicBoolean
/**
* V2Ray Native Library Manager
*
* Thread-safe singleton wrapper for Libv2ray native methods.
* Provides initialization protection and unified API for V2Ray core operations.
*/
object CoreNativeManager {
private val initialized = AtomicBoolean(false)
/**
* Initialize V2Ray core environment.
* This method is thread-safe and ensures initialization happens only once.
* Subsequent calls will be ignored silently.
*
*/
fun initCoreEnv(context: Context?) {
if (initialized.compareAndSet(false, true)) {
try {
Seq.setContext(context?.applicationContext)
val assetPath = Utils.userAssetPath(context)
val deviceId = Utils.getDeviceIdForXUDPBaseKey()
Libv2ray.initCoreEnv(assetPath, deviceId)
LogUtil.i(AppConfig.TAG, "V2Ray core environment initialized successfully")
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to initialize V2Ray core environment", e)
initialized.set(false)
throw e
}
} else {
LogUtil.d(AppConfig.TAG, "V2Ray core environment already initialized, skipping")
}
}
fun reconcileBrowserDialer(dialerAddr: String) {
try {
Libv2ray.reconcileBrowserDialer(dialerAddr)
LogUtil.i(AppConfig.TAG, "Browser dialer reconciled successfully with address: $dialerAddr")
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to reconcile browser dialer with address: $dialerAddr", e)
}
}
/**
* Get V2Ray core version.
*
* @return Version string of the V2Ray core
*/
fun getLibVersion(): String {
return try {
Libv2ray.checkVersionX()
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to check V2Ray version", e)
"Unknown"
}
}
/**
* Measure outbound connection delay.
*
* @param config The configuration JSON string
* @param testUrl The URL to test against
* @return Delay in milliseconds, or -1 if test failed
*/
fun measureOutboundDelay(config: String, testUrl: String): Long {
return try {
Libv2ray.measureOutboundDelay(config, testUrl)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to measure outbound delay", e)
-1L
}
}
/**
* Create a new core controller instance.
*
* @param handler The callback handler for core events
* @return A new CoreController instance
*/
fun newCoreController(handler: CoreCallbackHandler): CoreController {
return try {
Libv2ray.newCoreController(handler)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to create core controller", e)
throw e
}
}
}
@@ -1,692 +0,0 @@
package com.v2ray.ang.core
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.v2ray.ang.AppConfig
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
import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
/**
* Centralizes ProfileItem -> OutboundBean conversion.
* Most protocol builders mirror the previous *Fmt.toOutbound behavior.
*/
object CoreOutboundBuilder {
/** Dispatches a profile to protocol-specific outbound builder. */
fun convert(profileItem: ProfileItem): OutboundBean? {
val outbound = when (profileItem.configType) {
EConfigType.VMESS -> toOutboundVmess(profileItem)
EConfigType.SHADOWSOCKS -> toOutboundShadowsocks(profileItem)
EConfigType.SOCKS -> toOutboundSocks(profileItem)
EConfigType.VLESS -> toOutboundVless(profileItem)
EConfigType.TROJAN -> toOutboundTrojan(profileItem)
EConfigType.WIREGUARD -> toOutboundWireguard(profileItem)
EConfigType.HYSTERIA2 -> toOutboundHysteria2(profileItem)
EConfigType.HTTP -> toOutboundHttp(profileItem)
else -> null
}
outbound ?: return null
val ret = updateOutboundWithGlobalSettings(outbound)
if (!ret) return null
return outbound
}
/** Applies global outbound options (mux, protocol-specific tweaks, etc.). */
private fun updateOutboundWithGlobalSettings(outbound: OutboundBean): Boolean {
try {
var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
val protocol = outbound.protocol
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.HTTP.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.WIREGUARD.name, true)
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|| protocol.equals(EConfigType.HYSTERIA.name, true)
) {
muxEnabled = false
} else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
muxEnabled = false
}
if (muxEnabled) {
outbound.mux?.enabled = true
outbound.mux?.concurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8").orEmpty().toInt()
outbound.mux?.xudpConcurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "16").orEmpty().toInt()
outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC, "reject")
if (protocol.equals(EConfigType.VLESS.name, true) && outbound.settings?.flow?.isNotEmpty() == true) {
outbound.mux?.concurrency = -1
}
} else {
outbound.mux?.enabled = false
outbound.mux?.concurrency = -1
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
return false
}
return true
}
/** Creates an initial outbound template for a protocol type. */
fun createInitOutbound(configType: EConfigType): OutboundBean? {
return when (configType) {
EConfigType.VMESS,
EConfigType.VLESS,
EConfigType.SHADOWSOCKS,
EConfigType.SOCKS,
EConfigType.HTTP,
EConfigType.TROJAN -> OutboundBean(
protocol = configType.name.lowercase(),
settings = OutboundBean.OutSettingsBean(),
streamSettings = OutboundBean.StreamSettingsBean()
)
EConfigType.WIREGUARD -> OutboundBean(
protocol = configType.name.lowercase(),
settings = OutboundBean.OutSettingsBean(
secretKey = "",
peers = listOf(OutboundBean.OutSettingsBean.WireGuardBean())
)
)
EConfigType.HYSTERIA,
EConfigType.HYSTERIA2 -> OutboundBean(
protocol = EConfigType.HYSTERIA.name.lowercase(),
settings = OutboundBean.OutSettingsBean(),
streamSettings = OutboundBean.StreamSettingsBean()
)
else -> null
}
}
// ── Per-protocol builders — implementations are identical to each *Fmt.toOutbound ──
private fun toOutboundVmess(profileItem: ProfileItem): OutboundBean? {
val outboundBean = createInitOutbound(EConfigType.VMESS)
outboundBean?.settings?.let { settings ->
settings.address = getServerAddress(profileItem)
settings.port = profileItem.serverPort.orEmpty().toInt()
settings.id = profileItem.password.orEmpty()
settings.security = profileItem.method
settings.level = AppConfig.DEFAULT_LEVEL
}
val sni = outboundBean?.streamSettings?.let {
populateTransportSettings(it, profileItem)
}
outboundBean?.streamSettings?.let {
populateTlsSettings(it, profileItem, sni)
}
return outboundBean
}
private fun toOutboundVless(profileItem: ProfileItem): OutboundBean? {
val outboundBean = createInitOutbound(EConfigType.VLESS)
outboundBean?.settings?.let { settings ->
settings.address = getServerAddress(profileItem)
settings.port = profileItem.serverPort.orEmpty().toInt()
settings.id = profileItem.password.orEmpty()
settings.encryption = profileItem.method
settings.flow = profileItem.flow
settings.level = AppConfig.DEFAULT_LEVEL
}
val sni = outboundBean?.streamSettings?.let {
populateTransportSettings(it, profileItem)
}
outboundBean?.streamSettings?.let {
populateTlsSettings(it, profileItem, sni)
}
return outboundBean
}
private fun toOutboundShadowsocks(profileItem: ProfileItem): OutboundBean? {
val outboundBean = createInitOutbound(EConfigType.SHADOWSOCKS)
outboundBean?.settings?.let { settings ->
settings.address = getServerAddress(profileItem)
settings.port = profileItem.serverPort.orEmpty().toInt()
settings.password = profileItem.password
settings.method = profileItem.method
settings.level = AppConfig.DEFAULT_LEVEL
}
val sni = outboundBean?.streamSettings?.let {
populateTransportSettings(it, profileItem)
}
outboundBean?.streamSettings?.let {
populateTlsSettings(it, profileItem, sni)
}
return outboundBean
}
private fun toOutboundTrojan(profileItem: ProfileItem): OutboundBean? {
val outboundBean = createInitOutbound(EConfigType.TROJAN)
outboundBean?.settings?.let { settings ->
settings.address = getServerAddress(profileItem)
settings.port = profileItem.serverPort.orEmpty().toInt()
settings.password = profileItem.password
settings.flow = profileItem.flow
settings.level = AppConfig.DEFAULT_LEVEL
}
val sni = outboundBean?.streamSettings?.let {
populateTransportSettings(it, profileItem)
}
outboundBean?.streamSettings?.let {
populateTlsSettings(it, profileItem, sni)
}
return outboundBean
}
private fun toOutboundSocks(profileItem: ProfileItem): OutboundBean? {
val outboundBean = createInitOutbound(EConfigType.SOCKS)
outboundBean?.settings?.let { settings ->
settings.address = getServerAddress(profileItem)
settings.port = profileItem.serverPort.orEmpty().toInt()
settings.level = AppConfig.DEFAULT_LEVEL
if (profileItem.username.isNotNullEmpty()) {
settings.user = profileItem.username.orEmpty()
settings.pass = profileItem.password.orEmpty()
}
}
return outboundBean
}
private fun toOutboundHttp(profileItem: ProfileItem): OutboundBean? {
val outboundBean = createInitOutbound(EConfigType.HTTP)
outboundBean?.settings?.let { settings ->
settings.address = getServerAddress(profileItem)
settings.port = profileItem.serverPort.orEmpty().toInt()
settings.level = AppConfig.DEFAULT_LEVEL
if (profileItem.username.isNotNullEmpty()) {
settings.user = profileItem.username.orEmpty()
settings.pass = profileItem.password.orEmpty()
}
}
return outboundBean
}
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 = addresses
wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank()
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
}
wireguard.mtu = profileItem.mtu
wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
}
if (!profileItem.finalMask.isNullOrBlank()) {
outboundBean?.streamSettings = OutboundBean.StreamSettingsBean()
outboundBean?.streamSettings?.let {
updateOutboundFinalMask(it, profileItem)
it.network = null
}
}
return outboundBean
}
private fun toOutboundHysteria2(profileItem: ProfileItem): OutboundBean? {
val outboundBean = createInitOutbound(EConfigType.HYSTERIA2) ?: return null
profileItem.network = NetworkType.HYSTERIA.type
profileItem.alpn = "h3"
outboundBean.settings?.let { server ->
server.address = getServerAddress(profileItem)
server.port = profileItem.serverPort.orEmpty().toInt()
server.version = 2
}
val sni = outboundBean.streamSettings?.let {
populateTransportSettings(it, profileItem)
}
outboundBean.streamSettings?.let {
populateTlsSettings(it, profileItem, sni)
}
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.
*
* Sets up protocol-specific transport options based on the profile settings.
*
* @param streamSettings The stream settings to configure
* @param profileItem The profile containing transport configuration
* @return The Server Name Indication (SNI) value to use, or null if not applicable
*/
fun populateTransportSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem): String? {
val transport = profileItem.network.orEmpty()
val headerType = profileItem.headerType
val host = profileItem.host
val path = profileItem.path
val seed = profileItem.seed
// val quicSecurity = profileItem.quicSecurity
// val key = profileItem.quicKey
val mode = profileItem.mode
val serviceName = profileItem.serviceName
val authority = profileItem.authority
val xhttpMode = profileItem.xhttpMode
val xhttpExtra = profileItem.xhttpExtra
val finalMask = profileItem.finalMask
var sni: String? = null
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
when (streamSettings.network) {
NetworkType.TCP.type -> {
val tcpSetting = OutboundBean.StreamSettingsBean.TcpSettingsBean()
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
val requestObj = createTcpHttpRequest(host, path)
tcpSetting.header.request = requestObj
sni = requestObj.headers.Host?.getOrNull(0)
} else {
tcpSetting.header.type = "none"
sni = host
}
streamSettings.tcpSettings = tcpSetting
}
NetworkType.KCP.type -> {
val kcpSetting = OutboundBean.StreamSettingsBean.KcpSettingsBean()
profileItem.kcpMtu?.let { kcpSetting.mtu = it }
profileItem.kcpTti?.let { kcpSetting.tti = it }
streamSettings.kcpSettings = kcpSetting
val udpMaskList =
mutableListOf<OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean>()
if (!headerType.isNullOrEmpty() && headerType != "none") {
val kcpHeaderType = when {
headerType == "wechat-video" -> "wechat"
else -> headerType
}
udpMaskList.add(
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
type = "mkcp-legacy",
settings =
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
header = kcpHeaderType,
value = if (headerType == "dns" && !host.isNullOrEmpty()) {
host
} else {
null
}
)
)
)
}
if (seed.isNullOrEmpty()) {
udpMaskList.add(
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
type = "mkcp-legacy"
)
)
} else {
udpMaskList.add(
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
type = "mkcp-legacy",
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
value = seed
)
)
)
}
udpMaskList.reverse()
streamSettings.finalmask = OutboundBean.StreamSettingsBean.FinalMaskBean(
udp = udpMaskList.toList()
)
}
NetworkType.WS.type -> {
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
wssetting.host = host.orEmpty()
sni = host
wssetting.path = path ?: "/"
streamSettings.wsSettings = wssetting
}
NetworkType.HTTP_UPGRADE.type -> {
val httpupgradeSetting = OutboundBean.StreamSettingsBean.HttpupgradeSettingsBean()
httpupgradeSetting.host = host.orEmpty()
sni = host
httpupgradeSetting.path = path ?: "/"
streamSettings.httpupgradeSettings = httpupgradeSetting
}
NetworkType.XHTTP.type -> {
val xhttpSetting = OutboundBean.StreamSettingsBean.XhttpSettingsBean()
xhttpSetting.host = host.orEmpty()
sni = host
xhttpSetting.path = path ?: "/"
xhttpSetting.mode = xhttpMode
xhttpSetting.extra = JsonUtil.parseString(xhttpExtra)
streamSettings.xhttpSettings = xhttpSetting
}
NetworkType.H2.type, NetworkType.HTTP.type -> {
streamSettings.network = NetworkType.H2.type
val h2Setting = OutboundBean.StreamSettingsBean.HttpSettingsBean()
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
sni = h2Setting.host.getOrNull(0)
h2Setting.path = path ?: "/"
streamSettings.httpSettings = h2Setting
}
// "quic" -> {
// val quicsetting = QuicSettingBean()
// quicsetting.security = quicSecurity ?: "none"
// quicsetting.key = key.orEmpty()
// quicsetting.header.type = headerType ?: "none"
// quicSettings = quicsetting
// }
NetworkType.GRPC.type -> {
val grpcSetting = OutboundBean.StreamSettingsBean.GrpcSettingsBean()
grpcSetting.multiMode = mode == "multi"
grpcSetting.serviceName = serviceName.orEmpty()
grpcSetting.authority = authority.orEmpty()
grpcSetting.idle_timeout = 60
grpcSetting.health_check_timeout = 20
sni = authority
streamSettings.grpcSettings = grpcSetting
}
NetworkType.HYSTERIA.type -> {
val hysteriaSetting = OutboundBean.StreamSettingsBean.HysteriaSettingsBean(
version = 2,
auth = profileItem.password.orEmpty(),
)
val quicParams = OutboundBean.StreamSettingsBean.FinalMaskBean.QuicParamsBean(
brutalUp = profileItem.bandwidthUp?.nullIfBlank(),
brutalDown = profileItem.bandwidthDown?.nullIfBlank(),
)
quicParams.congestion = if (quicParams.brutalUp != null || quicParams.brutalDown != null) "brutal" else null
if (profileItem.portHopping.isNotNullEmpty()) {
val rawInterval = profileItem.portHoppingInterval?.trim().nullIfBlank()
val interval = if (rawInterval == null) {
"30"
} else {
val singleValue = rawInterval.toIntOrNull()
if (singleValue != null) {
if (singleValue < 5) {
"30"
} else {
rawInterval
}
} else {
val parts = rawInterval.split('-')
if (parts.size == 2) {
val start = parts[0].trim().toIntOrNull()
val end = parts[1].trim().toIntOrNull()
if (start != null && end != null) {
val minStart = maxOf(5, start)
val minEnd = maxOf(minStart, end)
"$minStart-$minEnd"
} else {
"30"
}
} else {
"30"
}
}
}
quicParams.udpHop = OutboundBean.StreamSettingsBean.FinalMaskBean.QuicParamsBean.UdpHopBean(
ports = profileItem.portHopping,
interval = interval
)
}
val finalmask = OutboundBean.StreamSettingsBean.FinalMaskBean(
quicParams = quicParams
)
if (profileItem.obfsPassword.isNotNullEmpty()) {
finalmask.udp = listOf(
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
type = "salamander",
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
password = profileItem.obfsPassword.orEmpty()
)
)
)
}
streamSettings.hysteriaSettings = hysteriaSetting
streamSettings.finalmask = finalmask
}
}
finalMask?.let {
val parsedFinalMask = JsonUtil.parseString(finalMask)
if (parsedFinalMask != null) {
streamSettings.finalmask = parsedFinalMask
} else {
LogUtil.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
}
}
return sni
}
/**
* Configures TLS or REALITY security settings for an outbound connection.
*
* Sets up security-related parameters like certificates, fingerprints, and SNI.
*
* @param streamSettings The stream settings to configure
* @param profileItem The profile containing security configuration
* @param sniExt An external SNI value to use if the profile doesn't specify one
*/
fun populateTlsSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
val streamSecurity = profileItem.security.orEmpty()
val allowInsecure = profileItem.insecure == true && profileItem.pinnedCA256.isNullOrEmpty()
val sni = if (profileItem.sni.isNullOrEmpty()) {
when {
sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt
profileItem.server.isNotNullEmpty() && Utils.isDomainName(profileItem.server) -> profileItem.server
else -> sniExt
}
} else {
profileItem.sni
}
streamSettings.security = streamSecurity.nullIfBlank()
if (streamSettings.security == null) return
val tlsSetting = OutboundBean.StreamSettingsBean.TlsSettingsBean(
allowInsecure = allowInsecure,
serverName = sni.nullIfBlank(),
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(),
spiderX = profileItem.spiderX.nullIfBlank(),
mldsa65Verify = profileItem.mldsa65Verify.nullIfBlank(),
)
if (streamSettings.security == AppConfig.TLS) {
streamSettings.tlsSettings = tlsSetting
streamSettings.realitySettings = null
} else if (streamSettings.security == AppConfig.REALITY) {
streamSettings.tlsSettings = null
streamSettings.realitySettings = tlsSetting
}
if (profileItem.finalMask.isNullOrEmpty()) {
updateOutboundFragment(streamSettings)
}
}
/**
* Updates the outbound with fragment settings for traffic optimization.
*
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
*
* @param streamSettings The streamSettings object to be modified
* @return true if fragment configuration was successful, false otherwise
*/
private fun updateOutboundFragment(streamSettings: OutboundBean.StreamSettingsBean): Boolean {
try {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
return true
}
if (streamSettings.security != AppConfig.TLS
&& streamSettings.security != AppConfig.REALITY
) {
return true
}
if (streamSettings.sockopt?.dialerProxy.isNotNullEmpty()) {
return true
}
var packets =
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
if (streamSettings.security == AppConfig.REALITY
&& packets == "tlshello"
) {
packets = "1-3"
}
val fragmentMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
type = "fragment",
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
packets = packets,
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
?: "50-100",
delay = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
?: "10-20",
maxSplit = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_MAXSPLIT)
?: "10"
)
)
val noiseMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
type = "noise",
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
noise = listOf(
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean.NoiseMaskBean(
rand = "10-20",
delay = "10-16",
)
)
)
)
val finalMaskObj = streamSettings.finalmask?.let { existingFinalMask ->
JsonUtil.parseString(JsonUtil.toJson(existingFinalMask))
} ?: JsonObject()
fun appendMask(scope: String, mask: OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean) {
val current = finalMaskObj.get(scope)
if (current != null && current.isJsonArray && current.asJsonArray.size() > 0) {
return
}
val newArray = JsonArray()
newArray.add(JsonUtil.parseString(JsonUtil.toJson(mask)))
if (current != null && current.isJsonArray) {
current.asJsonArray.forEach { newArray.add(it) }
}
finalMaskObj.add(scope, newArray)
}
appendMask("tcp", fragmentMask)
appendMask("udp", noiseMask)
streamSettings.finalmask = finalMaskObj
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to update outbound fragment", e)
return false
}
return true
}
private fun getServerAddress(profileItem: ProfileItem): String {
if (Utils.isPureIpAddress(profileItem.server.orEmpty())) {
return profileItem.server.orEmpty()
}
val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty())
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") {
return domain
}
//Resolve and replace domain
val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
if (resolvedIps.isNullOrEmpty()) {
return domain
}
return resolvedIps.first()
}
fun updateOutboundFinalMask(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem) {
val finalMask = profileItem.finalMask
finalMask?.let {
val parsedFinalMask = JsonUtil.parseString(profileItem.finalMask)
if (parsedFinalMask != null) {
streamSettings.finalmask = parsedFinalMask
} else {
LogUtil.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
}
}
}
}
@@ -1,500 +0,0 @@
package com.v2ray.ang.core
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Build
import android.os.ParcelFileDescriptor
import android.system.OsConstants
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.contracts.IDialerService
import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.dto.ConnectionTestResult
import com.v2ray.ang.dto.OutboundTrafficStat
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.BrowserDialerMode
import com.v2ray.ang.extension.isNotNullEmpty
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.helper.MessageHelper
import com.v2ray.ang.service.DialerNativeService
import com.v2ray.ang.service.DialerWebviewService
import com.v2ray.ang.service.NetworkMonitor
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import libv2ray.CoreCallbackHandler
import libv2ray.CoreController
import libv2ray.ProcessFinder
import java.lang.ref.SoftReference
import java.net.InetSocketAddress
object CoreServiceManager {
private val coreController: CoreController = CoreNativeManager.newCoreController(CoreCallback())
private val mMsgReceive = ReceiveMessageHandler()
private var currentConfig: ProfileItem? = null
private var processFinder: XrayProcessFinder? = null
private var browserDialer: IDialerService? = null
private var networkMonitor: NetworkMonitor? = null
@Volatile
private var isReloading = false
/** Tun descriptor the core was started with, null in the proxy only and root run modes. */
private var currentVpnInterface: ParcelFileDescriptor? = null
var serviceControl: SoftReference<ServiceControl>? = null
set(value) {
field = value
val service = value?.get()?.getService()
CoreNativeManager.initCoreEnv(service)
if (service != null && processFinder == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
processFinder = XrayProcessFinder(service)
coreController.registerProcessFinder(processFinder)
}
}
/**
* Checks if the V2Ray service is running.
* @return True if the service is running, false otherwise.
*/
fun isRunning() = coreController.isRunning
/**
* Gets the name of the currently running server.
* @return The name of the running server.
*/
fun getRunningServerName() = currentConfig?.remarks.orEmpty()
/**
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
* Starts the V2Ray core service.
*/
fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean {
if (isRunning()) {
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
return false
}
val service = getService()
if (service == null) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Service is null")
return false
}
try {
doStartCoreLoop(service, vpnInterface)
return true
} catch (e: Exception) {
val message = e.message?.takeUnless { it.isBlank() } ?: e.javaClass.simpleName
LogUtil.e(AppConfig.TAG, "StartCore-Manager: $message", e)
MessageHelper.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, message)
NotificationManager.cancelNotification()
return false
}
}
@Throws(Exception::class)
private fun doStartCoreLoop(service: Service, vpnInterface: ParcelFileDescriptor?) {
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
mFilter.addAction(Intent.ACTION_SCREEN_ON)
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
mFilter.addAction(Intent.ACTION_USER_PRESENT)
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
currentVpnInterface = vpnInterface
launchCore(service, vpnInterface)
startNetworkMonitor(service)
}
@Throws(Exception::class)
private fun launchCore(service: Service, vpnInterface: ParcelFileDescriptor?, isReload: Boolean = false) {
val guid = MmkvManager.getSelectServer() ?: error("No server selected")
val config = MmkvManager.decodeServerConfig(guid) ?: error("Failed to decode server config")
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}")
val result = CoreConfigManager.getV2rayConfig(service, guid)
LogUtil.d(AppConfig.TAG, result.content)
if (!result.status) {
error(result.errorMessage.ifBlank { "Failed to get V2Ray config" })
}
currentConfig = config
var tunFd = vpnInterface?.fd ?: 0
val dialerMode = BrowserDialerMode.from(config.browserDialerMode)
val dialerAddr = if (dialerMode != null) {
"127.0.0.1:${Utils.findRandomFreePort()}"
} else {
""
}
if (SettingsManager.isUsingHevTun()) {
tunFd = 0
}
NotificationManager.showNotification(currentConfig)
if (dialerAddr.isNotNullEmpty()) {
CoreNativeManager.reconcileBrowserDialer(dialerAddr)
}
coreController.startLoop(result.content, tunFd)
if (!isRunning()) {
error("Core failed to start")
}
if (browserDialer != null) {
browserDialer!!.stop()
browserDialer = null
}
when (dialerMode) {
BrowserDialerMode.OKHTTP -> {
browserDialer = DialerNativeService()
browserDialer!!.start(service, dialerAddr)
}
BrowserDialerMode.WEBVIEW -> {
browserDialer = DialerWebviewService()
browserDialer!!.start(service, dialerAddr)
}
else -> {}
}
if (!isReload) {
MessageHelper.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
}
NotificationManager.startSpeedNotification()
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
}
/**
* Stops the V2Ray core service.
* Unregisters broadcast receivers, stops notifications, and shuts down plugins.
* @return True if the core was stopped successfully, false otherwise.
*/
fun stopCoreLoop(): Boolean {
val service = getService() ?: return false
networkMonitor?.unregister()
networkMonitor = null
currentVpnInterface = null
if (isRunning()) {
CoroutineScope(Dispatchers.IO).launch {
try {
coreController.stopLoop()
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
}
}
}
// Close existing browser dialer
CoreNativeManager.reconcileBrowserDialer("")
if (browserDialer != null) {
browserDialer!!.stop()
browserDialer = null
}
MessageHelper.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
NotificationManager.cancelNotification()
try {
service.unregisterReceiver(mMsgReceive)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
}
return true
}
/**
* Subscribes to upstream network changes for whichever run mode is active.
* All three services share this manager, so the tunnel recovers from a handover in proxy only
* and root mode as well, not just behind the VPN interface.
*/
private fun startNetworkMonitor(service: Service) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return
if (networkMonitor != null) return
val connectivity = service.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return
networkMonitor = NetworkMonitor(
connectivity = connectivity,
onUnderlyingNetworksChanged = { networks -> serviceControl?.get()?.setUnderlyingNetworks(networks) },
onHandover = { reloadCore() },
).also { it.register() }
}
/**
* Restarts the core in place after the upstream network changed: the service, the notification
* and the VPN interface all stay up, so nothing of this is visible.
*
* The config is rebuilt on purpose, outbound server domains are resolved while building it and
* an address resolved on a network that is gone can be unusable on the new one.
*
* @return True if the core is running again.
*/
private fun reloadCore(): Boolean {
if (isReloading) return false
val service = getService() ?: return false
if (!isRunning()) return false
return try {
val tunFd = currentVpnInterface
isReloading = true
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core reload start...")
coreController.stopLoop()
launchCore(service, tunFd, isReload = true)
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core reload finished")
true
} catch (e: Exception) {
val message = e.message?.takeUnless { it.isBlank() } ?: e.javaClass.simpleName
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to reload core: $message", e)
MessageHelper.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, message)
false
} finally {
isReloading = false
}
}
/**
* Queries and resets all outbound traffic counters in one core call.
* Go side format: tag,direction,value;tag,direction,value;
*/
fun queryAllOutboundTrafficStats(): List<OutboundTrafficStat> {
// The stats manager is gone once the core stops, querying it then reaches into freed state.
if (!isRunning()) return emptyList()
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
}
/**
* Measures the connection delay for the current V2Ray configuration.
* Tests with primary URL first, then falls back to alternative URL if needed.
* Also fetches remote IP information if the delay test was successful.
*/
private fun measureV2rayDelay() {
if (!isRunning()) {
return
}
CoroutineScope(Dispatchers.IO).launch {
val service = getService() ?: return@launch
var time = -1L
var errorStr = ""
try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":").orEmpty()
}
if (time == -1L) {
try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":").orEmpty()
}
}
val result = ConnectionTestResult(
delayMillis = time,
errorMessage = errorStr,
)
MessageHelper.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_RESULT, result)
// Only fetch IP info if the delay test was successful
if (time >= 0) {
SpeedtestManager.getRemoteIPInfo()?.let { ip ->
MessageHelper.sendMsg2UI(
service,
AppConfig.MSG_MEASURE_DELAY_RESULT,
result.copy(
country = ip.country,
ipAddress = ip.ipAddress,
),
)
}
}
}
}
/**
* Gets the current service instance.
* @return The current service instance, or null if not available.
*/
private fun getService(): Service? {
return serviceControl?.get()?.getService()
}
/**
* Core callback handler implementation for handling V2Ray core events.
* Handles startup, shutdown, socket protection, and status emission.
*/
private class CoreCallback : CoreCallbackHandler {
/**
* Called when V2Ray core starts up.
* @return 0 for success, any other value for failure.
*/
override fun startup(): Long {
return 0
}
/**
* Called when V2Ray core shuts down.
* @return 0 for success, any other value for failure.
*/
override fun shutdown(): Long {
val serviceControl = serviceControl?.get() ?: return -1
return try {
serviceControl.stopService()
0
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
-1
}
}
/**
* Called when V2Ray core emits status information.
* @param l Status code.
* @param s Status message.
* @return Always returns 0.
*/
override fun onEmitStatus(l: Long, s: String?): Long {
return 0
}
}
/**
* Process finder implementation for Xray core.
* Uses ConnectivityManager to find the owning UID of a connection based on network parameters.
*/
private class XrayProcessFinder(context: Context) : ProcessFinder {
private val cm: ConnectivityManager? = context.getSystemService(ConnectivityManager::class.java)
override fun findProcessByConnection(network: String, srcIP: String, srcPort: Long, destIP: String, destPort: Long): Long {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return -1L
if (cm == null) return -1L
val proto = when (network) {
"tcp" -> OsConstants.IPPROTO_TCP
"udp" -> OsConstants.IPPROTO_UDP
else -> return -1L
}
if (destIP.isBlank() || destPort == 0L) {
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to :$destPort, (no dest)")
return -1L
}
return try {
val uid = cm.getConnectionOwnerUid(
proto,
InetSocketAddress(srcIP, srcPort.toInt()),
InetSocketAddress(destIP, destPort.toInt())
).toLong()
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid")
//LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid,${PackageUidResolver.uidToPackageName(uid.toString())}")
uid
} catch (_: Exception) {
-1L
}
}
}
/**
* Broadcast receiver for handling messages sent to the service.
* Handles registration, service control, and screen events.
*/
private class ReceiveMessageHandler : BroadcastReceiver() {
/**
* Handles received broadcast messages.
* Processes service control messages and screen state changes.
* @param ctx The context in which the receiver is running.
* @param intent The intent being received.
*/
override fun onReceive(ctx: Context?, intent: Intent?) {
val serviceControl = serviceControl?.get() ?: return
when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_REGISTER_CLIENT -> {
if (isRunning()) {
MessageHelper.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
} else {
MessageHelper.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
}
}
AppConfig.MSG_UNREGISTER_CLIENT -> {
// nothing to do
}
AppConfig.MSG_STATE_START -> {
// nothing to do
}
AppConfig.MSG_STATE_STOP -> {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Stop service")
serviceControl.stopService()
}
AppConfig.MSG_STATE_RESTART -> {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Restart service")
serviceControl.stopService()
Thread.sleep(500L)
LauncherManager.startService(serviceControl.getService())
}
AppConfig.MSG_MEASURE_DELAY -> {
measureV2rayDelay()
}
}
when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen off")
NotificationManager.stopSpeedNotification()
}
Intent.ACTION_SCREEN_ON -> {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen on")
NotificationManager.startSpeedNotification()
}
}
}
}
}
@@ -1,129 +0,0 @@
package com.v2ray.ang.core
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
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.SettingsManager
import com.v2ray.ang.helper.MessageHelper
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.util.LogUtil
import com.v2ray.ang.util.Utils
object LauncherManager {
fun startServiceFromToggle(context: Context): Boolean {
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
context.toast(R.string.app_tile_first_use)
return false
}
try {
startContextService(context)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "LauncherManager: ${e.message}", e)
context.toast(e.message ?: e.javaClass.simpleName)
return false
}
return true
}
fun startService(context: Context, guid: String? = null) {
LogUtil.i(AppConfig.TAG, "LauncherManager: startService from ${context::class.java.simpleName}")
if (guid != null) {
MmkvManager.setSelectServer(guid)
}
try {
startContextService(context)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "LauncherManager: ${e.message}", e)
context.toast(e.message ?: e.javaClass.simpleName)
}
}
fun stopService(context: Context) {
//context.toast(R.string.toast_services_stop)
MessageHelper.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
}
@Throws(Exception::class)
private fun startContextService(context: Context) {
// Note: isRunning check is removed here to avoid loading Native libraries in the UI process.
// The check is performed in CoreServiceManager when the service starts in the daemon process.
val guid = MmkvManager.getSelectServer()
?: run {
LogUtil.e(AppConfig.TAG, "LauncherManager: No server selected")
error(context.getString(R.string.app_tile_first_use))
}
val config = MmkvManager.decodeServerConfig(guid)
?: run {
LogUtil.e(AppConfig.TAG, "LauncherManager: Failed to decode server config")
error(context.getString(R.string.toast_config_file_invalid))
}
if (!config.configType.isComplexType()
&& !Utils.isValidUrl(config.server)
&& !Utils.isPureIpAddress(config.server.orEmpty())
) {
LogUtil.e(AppConfig.TAG, "LauncherManager: Invalid server configuration")
error(context.getString(R.string.toast_config_file_invalid))
}
SettingsManager.refreshRuntimeSocksPort()
if (config.insecure == true && config.pinnedCA256.isNullOrEmpty()) {
context.toastError(R.string.toast_allow_insecure_deprecated)
Utils.setClipboard(context, context.getString(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 isRootMode = SettingsManager.isRootMode()
if (isRootMode && !RootManager.isRootAvailable()) {
LogUtil.e(AppConfig.TAG, "LauncherManager: root mode requires root but none available")
error(context.getString(R.string.toast_root_required))
}
val intent = if (isRootMode) {
LogUtil.i(AppConfig.TAG, "LauncherManager: Starting Root service")
Intent(context.applicationContext, CoreRootService::class.java)
} else if (SettingsManager.isVpnMode()) {
LogUtil.i(AppConfig.TAG, "LauncherManager: Starting VPN service")
Intent(context.applicationContext, CoreVpnService::class.java)
} else {
LogUtil.i(AppConfig.TAG, "LauncherManager: Starting Proxy service")
Intent(context.applicationContext, CoreProxyOnlyService::class.java)
}
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: SecurityException) {
LogUtil.e(AppConfig.TAG, "LauncherManager: 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, "LauncherManager: Foreground service start not allowed", e)
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
}
throw e
}
}
}
@@ -1,8 +0,0 @@
package com.v2ray.ang.dto
data class AppInfo(
val appName: String,
val packageName: String,
val isSystemApp: Boolean,
var isSelected: Int
)
@@ -1,13 +0,0 @@
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,10 +0,0 @@
package com.v2ray.ang.dto
data class CheckUpdateResult(
val hasUpdate: Boolean,
val latestVersion: String? = null,
val releaseNotes: String? = null,
val downloadUrl: String? = null,
val error: String? = null,
val isPreRelease: Boolean = false
)
@@ -1,9 +0,0 @@
package com.v2ray.ang.dto
data class ConfigResult(
var status: Boolean,
var guid: String? = null,
var content: String = "",
var errorMessage: String = "",
)
@@ -1,11 +0,0 @@
package com.v2ray.ang.dto
import java.io.Serializable
/** Locale-neutral result sent from the daemon for presentation by the UI process. */
data class ConnectionTestResult(
val delayMillis: Long,
val errorMessage: String = "",
val country: String? = null,
val ipAddress: String? = null,
) : Serializable
@@ -1,25 +0,0 @@
package com.v2ray.ang.dto
import android.content.Context
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.CoreResolvedType
data class CoreConfigContext(
val context: Context,
val guid: String,
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,
)
}
@@ -1,23 +0,0 @@
package com.v2ray.ang.dto
import com.google.gson.annotations.SerializedName
data class GitHubRelease(
@SerializedName("tag_name")
val tagName: String,
@SerializedName("body")
val body: String,
@SerializedName("assets")
val assets: List<Asset>,
@SerializedName("prerelease")
val prerelease: Boolean = false,
@SerializedName("published_at")
val publishedAt: String = ""
) {
data class Asset(
@SerializedName("name")
val name: String,
@SerializedName("browser_download_url")
val browserDownloadUrl: String
)
}
@@ -1,6 +0,0 @@
package com.v2ray.ang.dto
data class GroupMapItem(
var id: String,
var remarks: String,
)
@@ -1,17 +0,0 @@
package com.v2ray.ang.dto
data class IPAPIInfo(
var ip: String? = null,
var clientIp: String? = null,
var ip_addr: String? = null,
var query: String? = null,
var country: String? = null,
var country_name: String? = null,
var country_code: String? = null,
var countryCode: String? = null,
var location: LocationBean? = null
) {
data class LocationBean(
var country_code: String? = null
)
}
@@ -1,7 +0,0 @@
package com.v2ray.ang.dto
data class LocateTarget(
val groupId: String,
val groupIndex: Int,
val itemPosition: Int
)
@@ -1,7 +0,0 @@
package com.v2ray.ang.dto
data class OutboundTrafficStat(
val tag: String,
val direction: String,
val value: Long,
)
@@ -1,14 +0,0 @@
package com.v2ray.ang.dto
sealed class RealPingEvent {
/** Periodic progress update while the batch is still running. */
data class Progress(val text: String) : RealPingEvent()
/** A single server result is available. */
data class Result(val guid: String, val delayMillis: Long) : RealPingEvent()
/** The entire batch has finished or been cancelled. */
data class Finish(val status: String) : RealPingEvent()
}
@@ -1,9 +0,0 @@
package com.v2ray.ang.dto
import java.io.Serializable
data class SubscriptionUpdateMessage(
val key: Int,
val forcedUpdate: Boolean,
val subIds: List<String> = listOf()
) : Serializable
@@ -1,24 +0,0 @@
package com.v2ray.ang.dto
/**
* Result of subscription update operation
*/
data class SubscriptionUpdateResult(
val configCount: Int = 0, // Total configs updated
val successCount: Int = 0, // Subscriptions updated successfully
val failureCount: Int = 0, // Subscriptions failed to update
val skipCount: Int = 0 // Subscriptions skipped (disabled)
) {
/**
* Combine two results by adding their counts
*/
operator fun plus(other: SubscriptionUpdateResult): SubscriptionUpdateResult {
return SubscriptionUpdateResult(
configCount = this.configCount + other.configCount,
successCount = this.successCount + other.successCount,
failureCount = this.failureCount + other.failureCount,
skipCount = this.skipCount + other.skipCount
)
}
}
@@ -1,11 +0,0 @@
package com.v2ray.ang.dto
import java.io.Serializable
data class TestServiceMessage(
val key: Int,
val subscriptionId: String = "",
val serverGuids: List<String> = emptyList(),
val onlyTcp: Boolean = false
) : Serializable
@@ -1,11 +0,0 @@
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,
val requestHeaders: String? = null
)
@@ -1,468 +0,0 @@
package com.v2ray.ang.dto
import com.google.gson.annotations.SerializedName
import com.v2ray.ang.AppConfig
import com.v2ray.ang.enums.EConfigType
data class V2rayConfig(
var remarks: String? = null,
var stats: Any? = null,
val log: LogBean,
var policy: PolicyBean? = null,
val inbounds: ArrayList<InboundBean>,
var outbounds: ArrayList<OutboundBean>,
var dns: DnsBean? = null,
val routing: RoutingBean,
val api: Any? = null,
val transport: Any? = null,
val reverse: Any? = null,
var fakedns: Any? = null,
val browserForwarder: Any? = null,
var observatory: Any? = null,
var burstObservatory: Any? = null
) {
data class LogBean(
val access: String? = null,
val error: String? = null,
var loglevel: String? = null,
val dnsLog: Boolean? = null
)
data class InboundBean(
var tag: String,
var port: Int?,
var protocol: String,
var listen: String? = null,
var settings: InSettingsBean? = null,
var sniffing: SniffingBean? = null,
val streamSettings: Any? = null,
val allocate: Any? = null
) {
data class InSettingsBean(
var auth: String? = null,
var udp: Boolean? = null,
var userLevel: Int? = null,
var accounts: List<SocksAccountBean>? = null,
var name: String? = null,
var mtu: Int? = null
) {
data class SocksAccountBean(
var user: String = "",
var pass: String = ""
)
}
data class SniffingBean(
var enabled: Boolean,
val destOverride: ArrayList<String>,
val metadataOnly: Boolean? = null,
var routeOnly: Boolean? = null
)
}
data class OutboundBean(
var tag: String = "proxy",
var protocol: String,
var settings: OutSettingsBean? = null,
var streamSettings: StreamSettingsBean? = null,
val sendThrough: String? = null,
var mux: MuxBean? = MuxBean(false)
) {
data class OutSettingsBean(
/*Common */
var address: Any? = null,
var port: Int? = null,
var level: Int? = null,
var email: String? = null,
/*HTTP/SOCKS*/
var user: String? = null,
var pass: String? = null,
var headers: Map<String, String>? = null,
/*VMess/VLESS*/
var id: String? = null,
var security: String? = null,
var encryption: String? = null,
/*VLESS*/
var flow: String? = null,
/*Trojan/Shadowsocks*/
var password: String? = null,
/*Shadowsocks*/
var method: String? = null,
/*Hysteria/Hysteria2*/
var version: Int? = null,
/*Wireguard*/
var secretKey: String? = null,
val peers: List<WireGuardBean>? = null,
var reserved: List<Int>? = null,
var mtu: Int? = null,
var domainStrategy: String? = null,
) {
data class WireGuardBean(
var publicKey: String = "",
var preSharedKey: String? = null,
var endpoint: String = ""
)
}
data class StreamSettingsBean(
var network: String? = AppConfig.DEFAULT_NETWORK,
var security: String? = null,
var tcpSettings: TcpSettingsBean? = null,
var kcpSettings: KcpSettingsBean? = null,
var wsSettings: WsSettingsBean? = null,
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
var xhttpSettings: XhttpSettingsBean? = null,
var httpSettings: HttpSettingsBean? = null,
var tlsSettings: TlsSettingsBean? = null,
var quicSettings: QuicSettingBean? = null,
var realitySettings: TlsSettingsBean? = null,
var grpcSettings: GrpcSettingsBean? = null,
var hysteriaSettings: HysteriaSettingsBean? = null,
var finalmask: Any? = null,
val dsSettings: Any? = null,
var sockopt: SockoptBean? = null
) {
data class TcpSettingsBean(
var header: HeaderBean = HeaderBean(),
val acceptProxyProtocol: Boolean? = null
) {
data class HeaderBean(
var type: String = "none",
var request: RequestBean? = null,
var response: Any? = null
) {
data class RequestBean(
var path: List<String> = ArrayList(),
var headers: HeadersBean = HeadersBean(),
val version: String? = null,
val method: String? = null
) {
data class HeadersBean(
var Host: List<String>? = ArrayList(),
@SerializedName("User-Agent")
val userAgent: List<String>? = null,
@SerializedName("Accept-Encoding")
val acceptEncoding: List<String>? = null,
val Connection: List<String>? = null,
val Pragma: Any? = null
)
}
}
}
data class KcpSettingsBean(
var mtu: Int = 1350,
var tti: Int = 50,
var uplinkCapacity: Int = 12,
var downlinkCapacity: Int = 100,
var congestion: Boolean = false,
var readBufferSize: Int = 1,
var writeBufferSize: Int = 1
)
data class WsSettingsBean(
var path: String? = null,
var host: String? = null,
var headers: Map<String, String>? = null,
val maxEarlyData: Int? = null,
val useBrowserForwarding: Boolean? = null,
val acceptProxyProtocol: Boolean? = null
)
data class HttpupgradeSettingsBean(
var path: String? = null,
var host: String? = null,
val acceptProxyProtocol: Boolean? = null
)
data class XhttpSettingsBean(
var path: String? = null,
var host: String? = null,
var mode: String? = null,
var extra: Any? = null,
)
data class HttpSettingsBean(
var host: List<String> = ArrayList(),
var path: String? = null
)
data class SockoptBean(
var TcpNoDelay: Boolean? = null,
var tcpKeepAliveIdle: Int? = null,
var tcpFastOpen: Boolean? = null,
var tproxy: String? = null,
var mark: Int? = null,
var dialerProxy: String? = null,
var domainStrategy: String? = null,
var happyEyeballs: HappyEyeballsBean? = null,
)
data class HappyEyeballsBean(
var prioritizeIPv6: Boolean? = null,
var maxConcurrentTry: Int? = 4,
var tryDelayMs: Int? = 250, // ms
var interleave: Int? = null,
)
data class TlsSettingsBean(
var allowInsecure: Boolean = false,
var serverName: String? = null,
val alpn: List<String>? = null,
val minVersion: String? = null,
val maxVersion: String? = null,
val preferServerCipherSuites: Boolean? = null,
val cipherSuites: String? = null,
val fingerprint: String? = null,
val certificates: List<Any>? = null,
val disableSystemRoot: Boolean? = null,
val enableSessionResumption: Boolean? = null,
var echConfigList: String? = null,
var verifyPeerCertByName: String? = null,
var pinnedPeerCertSha256: String? = null,
// REALITY settings
var publicKey: String? = null,
var shortId: String? = null,
var spiderX: String? = null,
var mldsa65Verify: String? = null
)
data class QuicSettingBean(
var security: String = "none",
var key: String = "",
var header: HeaderBean = HeaderBean()
) {
data class HeaderBean(var type: String = "none")
}
data class GrpcSettingsBean(
var serviceName: String = "",
var authority: String? = null,
var multiMode: Boolean? = null,
var idle_timeout: Int? = null,
var health_check_timeout: Int? = null
)
data class HysteriaSettingsBean(
var version: Int,
var auth: String? = null
)
//https://xtls.github.io/config/transport.html#finalmaskobject
data class FinalMaskBean(
var tcp: List<MaskBean>? = null,
var udp: List<MaskBean>? = null,
var quicParams: QuicParamsBean? = null
) {
data class MaskBean(
var type: String,
var settings: MaskSettingsBean? = null
) {
data class MaskSettingsBean(
val password: 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,
// noise
val reset: Int? = null,
val noise: List<NoiseMaskBean>? = null
) {
data class NoiseMaskBean(
val rand: String? = null,
// val randRange: String? = null,
// val type: String? = null,
// val packet: String? = null,
val delay: String? = null,
)
}
}
data class QuicParamsBean(
var congestion: String? = null,
var brutalUp: String? = null,
var brutalDown: String? = null,
var udpHop: UdpHopBean? = null,
) {
// Nested data class for the udpHop JSON object
data class UdpHopBean(
var ports: String? = null,
var interval: String? = null
)
}
}
}
data class MuxBean(
var enabled: Boolean,
var concurrency: Int? = null,
var xudpConcurrency: Int? = null,
var xudpProxyUDP443: String? = null,
)
fun getServerAddress(): String? {
return if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
settings?.peers?.firstOrNull()?.endpoint?.substringBeforeLast(":")
} else {
settings?.address as? String
}
}
fun getServerPort(): Int? {
return if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
settings?.peers?.firstOrNull()?.endpoint?.substringAfterLast(":")?.toIntOrNull()
} else {
settings?.port
}
}
fun ensureSockopt(): StreamSettingsBean.SockoptBean {
val stream = streamSettings ?: StreamSettingsBean().also {
streamSettings = it
}
val sockopt = stream.sockopt ?: StreamSettingsBean.SockoptBean().also {
stream.sockopt = it
}
return sockopt
}
}
data class DnsBean(
var servers: ArrayList<Any>? = null,
var hosts: Map<String, Any>? = null,
val clientIp: String? = null,
val disableCache: Boolean? = null,
val queryStrategy: String? = null,
val enableParallelQuery: Boolean? = null,
val tag: String? = null
) {
data class ServersBean(
var address: String = "",
var port: Int? = null,
var domains: List<String>? = null,
var expectIPs: List<String>? = null,
val clientIp: String? = null,
val skipFallback: Boolean? = null,
val tag: String? = null,
)
}
data class RoutingBean(
var domainStrategy: String,
var domainMatcher: String? = null,
var rules: ArrayList<RulesBean>,
var balancers: List<BalancerBean>? = null
) {
data class RulesBean(
var type: String = "field",
var ip: List<String>? = null,
var domain: List<String>? = null,
var process: List<String>? = null,
var outboundTag: String? = null,
var balancerTag: String? = null,
var port: String? = null,
val sourcePort: String? = null,
val network: String? = null,
val source: List<String>? = null,
val user: List<String>? = null,
var inboundTag: List<String>? = null,
val protocol: List<String>? = null,
val attrs: String? = null,
val domainMatcher: String? = null
)
data class BalancerBean(
val tag: String,
val selector: List<String>,
val fallbackTag: String? = null,
val strategy: StrategyObject? = null
)
data class StrategyObject(
val type: String = "random", // "random" | "roundRobin" | "leastPing" | "leastLoad"
val settings: StrategySettingsObject? = null
)
data class StrategySettingsObject(
val expected: Int? = null,
val maxRTT: String? = null,
val tolerance: Double? = null,
val baselines: List<String>? = null,
val costs: List<CostObject>? = null
)
data class CostObject(
val regexp: Boolean = false,
val match: String,
val value: Double
)
}
data class PolicyBean(
var levels: Map<String, LevelBean>,
var system: Any? = null
) {
data class LevelBean(
var handshake: Int? = null,
var connIdle: Int? = null,
var uplinkOnly: Int? = null,
var downlinkOnly: Int? = null,
val statsUserUplink: Boolean? = null,
val statsUserDownlink: Boolean? = null,
var bufferSize: Int? = null
)
}
data class ObservatoryObject(
val subjectSelector: List<String>,
val probeUrl: String,
val probeInterval: String,
val enableConcurrency: Boolean = false
)
data class BurstObservatoryObject(
val subjectSelector: List<String>,
val pingConfig: PingConfigObject
) {
data class PingConfigObject(
val destination: String,
val connectivity: String? = null,
val httpMethod: String? = null,
val interval: String,
val sampling: Int,
val timeout: String? = null
)
}
data class FakednsBean(
var ipPool: String = "198.18.0.0/15",
var poolSize: Int = 10000
) // roughly 10 times smaller than total ip pool
fun getProxyOutbound(): OutboundBean? {
outbounds.forEach { outbound ->
EConfigType.entries.forEach {
if (outbound.protocol.equals(it.name, true)) {
return outbound
}
}
}
return null
}
fun getAllProxyOutbound(): List<OutboundBean> {
return outbounds.filter { outbound ->
EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
}
}
}
@@ -1,168 +0,0 @@
package com.v2ray.ang.dto
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.BalancerStrategyType
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType
@Suppress("PropertyName")
data class V2rayNShareItem(
// val IndexId: String?,
val ConfigType: Int?,
// val CoreType: Int?,
val ConfigVersion: Int?,
val Subid: String?,
val IsSub: Boolean?,
val PreSocksPort: Int?,
// val DisplayLog: Boolean?,
val Remarks: String?,
val Address: String?,
val Port: Int?,
val Password: String?,
val Username: String?,
val Network: String?,
val StreamSecurity: String?,
val AllowInsecure: String?,
val Sni: String?,
val Alpn: String?,
val Fingerprint: String?,
val PublicKey: String?,
val ShortId: String?,
val SpiderX: String?,
val Mldsa65Verify: String?,
// val MuxEnabled: Boolean?,
// val Cert: String?,
val CertSha: String?,
val EchConfigList: String?,
val VerifyPeerCertByName: String?,
val Finalmask: String?,
val ProtoExtraObj: V2rayNProtocolExtraShareItem?,
val TransportExtraObj: V2rayNTransportExtraShareItem?,
) {
data class V2rayNProtocolExtraShareItem(
val AlterId: Int?,
val VmessSecurity: String?,
val Flow: String?,
val VlessEncryption: String?,
val SsMethod: String?,
val WgPublicKey: String?,
val WgPresharedKey: String?,
val WgInterfaceAddress: String?,
val WgReserved: String?,
val WgMtu: Int?,
val SalamanderPass: String?,
val UpMbps: Int?,
val DownMbps: Int?,
val Ports: String?,
val HopInterval: String?,
val GroupType: String?,
val ChildItems: String?,
val SubChildItems: String?,
val Filter: String?,
val MultipleLoad: Int?,
)
data class V2rayNTransportExtraShareItem(
val RawHeaderType: String?,
val Host: String?,
val Path: String?,
val XhttpMode: String?,
val XhttpExtra: String?,
val GrpcAuthority: String?,
val GrpcServiceName: String?,
val GrpcMode: String?,
val KcpHeaderType: String?,
val KcpSeed: String?,
val KcpMtu: Int?,
)
fun toProfileItem(): ProfileItem {
val configType = when (ConfigType) {
1 -> EConfigType.VMESS
2 -> EConfigType.CUSTOM
3 -> EConfigType.SHADOWSOCKS
4 -> EConfigType.SOCKS
5 -> EConfigType.VLESS
6 -> EConfigType.TROJAN
7 -> EConfigType.HYSTERIA2
// 8 -> EConfigType.TUIC
9 -> EConfigType.WIREGUARD
10 -> EConfigType.HTTP
101 -> EConfigType.POLICYGROUP
102 -> EConfigType.PROXYCHAIN
else -> error("Unknown ConfigType: $ConfigType")
}
val network = if (configType == EConfigType.HYSTERIA2) "hysteria" else
when (Network) {
"raw" -> "tcp"
else -> NetworkType.fromString(Network).type
}
val profile = ProfileItem(
configType = configType,
remarks = Remarks.orEmpty(),
server = Address.orEmpty(),
serverPort = Port.toString(),
password = Password.orEmpty(),
method = when (configType) {
EConfigType.VMESS -> ProtoExtraObj?.VmessSecurity.orEmpty()
EConfigType.SHADOWSOCKS -> ProtoExtraObj?.SsMethod.orEmpty()
EConfigType.VLESS -> ProtoExtraObj?.VlessEncryption.orEmpty()
else -> null
},
flow = ProtoExtraObj?.Flow.orEmpty(),
username = Username.orEmpty(),
network = network,
headerType = when (network) {
NetworkType.TCP.type -> TransportExtraObj?.RawHeaderType
NetworkType.KCP.type -> TransportExtraObj?.KcpHeaderType
else -> null
},
host = TransportExtraObj?.Host,
path = TransportExtraObj?.Path,
seed = TransportExtraObj?.KcpSeed,
kcpMtu = TransportExtraObj?.KcpMtu,
mode = TransportExtraObj?.GrpcMode,
serviceName = TransportExtraObj?.GrpcServiceName,
authority = TransportExtraObj?.GrpcAuthority,
xhttpMode = TransportExtraObj?.XhttpMode,
xhttpExtra = TransportExtraObj?.XhttpExtra,
finalMask = Finalmask,
security = StreamSecurity,
sni = Sni,
alpn = Alpn,
fingerPrint = Fingerprint,
insecure = AllowInsecure?.toBoolean(),
echConfigList = EchConfigList,
verifyPeerCertByName = VerifyPeerCertByName,
pinnedCA256 = CertSha,
publicKey = PublicKey,
shortId = ShortId,
spiderX = SpiderX,
mldsa65Verify = Mldsa65Verify,
secretKey = if (configType == EConfigType.WIREGUARD) Password else null,
preSharedKey = ProtoExtraObj?.WgPresharedKey,
localAddress = ProtoExtraObj?.WgInterfaceAddress,
reserved = ProtoExtraObj?.WgReserved,
mtu = ProtoExtraObj?.WgMtu,
obfsPassword = ProtoExtraObj?.SalamanderPass,
portHopping = ProtoExtraObj?.Ports,
portHoppingInterval = ProtoExtraObj?.HopInterval,
bandwidthDown = ProtoExtraObj?.DownMbps?.takeIf { it > 0 }?.let { "${it}Mbps" },
bandwidthUp = ProtoExtraObj?.UpMbps?.takeIf { it > 0 }?.let { "${it}Mbps" },
policyGroupType = when (ProtoExtraObj?.MultipleLoad) {
2 -> BalancerStrategyType.RANDOM.policyGroupType
3 -> BalancerStrategyType.ROUND_ROBIN.policyGroupType
4 -> BalancerStrategyType.LEAST_LOAD.policyGroupType
else -> BalancerStrategyType.LEAST_PING.policyGroupType
},
// NOTE: not safe, suggest converting and rewriting
// policyGroupSubscriptionId = ProtoExtraObj?.SubChildItems,
policyGroupSubscriptionId = if (ProtoExtraObj?.SubChildItems == "self") "self" else null,
policyGroupFilter = ProtoExtraObj?.Filter,
// NOTE: proxyChainProfiles stores remarks, not IndexId
// proxyChainProfiles = ProtoExtraObj?.ChildItems,
)
// profile.description =
return profile
}
}
@@ -1,22 +0,0 @@
package com.v2ray.ang.dto
data class VmessQRCode(
var v: String = "",
var ps: String = "",
var add: String = "",
var port: String = "",
var id: String = "",
var aid: String = "0",
var scy: String = "",
var net: String = "",
var type: String = "",
var host: String = "",
var path: String = "",
var tls: String = "",
var sni: String = "",
var alpn: String = "",
var fp: String = "",
var insecure: String = "",
var vcn: String = "",
var pcs: String = ""
)
@@ -1,6 +0,0 @@
package com.v2ray.ang.dto.entities
data class AssetUrlCache(
val guid: String,
val assetUrl: AssetUrlItem
)
@@ -1,9 +0,0 @@
package com.v2ray.ang.dto.entities
data class AssetUrlItem(
var remarks: String = "",
var url: String = "",
val addedTime: Long = System.currentTimeMillis(),
var lastUpdated: Long = -1,
var locked: Boolean? = false,
)
@@ -1,112 +0,0 @@
package com.v2ray.ang.dto.entities
import com.v2ray.ang.AppConfig
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.util.Utils
data class ProfileItem(
val configVersion: Int = 4,
val configType: EConfigType,
var subscriptionId: String = "",
var addedTime: Long = System.currentTimeMillis(),
var remarks: String = "",
var description: String? = null,
var server: String? = null,
var serverPort: String? = null,
var password: String? = null,
var method: String? = null,
var flow: String? = null,
var username: String? = null,
var network: String? = null,
var headerType: String? = null,
var host: String? = null,
var path: String? = null,
var seed: String? = null,
var kcpMtu: Int? = null,
var kcpTti: Int? = null,
var quicSecurity: String? = null,
var quicKey: String? = null,
var mode: String? = null,
var serviceName: String? = null,
var authority: String? = null,
var xhttpMode: String? = null,
var xhttpExtra: String? = null,
var finalMask: String? = null,
var security: String? = null,
var sni: String? = null,
var alpn: String? = null,
var fingerPrint: String? = null,
var insecure: Boolean? = null,
var echConfigList: String? = null,
var verifyPeerCertByName: String? = null,
var pinnedCA256: String? = null,
var publicKey: String? = null,
var shortId: String? = null,
var spiderX: String? = null,
var mldsa65Verify: String? = null,
var secretKey: String? = null,
var preSharedKey: String? = null,
var localAddress: String? = null,
var reserved: String? = null,
var mtu: Int? = null,
var obfsPassword: String? = null,
var portHopping: String? = null,
var portHoppingInterval: String? = null,
@Deprecated("Use pinnedCA256")
var pinSHA256: String? = null,
var bandwidthDown: String? = null,
var bandwidthUp: String? = null,
var policyGroupType: String? = null,
var policyGroupSubscriptionId: String? = null,
var policyGroupFilter: String? = null,
var policyGroupTestOutbounds: Boolean? = null,
var policyGroupFallbackTag: String? = null,
var proxyChainProfiles: String? = null,
var browserDialerMode: String? = null,
) {
companion object {
fun create(configType: EConfigType): ProfileItem =
ProfileItem(configType = configType)
}
fun getServerAddressAndPort(): String {
if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
return "${AppConfig.LOOPBACK}:${AppConfig.PORT_SOCKS}"
}
return "${Utils.getIpv6Address(server)}:$serverPort"
}
/**
* Dedicated identity for "remove duplicate configurations".
*
* Ignores metadata that does not affect connection:
* - configVersion
* - subscriptionId
* - addedTime
* - remarks
* - description
*
* All other fields, including configType, are included in the comparison.
*
* Returns a copy; the caller must not modify it further.
*/
fun duplicateIdentity(): ProfileItem =
copy(
configVersion = 0,
subscriptionId = "",
addedTime = 0L,
remarks = "",
description = null
)
}
@@ -1,15 +0,0 @@
package com.v2ray.ang.dto.entities
data class RulesetItem(
var id: String = "",
var remarks: String? = "",
var ip: List<String>? = null,
var domain: List<String>? = null,
var process: List<String>? = null,
var outboundTag: String = "",
var port: String? = null,
var network: String? = null,
var protocol: List<String>? = null,
var enabled: Boolean = true,
var locked: Boolean? = false,
)
@@ -1,3 +0,0 @@
package com.v2ray.ang.dto.entities
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L)
@@ -1,7 +0,0 @@
package com.v2ray.ang.dto.entities
data class ServersCache(
val guid: String,
val profile: ProfileItem,
val testDelayMillis: Long = 0L,
)
@@ -1,6 +0,0 @@
package com.v2ray.ang.dto.entities
data class SubscriptionCache(
val guid: String,
val subscription: SubscriptionItem
)
@@ -1,18 +0,0 @@
package com.v2ray.ang.dto.entities
data class SubscriptionItem(
var remarks: String = "",
var url: String = "",
var enabled: Boolean = true,
val addedTime: Long = System.currentTimeMillis(),
var lastUpdated: Long = -1,
var autoUpdate: Boolean = false,
var updateInterval: Long = 1440, // in minutes, default to 24 hours
var prevProfile: String? = null,
var nextProfile: String? = null,
var filter: String? = null,
var allowInsecureUrl: Boolean = false,
var userAgent: String? = null,
var requestHeaders: String? = null,
)
@@ -1,9 +0,0 @@
package com.v2ray.ang.dto.entities
data class WebDavConfig(
val baseUrl: String,
val username: String? = null,
val password: String? = null,
val remoteBasePath: String = "/",
val timeoutSeconds: Long = 30
)
@@ -1,28 +0,0 @@
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).
* @param supportsObservatory Whether an observatory can health-filter this strategy.
*/
enum class BalancerStrategyType(
val policyGroupType: String,
val policyGroupTypeValue: String,
val requiresBurstObservatory: Boolean = false,
val requiresObservatory: Boolean = false,
val supportsObservatory: Boolean = false,
) {
LEAST_LOAD("leastLoad", "1", requiresBurstObservatory = true),
RANDOM("random", "2", supportsObservatory = true),
ROUND_ROBIN("roundRobin", "3", supportsObservatory = true),
LEAST_PING("leastPing", "", requiresObservatory = true); // default / else
companion object {
fun from(policyGroupType: String?): BalancerStrategyType =
entries.firstOrNull { it.policyGroupTypeValue == policyGroupType } ?: LEAST_PING
}
}
@@ -1,10 +0,0 @@
package com.v2ray.ang.enums
enum class BrowserDialerMode(val value: String) {
OKHTTP("OkHttp"),
WEBVIEW("WebView");
companion object {
fun from(value: String?): BrowserDialerMode? = entries.find { it.value == value }
}
}
@@ -1,8 +0,0 @@
package com.v2ray.ang.enums
/** Runtime type used during config assembly only. */
enum class CoreResolvedType {
NORMAL,
POLICYGROUP,
PROXYCHAIN,
}
@@ -1,24 +0,0 @@
package com.v2ray.ang.enums
import com.v2ray.ang.AppConfig
enum class EConfigType(val value: Int, val protocolScheme: String) {
VMESS(1, AppConfig.VMESS),
CUSTOM(2, AppConfig.CUSTOM),
SHADOWSOCKS(3, AppConfig.SHADOWSOCKS),
SOCKS(4, AppConfig.SOCKS),
VLESS(5, AppConfig.VLESS),
TROJAN(6, AppConfig.TROJAN),
WIREGUARD(7, AppConfig.WIREGUARD),
// TUIC(8, AppConfig.TUIC),
HYSTERIA2(9, AppConfig.HYSTERIA2),
HYSTERIA(900, AppConfig.HYSTERIA),
HTTP(10, AppConfig.HTTP),
POLICYGROUP(101, AppConfig.CUSTOM),
PROXYCHAIN(102, AppConfig.CUSTOM);
companion object {
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
}
}
@@ -1,57 +0,0 @@
package com.v2ray.ang.enums
import androidx.core.os.LocaleListCompat
import java.util.Locale
enum class Language(val code: String, val languageTag: String) {
AUTO("auto", ""),
ENGLISH("en", "en"),
CHINA("zh-rCN", "zh-CN"),
TRADITIONAL_CHINESE("zh-rTW", "zh-TW"),
VIETNAMESE("vi", "vi"),
RUSSIAN("ru", "ru"),
PERSIAN("fa", "fa"),
ARABIC("ar", "ar"),
BANGLA("bn", "bn"),
BAKHTIARI("bqi-rIR", "bqi-IR");
val locale: Locale?
get() = languageTag.takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag)
fun toLocaleList(): LocaleListCompat =
if (languageTag.isEmpty()) {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(languageTag)
}
companion object {
fun fromCode(code: String): Language {
return entries.find { it.code == code } ?: AUTO
}
fun fromLanguageTag(languageTag: String?): Language {
if (languageTag.isNullOrBlank()) return AUTO
val locale = Locale.forLanguageTag(languageTag)
return when (locale.language) {
"en" -> ENGLISH
"zh" -> if (
locale.script == "Hant" || locale.country in setOf("TW", "HK", "MO")
) {
TRADITIONAL_CHINESE
} else {
CHINA
}
"vi" -> VIETNAMESE
"ru" -> RUSSIAN
"fa" -> PERSIAN
"ar" -> ARABIC
"bn" -> BANGLA
"bqi" -> BAKHTIARI
else -> AUTO
}
}
}
}
@@ -1,19 +0,0 @@
package com.v2ray.ang.enums
enum class NetworkType(val type: String) {
TCP("tcp"),
KCP("kcp"),
WS("ws"),
HTTP_UPGRADE("httpupgrade"),
XHTTP("xhttp"),
HTTP("http"),
H2("h2"),
//QUIC("quic"),
GRPC("grpc"),
HYSTERIA("hysteria");
companion object {
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
}
}
@@ -1,22 +0,0 @@
package com.v2ray.ang.enums
/**
* Enum defining different notification channels.
* Each channel has a unique channelId, notificationId, and display name.
*/
enum class NotificationChannelType(
val channelId: String,
val channelName: String,
val notificationId: Int
) {
SUBSCRIPTION_UPDATE(
channelId = "subscription_update_channel",
channelName = "Subscription Update Service",
notificationId = 13
),
CORE_TEST(
channelId = "core_test_channel",
channelName = "Core Test Service",
notificationId = 12
)
}
@@ -1,42 +0,0 @@
package com.v2ray.ang.enums
import android.Manifest
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import com.v2ray.ang.R
/**
* Permission types used in the app, handling API level differences.
*/
enum class PermissionType {
/** Camera permission (used for scanning QR codes) */
CAMERA {
override fun getPermission(): String = Manifest.permission.CAMERA
},
/** 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 */
abstract fun getPermission(): String
/** Return the string resource for the human-readable permission label. */
@StringRes
fun getLabelRes(): Int {
return when (this) {
CAMERA -> R.string.permission_camera
POST_NOTIFICATIONS -> R.string.permission_notification
ACCESS_LOCAL_NETWORK -> R.string.permission_local_network
}
}
}
@@ -1,9 +0,0 @@
package com.v2ray.ang.enums
enum class RoutingType(val fileName: String) {
WHITE("custom_routing_white"),
BLACK("custom_routing_black"),
GLOBAL("custom_routing_global"),
WHITE_IRAN("custom_routing_white_iran"),
WHITE_RUSSIA("custom_routing_white_russia")
}
@@ -1,39 +0,0 @@
package com.v2ray.ang.enums
/**
* VPN interface address configuration enum class
* Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration.
* Each option provides client and router addresses to establish point-to-point VPN tunnels.
*/
enum class VpnInterfaceAddressConfig(
val displayName: String,
val ipv4Client: String,
val ipv4Router: String,
val ipv6Client: String,
val ipv6Router: String
) {
OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"),
OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"),
OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"),
OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"),
OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"),
OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"),
OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2");
companion object {
/**
* Retrieves the VPN interface address configuration based on the specified index.
*
* @param index The configuration index (0-based) corresponding to user selection
* @return The VpnInterfaceAddressConfig instance at the specified index,
* or OPTION_1 (default) if the index is out of bounds
*/
fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig {
return if (index in entries.toTypedArray().indices) {
entries[index]
} else {
OPTION_1 // Default to the first configuration
}
}
}
}
@@ -1,10 +0,0 @@
package com.v2ray.ang.extension
/**
* Moves an item to another index while preserving the relative order of the remaining items.
*/
internal fun <T> MutableList<T>.moveItem(fromIndex: Int, toIndex: Int): Boolean {
if (fromIndex !in indices || toIndex !in indices || fromIndex == toIndex) return false
add(toIndex, removeAt(fromIndex))
return true
}
@@ -1,53 +0,0 @@
package com.v2ray.ang.extension
/**
* Removes all whitespace from the string.
*
* @return The string without whitespace.
*/
fun String?.removeWhiteSpace(): String? = this?.replace(" ", "")
/**
* Returns null if the string is null or blank, otherwise returns the string itself.
*
* @return The string or null.
*/
fun String?.nullIfBlank(): String? = this?.takeIf { it.isNotBlank() }
/**
* Converts the string to a Long value, or returns 0 if the conversion fails.
*
* @return The Long value.
*/
fun String.toLongEx(): Long = toLongOrNull() ?: 0
/**
* Checks if the CharSequence is not null and not empty.
*
* @return True if the CharSequence is not null and not empty, false otherwise.
*/
fun CharSequence?.isNotNullEmpty(): Boolean = !this.isNullOrBlank()
fun String.concatUrl(vararg paths: String): String {
val builder = StringBuilder(this.trimEnd('/'))
paths.forEach { path ->
val trimmedPath = path.trim('/')
if (trimmedPath.isNotEmpty()) {
builder.append('/').append(trimmedPath)
}
}
return builder.toString()
}
/**
* Helper function to match text either by Regex or literal string.
*/
fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean = true): Boolean {
if (keyword.isNullOrEmpty()) {
return true
}
return regex?.containsMatchIn(this)
?: this.contains(keyword, ignoreCase = ignoreCase)
}
@@ -1,124 +0,0 @@
package com.v2ray.ang.extension
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import com.v2ray.ang.ui.compose.AppSnackbarManager
import com.v2ray.ang.ui.compose.ToastType
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toast(message: Int) {
val text = getString(message)
dispatchMessage(text, ToastType.NORMAL) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
}
}
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toast(message: CharSequence) {
dispatchMessage(message, ToastType.NORMAL) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toastSuccess(message: Int) {
val text = getString(message)
dispatchMessage(text, ToastType.SUCCESS) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
}
}
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toastSuccess(message: CharSequence) {
dispatchMessage(message, ToastType.SUCCESS) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toastError(message: Int) {
val text = getString(message)
dispatchMessage(text, ToastType.ERROR) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
}
}
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toastError(message: CharSequence) {
dispatchMessage(message, ToastType.ERROR) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
/**
* Shows an info toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toastInfo(message: Int) {
val text = getString(message)
dispatchMessage(text, ToastType.INFO) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
}
}
/**
* Shows an info toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toastInfo(message: CharSequence) {
dispatchMessage(message, ToastType.INFO) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
private inline fun runOnMain(crossinline block: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) {
block()
} else {
Handler(Looper.getMainLooper()).post { block() }
}
}
private inline fun dispatchMessage(
message: CharSequence,
type: ToastType,
long: Boolean = false,
crossinline fallback: () -> Unit
) {
val handledBySnackbar = AppSnackbarManager.show(
message = message,
type = type,
long = long
)
if (!handledBySnackbar) {
runOnMain { fallback() }
}
}
@@ -1,78 +0,0 @@
package com.v2ray.ang.extension
import android.content.Intent
import android.os.Build
import android.os.Bundle
import com.v2ray.ang.enums.EConfigType
import java.io.Serializable
import java.net.URI
import java.util.Locale
const val THRESHOLD = 1000L
const val DIVISOR = 1024.0
/**
* Converts a Long value to a speed string.
*
* @return The speed string.
*/
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
/**
* Converts a Long value to a traffic string.
*
* @return The traffic string.
*/
fun Long.toTrafficString(): String {
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
var size = this.toDouble()
var unitIndex = 0
while (size >= THRESHOLD && unitIndex < units.size - 1) {
size /= DIVISOR
unitIndex++
}
return String.format(Locale.getDefault(), "%.1f %s", size, units[unitIndex])
}
val URI.idnHost: String
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
/**
* Retrieves a serializable object from the Bundle.
*
* @param key The key of the serializable object.
* @return The serializable object, or null if not found.
*/
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
}
/**
* Retrieves a serializable object from the Intent.
*
* @param key The key of the serializable object.
* @return The serializable object, or null if not found.
*/
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
}
/**
* 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,27 +0,0 @@
package com.v2ray.ang.fmt
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
object CustomFmt : FmtBase() {
/**
* Parses a JSON string into a ProfileItem object.
*
* @param str the JSON string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem {
val config = ProfileItem.create(EConfigType.CUSTOM)
val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java)
val outbound = fullConfig?.getProxyOutbound()
config.remarks = fullConfig?.remarks ?: System.currentTimeMillis().toString()
config.server = outbound?.getServerAddress()
config.serverPort = outbound?.getServerPort()?.toString()
return config
}
}
@@ -1,175 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
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
import com.v2ray.ang.util.Utils
import java.net.URI
open class FmtBase {
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @param userInfo the user information to include in the URI
* @param dicQuery the query parameters to include in the URI
* @return the converted URI string
*/
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
val query = if (dicQuery != null)
"?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + Utils.encodeURIComponent(it.second) })
else ""
val url = String.format(
"%s@%s:%s",
Utils.encodeURIComponent(userInfo ?: ""),
Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
config.serverPort
)
return "${url}${query}#${Utils.encodeURIComponent(config.remarks)}"
}
/**
* Extracts query parameters from a URI.
*
* @param uri the URI to extract query parameters from
* @return a map of query parameters
*/
fun getQueryParam(uri: URI): Map<String, String> {
return uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.decodeURIComponent(v) } }
}
/**
* Populates a ProfileItem object with values from query parameters.
*
* @param config the ProfileItem object to populate
* @param queryParam the query parameters to use for populating the ProfileItem
*/
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>) {
config.network = queryParam["type"] ?: NetworkType.TCP.type
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.kcpMtu = queryParam["mtu"]?.toIntOrNull()
config.kcpTti = queryParam["tti"]?.toIntOrNull()
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
config.serviceName = queryParam["serviceName"]
config.authority = queryParam["authority"]
config.xhttpMode = queryParam["mode"]
config.xhttpExtra = queryParam["extra"]
config.finalMask = queryParam["fm"]
config.security = queryParam["security"]
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
config.security = null
}
// Support multiple possible query keys for allowInsecure like the C# implementation
val allowInsecureKeys = arrayOf("insecure", "allowInsecure", "allow_insecure")
config.insecure = when {
allowInsecureKeys.any { queryParam[it] == "1" } -> true
allowInsecureKeys.any { queryParam[it] == "0" } -> false
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"]
config.spiderX = queryParam["spx"]
config.mldsa65Verify = queryParam["pqv"]
config.flow = queryParam["flow"]
}
/**
* Creates a map of query parameters from a ProfileItem object.
*
* @param config the ProfileItem object to create query parameters from
* @return a map of query parameters
*/
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
val dicQuery = HashMap<String, String>()
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
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 }
config.shortId?.nullIfBlank()?.let { dicQuery["sid"] = it }
config.spiderX?.nullIfBlank()?.let { dicQuery["spx"] = it }
config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it }
config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it }
config.finalMask?.nullIfBlank()?.let { dicQuery["fm"] = it }
config.kcpMtu?.let { dicQuery["mtu"] = it.toString() }
config.kcpTti?.let { dicQuery["tti"] = it.toString() }
// Add two keys for compatibility: "insecure" and "allowInsecure"
if (config.security == AppConfig.TLS) {
val insecureFlag = if (config.insecure == true) "1" else "0"
dicQuery["insecure"] = insecureFlag
dicQuery["allowInsecure"] = insecureFlag
}
val networkType = NetworkType.fromString(config.network)
dicQuery["type"] = networkType.type
when (networkType) {
NetworkType.TCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.host?.nullIfBlank()?.let { dicQuery["host"] = it }
}
NetworkType.KCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.seed?.nullIfBlank()?.let { dicQuery["seed"] = it }
}
NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
config.host?.nullIfBlank()?.let { dicQuery["host"] = it }
config.path?.nullIfBlank()?.let { dicQuery["path"] = it }
}
NetworkType.XHTTP -> {
config.host?.nullIfBlank()?.let { dicQuery["host"] = it }
config.path?.nullIfBlank()?.let { dicQuery["path"] = it }
config.xhttpMode?.nullIfBlank()?.let { dicQuery["mode"] = it }
config.xhttpExtra?.nullIfBlank()?.let { dicQuery["extra"] = it }
}
NetworkType.HTTP, NetworkType.H2 -> {
dicQuery["type"] = "http"
config.host?.nullIfBlank()?.let { dicQuery["host"] = it }
config.path?.nullIfBlank()?.let { dicQuery["path"] = it }
}
// NetworkType.QUIC -> {
// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
// config.quicSecurity?.nullIfBlank()?.let { dicQuery["quicSecurity"] = it }
// config.quicKey?.nullIfBlank()?.let { dicQuery["key"] = it }
// }
NetworkType.GRPC -> {
config.mode?.nullIfBlank()?.let { dicQuery["mode"] = it }
config.authority?.nullIfBlank()?.let { dicQuery["authority"] = it }
config.serviceName?.nullIfBlank()?.let { dicQuery["serviceName"] = it }
}
else -> {}
}
return dicQuery
}
}
@@ -1,73 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
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.util.Utils
import java.net.URI
object Hysteria2Fmt : FmtBase() {
/**
* Parses a Hysteria2 URI string into a ProfileItem object.
*
* @param str the Hysteria2 URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem {
val config = ProfileItem.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.security = AppConfig.TLS
config.network = NetworkType.HYSTERIA.type
if (!uri.rawQuery.isNullOrEmpty()) {
val queryParam = getQueryParam(uri)
getItemFormQuery(config, queryParam)
config.security = queryParam["security"] ?: AppConfig.TLS
config.obfsPassword = queryParam["obfs-password"]
config.portHopping = queryParam["mport"]
config.pinnedCA256 = queryParam["pinSHA256"]
}
return config
}
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>()
config.security.let { if (it != null) dicQuery["security"] = it }
config.sni?.nullIfBlank()?.let { dicQuery["sni"] = it }
config.alpn?.nullIfBlank()?.let { dicQuery["alpn"] = it }
config.insecure.let { dicQuery["insecure"] = if (it == true) "1" else "0" }
if (config.obfsPassword.isNotNullEmpty()) {
dicQuery["obfs"] = "salamander"
dicQuery["obfs-password"] = config.obfsPassword.orEmpty()
}
if (config.portHopping.isNotNullEmpty()) {
dicQuery["mport"] = config.portHopping.orEmpty()
}
if (config.pinnedCA256.isNotNullEmpty()) {
dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty()
}
return toUri(config, config.password, dicQuery)
}
}
@@ -1,125 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
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.util.LogUtil
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt : FmtBase() {
/**
* Parses a Shadowsocks URI string into a ProfileItem object.
*
* @param str the Shadowsocks URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? {
return parseSip002(str) ?: parseLegacy(str)
}
/**
* Parses a SIP002 Shadowsocks URI string into a ProfileItem object.
*
* @param str the SIP002 Shadowsocks URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parseSip002(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null
if (uri.userInfo.isNullOrEmpty()) return null
config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
val result = if (uri.userInfo.contains(":")) {
uri.userInfo.split(":", limit = 2)
} else {
Utils.decode(uri.userInfo).split(":", limit = 2)
}
if (result.count() == 2) {
config.method = result.first()
config.password = result.last()
}
if (!uri.rawQuery.isNullOrEmpty()) {
val queryParam = getQueryParam(uri)
if (queryParam["plugin"]?.contains("obfs=http") == true) {
val queryPairs = HashMap<String, String>()
for (pair in queryParam["plugin"]?.split(";") ?: listOf()) {
val idx = pair.split("=")
if (idx.count() == 2) {
queryPairs.put(idx.first(), idx.last())
}
}
config.network = NetworkType.TCP.type
config.headerType = "http"
config.host = queryPairs["obfs-host"]
config.path = queryPairs["path"]
}
}
return config
}
/**
* Parses a legacy Shadowsocks URI string into a ProfileItem object.
*
* @param str the legacy Shadowsocks URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parseLegacy(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.decodeURIComponent(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result) ?: return null
config.server = match.groupValues[3].removeSurrounding("[", "]")
config.serverPort = match.groupValues[4]
config.password = match.groupValues[2]
config.method = match.groupValues[1].lowercase()
return config
}
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String {
val pw = "${config.method}:${config.password}"
return toUri(config, Utils.encode(pw, true), null)
}
}
@@ -1,58 +0,0 @@
package com.v2ray.ang.fmt
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
import com.v2ray.ang.util.Utils
import java.net.URI
object SocksFmt : FmtBase() {
/**
* Parses a Socks URI string into a ProfileItem object.
*
* @param str the Socks URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SOCKS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null
config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
if (uri.userInfo?.isEmpty() == false) {
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()
}
}
return config
}
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String {
val pw =
if (config.username.isNotNullEmpty())
"${config.username}:${config.password}"
else
":"
return toUri(config, Utils.encode(pw, true), null)
}
}
@@ -1,52 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
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.util.Utils
import java.net.URI
object TrojanFmt : FmtBase() {
/**
* Parses a Trojan URI string into a ProfileItem object.
*
* @param str the Trojan URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem {
val config = ProfileItem.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
if (uri.rawQuery.isNullOrEmpty()) {
config.network = NetworkType.TCP.type
config.security = AppConfig.TLS
config.insecure = false
} else {
val queryParam = getQueryParam(uri)
getItemFormQuery(config, queryParam)
config.security = queryParam["security"] ?: AppConfig.TLS
}
return config
}
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
return toUri(config, config.password, dicQuery)
}
}
@@ -1,22 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.V2rayNShareItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
object V2rayNFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
try {
val jsonBase64Payload = str.substringAfterLast('/')
val jsonPayload = Utils.decode(jsonBase64Payload)
val v2rayNShareItem = JsonUtil.fromJson(jsonPayload, V2rayNShareItem::class.java)
return v2rayNShareItem?.toProfileItem()
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to parse V2rayN format", e)
}
return null
}
}
@@ -1,48 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt : FmtBase() {
/**
* Parses a Vless URI string into a ProfileItem object.
*
* @param str the Vless URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.VLESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = queryParam["encryption"] ?: "none"
getItemFormQuery(config, queryParam)
return config
}
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
dicQuery["encryption"] = config.method ?: "none"
return toUri(config, config.password, dicQuery)
}
}
@@ -1,178 +0,0 @@
package com.v2ray.ang.fmt
import android.text.TextUtils
import com.v2ray.ang.AppConfig
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.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
import java.net.URI
object VmessFmt : FmtBase() {
/**
* Parses a Vmess string into a ProfileItem object.
*
* @param str the Vmess string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
return parseVmessStd(str)
}
val config = ProfileItem.create(EConfigType.VMESS)
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
LogUtil.w(AppConfig.TAG, "Toast decoding failed")
return null
}
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java) ?: return null
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
LogUtil.w(AppConfig.TAG, "Toast incorrect protocol")
return null
}
config.remarks = vmessQRCode.ps
config.server = vmessQRCode.add
config.serverPort = vmessQRCode.port
config.password = vmessQRCode.id
config.method =
if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
config.network = vmessQRCode.net
if (config.network.isNullOrEmpty()) {
config.network = NetworkType.TCP.type
}
config.headerType = vmessQRCode.type
config.host = vmessQRCode.host
config.path = vmessQRCode.path
when (NetworkType.fromString(config.network)) {
NetworkType.KCP -> {
config.seed = vmessQRCode.path
}
// NetworkType.QUIC -> {
// config.quicSecurity = vmessQRCode.host
// config.quicKey = vmessQRCode.path
// }
NetworkType.GRPC -> {
config.mode = vmessQRCode.type
config.serviceName = vmessQRCode.path
config.authority = vmessQRCode.host
}
else -> {}
}
config.security = vmessQRCode.tls
config.sni = vmessQRCode.sni
config.fingerPrint = vmessQRCode.fp
config.alpn = vmessQRCode.alpn
config.insecure = when (vmessQRCode.insecure) {
"1" -> true
"0" -> false
else -> false
}
config.verifyPeerCertByName = vmessQRCode.vcn
config.pinnedCA256 = vmessQRCode.pcs
return config
}
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String {
val vmessQRCode = VmessQRCode()
vmessQRCode.v = "2"
vmessQRCode.ps = config.remarks
vmessQRCode.add = config.server.orEmpty()
vmessQRCode.port = config.serverPort.orEmpty()
vmessQRCode.id = config.password.orEmpty()
vmessQRCode.scy = config.method.orEmpty()
vmessQRCode.aid = "0"
vmessQRCode.net = config.network.orEmpty()
vmessQRCode.type = config.headerType.orEmpty()
when (NetworkType.fromString(config.network)) {
NetworkType.KCP -> {
vmessQRCode.path = config.seed.orEmpty()
}
// NetworkType.QUIC -> {
// vmessQRCode.host = config.quicSecurity.orEmpty()
// vmessQRCode.path = config.quicKey.orEmpty()
// }
NetworkType.GRPC -> {
vmessQRCode.type = config.mode.orEmpty()
vmessQRCode.path = config.serviceName.orEmpty()
vmessQRCode.host = config.authority.orEmpty()
}
else -> {}
}
config.host?.nullIfBlank()?.let { vmessQRCode.host = it }
config.path?.nullIfBlank()?.let { vmessQRCode.path = it }
vmessQRCode.tls = config.security.orEmpty()
vmessQRCode.sni = config.sni.orEmpty()
vmessQRCode.fp = config.fingerPrint.orEmpty()
vmessQRCode.alpn = config.alpn.orEmpty()
vmessQRCode.insecure = when (config.insecure) {
true -> "1"
false -> "0"
else -> ""
}
vmessQRCode.vcn = config.verifyPeerCertByName.orEmpty()
vmessQRCode.pcs = config.pinnedCA256.orEmpty()
val json = JsonUtil.toJson(vmessQRCode)
return Utils.encode(json)
}
/**
* Parses a standard Vmess URI string into a ProfileItem object.
*
* @param str the standard Vmess URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parseVmessStd(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = AppConfig.DEFAULT_SECURITY
getItemFormQuery(config, queryParam)
return config
}
}
@@ -1,124 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
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
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.Utils
import java.net.URI
object WireguardFmt : FmtBase() {
/**
* Parses a URI string into a ProfileItem object.
*
* @param str the URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.secretKey = uri.userInfo.orEmpty()
config.localAddress = queryParam["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
config.publicKey = queryParam["publickey"].orEmpty()
config.preSharedKey = queryParam["presharedkey"]?.nullIfBlank()
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = queryParam["reserved"] ?: "0,0,0"
return config
}
/**
* Parses a Wireguard configuration file string into a ProfileItem object.
*
* @param str the Wireguard configuration file string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parseWireguardConfFile(str: String): ProfileItem {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val interfaceParams: MutableMap<String, String> = mutableMapOf()
val peerParams: MutableMap<String, String> = mutableMapOf()
var currentSection: String? = null
str.lines().forEach { line ->
val trimmedLine = line.trim()
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
return@forEach
}
when {
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
else -> {
if (currentSection != null) {
val parts = trimmedLine.split("=", limit = 2).map { it.trim() }
if (parts.size == 2) {
val key = parts[0].lowercase()
val value = parts[1]
when (currentSection) {
"Interface" -> interfaceParams[key] = value
"Peer" -> peerParams[key] = value
}
}
}
}
}
}
config.secretKey = interfaceParams["privatekey"].orEmpty()
config.remarks = System.currentTimeMillis().toString()
config.localAddress = interfaceParams["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.publicKey = peerParams["publickey"].orEmpty()
config.preSharedKey = peerParams["presharedkey"]?.nullIfBlank()
val endpoint = peerParams["endpoint"].orEmpty()
val endpointParts = endpoint.split(":", limit = 2)
if (endpointParts.size == 2) {
config.server = endpointParts[0]
config.serverPort = endpointParts[1]
} else {
config.server = endpoint
config.serverPort = ""
}
config.reserved = peerParams["reserved"] ?: "0,0,0"
return config
}
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] = config.publicKey.orEmpty()
if (config.reserved != null) {
dicQuery["reserved"] = config.reserved.removeWhiteSpace().orEmpty()
}
dicQuery["address"] = config.localAddress.removeWhiteSpace().orEmpty()
if (config.mtu != null) {
dicQuery["mtu"] = config.mtu.toString()
}
if (config.preSharedKey != null) {
dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().orEmpty()
}
return toUri(config, config.secretKey, dicQuery)
}
}
@@ -1,714 +0,0 @@
package com.v2ray.ang.handler
import android.content.Context
import android.graphics.Bitmap
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.core.CoreConfigManager
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
import com.v2ray.ang.fmt.Hysteria2Fmt
import com.v2ray.ang.fmt.ShadowsocksFmt
import com.v2ray.ang.fmt.SocksFmt
import com.v2ray.ang.fmt.TrojanFmt
import com.v2ray.ang.fmt.V2rayNFmt
import com.v2ray.ang.fmt.VlessFmt
import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils
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,
AppConfig.V2RAYNFMTS to V2rayNFmt::parse
)
}
/**
* Shares the configuration to the clipboard.
*
* @param context The context.
* @param guid The GUID of the configuration.
* @return The result code.
*/
fun share2Clipboard(context: Context, guid: String): Int {
try {
val conf = shareConfig(guid)
if (TextUtils.isEmpty(conf)) {
return -1
}
Utils.setClipboard(context, conf)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to share config to clipboard", e)
return -1
}
return 0
}
/**
* Shares non-custom configurations to the clipboard.
*
* @param context The context.
* @param serverList The list of server GUIDs.
* @return The number of configurations shared.
*/
fun shareNonCustomConfigsToClipboard(context: Context, serverList: List<String>): Int {
try {
val sb = StringBuilder()
for (guid in serverList) {
val url = shareConfig(guid)
if (TextUtils.isEmpty(url)) {
continue
}
sb.append(url)
sb.appendLine()
}
if (sb.count() > 0) {
Utils.setClipboard(context, sb.toString())
}
return sb.lines().count() - 1
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
return -1
}
}
/**
* Shares the configuration as a QR code.
*
* @param guid The GUID of the configuration.
* @return The QR code bitmap.
*/
fun share2QRCode(guid: String): Bitmap? {
try {
val conf = shareConfig(guid)
if (TextUtils.isEmpty(conf)) {
return null
}
return QRCodeDecoder.createQRCode(conf)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to share config as QR code", e)
return null
}
}
/**
* Shares the full content of the configuration to the clipboard.
*
* @param context The context.
* @param guid The GUID of the configuration.
* @return The result code.
*/
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
try {
if (guid == null) return -1
val result = CoreConfigManager.getV2rayConfig(context, guid)
if (result.status) {
Utils.setClipboard(context, result.content)
} else {
return -1
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
return -1
}
return 0
}
/**
* Shares the configuration.
*
* @param guid The GUID of the configuration.
* @return The configuration string.
*/
private fun shareConfig(guid: String): String {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ""
return config.configType.protocolScheme + when (config.configType) {
EConfigType.VMESS -> VmessFmt.toUri(config)
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
EConfigType.SOCKS -> SocksFmt.toUri(config)
EConfigType.VLESS -> VlessFmt.toUri(config)
EConfigType.TROJAN -> TrojanFmt.toUri(config)
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
else -> {}
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
return ""
}
}
/**
* Imports a batch of configurations.
*
* @param server The server string.
* @param subid The subscription ID.
* @param append Whether to append the configurations.
* @return A pair containing the number of configurations and subscriptions imported.
*/
fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> {
var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) {
count = parseBatchConfig(server, subid, append)
}
if (count <= 0) {
count = parseCustomConfigServer(server, subid, append)
}
var countSub = parseBatchSubscription(server)
if (countSub <= 0) {
countSub = parseBatchSubscription(Utils.decode(server))
}
if (countSub > 0) {
updateConfigViaSubAll()
}
return count to countSub
}
/**
* Parses a batch of subscriptions.
*
* @param servers The servers string.
* @return The number of subscriptions parsed.
*/
private fun parseBatchSubscription(servers: String?): Int {
try {
if (servers == null) {
return 0
}
var count = 0
servers.lines()
.distinct()
.forEach { str ->
if (Utils.isValidSubUrl(str)) {
count += importUrlAsSubscription(str)
}
}
return count
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to parse batch subscription", e)
}
return 0
}
/**
* Parses a batch of configurations.
*
* @param servers The servers string.
* @param subid The subscription ID.
* @param append Whether to append the configurations.
* @return The number of configurations parsed.
*/
private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
try {
if (servers == null) {
return 0
}
// Find the currently selected server that belongs to the same subscription before replacement.
val removedSelected = getRemovedSelectedProfile(subid, append)
val subItem = MmkvManager.decodeSubscription(subid)
// Parse all configs first (no I/O during parsing)
val configs = mutableListOf<ProfileItem>()
servers.lines()
.distinct()
.reversed()
.forEach {
val config = parseConfig(it, subid, subItem)
if (config != null) {
configs.add(config)
}
}
// Batch save all parsed configs (only one serverList read/write)
if (configs.isNotEmpty()) {
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
val keyToProfile = batchSaveConfigs(configs, subid)
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
matchKey?.let { MmkvManager.setSelectServer(it) }
}
return configs.size
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to parse batch config", e)
}
return 0
}
/**
* Batch save configurations to reduce serverList read/write operations.
* Reads serverList once, saves all configs, then writes serverList once.
*
* @param configs The list of ProfileItem to save.
* @param subid The subscription ID.
* @return Map of generated keys to their corresponding ProfileItem.
*/
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): Map<String, ProfileItem> {
val keyToProfile = mutableMapOf<String, ProfileItem>()
// Read serverList once
val serverList = MmkvManager.decodeServerList(subid)
configs.forEach { config ->
val key = Utils.getUuid()
// Save profile directly without updating serverList
MmkvManager.encodeProfileDirect(key, JsonUtil.toJson(config))
if (!serverList.contains(key)) {
serverList.add(0, key)
}
keyToProfile[key] = config
}
// Write serverList once
MmkvManager.encodeServerList(serverList, subid)
return keyToProfile
}
/**
* Finds a matched profile key from the given key-profile map using multi-level matching.
* Matching priority (from highest to lowest):
* 1. Exact match: server + port + password
* 2. Match by remarks (exact match)
* 3. Match by server + port
* 4. Match by server only
*
* @param keyToProfile Map of server keys to their ProfileItem
* @param target Target profile to match
* @return Matched key or null
*/
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
if (keyToProfile.isEmpty()) 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()) {
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.remarks, target.remarks)
}?.key?.let { return it }
}
// Level 2: Exact match (server + port + password)
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.server, target.server) &&
isSameText(saved.serverPort, target.serverPort) &&
isSameText(saved.password, target.password)
}?.key?.let { return it }
// Level 3: Match by server + port
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.server, target.server) &&
isSameText(saved.serverPort, target.serverPort)
}?.key?.let { return it }
// Level 4: Match by server only
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.server, target.server)
}?.key?.let { return it }
// 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 }
}
/**
* Case-insensitive trimmed string comparison.
*
* @param left First string
* @param right Second string
* @return True if both are non-empty and equal (case-insensitive, trimmed)
*/
private fun isSameText(left: String?, right: String?): Boolean {
if (left.isNullOrBlank() || right.isNullOrBlank()) return false
return left.trim().equals(right.trim(), ignoreCase = true)
}
/**
* Parses a custom configuration server.
*
* @param server The server string.
* @param subid The subscription ID.
* @param append Whether to append the configurations.
* @return The number of configurations parsed.
*/
private fun parseCustomConfigServer(server: String?, subid: String, append: Boolean): Int {
if (server == null) {
return 0
}
if (server.contains("inbounds")
&& server.contains("outbounds")
&& server.contains("routing")
) {
try {
val serverList: Array<Any> =
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) {
LogUtil.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
}
try {
// For compatibility
val config = CustomFmt.parse(server) ?: return 0
config.subscriptionId = subid
config.description = generateDescription(config)
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
}
return 0
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
try {
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
config.description = generateDescription(config)
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
}
return 0
} else {
return 0
}
}
/**
* Parses the configuration from a QR code or string.
* Only parses and returns ProfileItem, does not save.
*
* @param str The configuration string.
* @param subid The subscription ID.
* @param subItem The subscription item.
* @return The parsed ProfileItem or null if parsing fails or filtered out.
*/
private fun parseConfig(
str: String?,
subid: String,
subItem: SubscriptionItem?
): ProfileItem? {
try {
if (str == null || TextUtils.isEmpty(str)) {
return null
}
val config = configFmtParsers.firstNotNullOfOrNull { (scheme, parser) ->
if (str.startsWith(scheme)) parser(str) else null
}
if (config == null) {
return null
}
// Apply filter
if (subItem?.filter.isNotNullEmpty() && config.remarks.isNotNullEmpty()) {
val matched = Regex(pattern = subItem?.filter.orEmpty())
.containsMatchIn(input = config.remarks)
if (!matched) return null
}
config.subscriptionId = subid
config.description = generateDescription(config)
if (str.startsWith(AppConfig.V2RAYNFMTS, ignoreCase = true)
&& config.policyGroupSubscriptionId == "self"
) {
config.policyGroupSubscriptionId = subid
}
return config
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to parse config", e)
return null
}
}
/**
* Updates the configuration via all subscriptions.
*
* @return Detailed result of the subscription update operation.
*/
fun updateConfigViaSubAll(): SubscriptionUpdateResult {
return try {
val subscriptions = MmkvManager.decodeSubscriptions()
subscriptions.fold(SubscriptionUpdateResult()) { acc, subscription ->
acc + updateConfigViaSub(subscription)
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
SubscriptionUpdateResult()
}
}
/**
* Updates the configuration via a subscription.
*
* @param it The subscription item.
* @return Subscription update result.
*/
fun updateConfigViaSub(it: SubscriptionCache): SubscriptionUpdateResult {
try {
// Check if disabled
if (!it.subscription.enabled) {
return SubscriptionUpdateResult(skipCount = 1)
}
// Validate subscription info
if (TextUtils.isEmpty(it.guid)
|| TextUtils.isEmpty(it.subscription.remarks)
|| TextUtils.isEmpty(it.subscription.url)
) {
return SubscriptionUpdateResult(skipCount = 1)
}
val url = HttpUtil.toIdnUrl(it.subscription.url)
if (!Utils.isValidUrl(url)) {
return SubscriptionUpdateResult(failureCount = 1)
}
if (!it.subscription.allowInsecureUrl) {
if (!Utils.isValidSubUrl(url)) {
return SubscriptionUpdateResult(failureCount = 1)
}
}
LogUtil.i(AppConfig.TAG, url)
val userAgent = it.subscription.userAgent
val requestHeaders = it.subscription.requestHeaders
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var configText = try {
val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(
UrlContentRequest(
url = url,
userAgent = userAgent,
requestHeaders = requestHeaders,
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(
UrlContentRequest(
url = url,
userAgent = userAgent,
requestHeaders = requestHeaders
)
)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
""
}
}
if (configText.isEmpty()) {
return SubscriptionUpdateResult(failureCount = 1)
}
val count = parseConfigViaSub(configText, it.guid, false)
if (count > 0) {
it.subscription.lastUpdated = System.currentTimeMillis()
MmkvManager.encodeSubscription(it.guid, it.subscription)
LogUtil.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs")
return SubscriptionUpdateResult(
configCount = count,
successCount = 1
)
} else {
// Got response but no valid configs parsed
return SubscriptionUpdateResult(failureCount = 1)
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to update config via subscription", e)
return SubscriptionUpdateResult(failureCount = 1)
}
}
/**
* Removes invalid server configurations for a subscription.
*
* @param subId The subscription ID.
*/
fun removeInvalidServer(subId: String) {
val serverList = MmkvManager.decodeServerList(subId)
val invalidServers = serverList.filter {
val aff = MmkvManager.decodeServerAffiliationInfo(it)
aff != null && aff.testDelayMillis < 0L
}
MmkvManager.removeServers(invalidServers, subId)
}
/**
* Sorts servers by test results for a subscription.
*
* @param subId The subscription ID.
*/
fun sortByTestResultsForSub(subId: String) {
val serverList = MmkvManager.decodeServerList(subId)
if (serverList.isEmpty()) return
val sorted = serverList
.map { guid ->
val delay =
MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
guid to if (delay <= 0L) Long.MAX_VALUE else delay
}
.sortedBy { it.second }
.map { it.first }
.toMutableList()
MmkvManager.encodeServerList(sorted, subId)
}
/**
* Parses the configuration via a subscription.
*
* @param server The server string.
* @param subid The subscription ID.
* @param append Whether to append the configurations.
* @return The number of configurations parsed.
*/
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) {
count = parseBatchConfig(server, subid, append)
}
if (count <= 0) {
count = parseCustomConfigServer(server, subid, append)
}
return count
}
/**
* Imports a URL as a subscription.
*
* @param url The URL.
* @return The number of subscriptions imported.
*/
private fun importUrlAsSubscription(url: String): Int {
val subscriptions = MmkvManager.decodeSubscriptions()
subscriptions.forEach {
if (it.subscription.url == url) {
return 0
}
}
val uri = URI(Utils.fixIllegalUrl(url))
val subItem = SubscriptionItem()
subItem.remarks = uri.fragment ?: "import sub"
subItem.url = url
MmkvManager.encodeSubscription("", subItem)
return 1
}
/** Generates a description for the profile.
*
* @param profile The profile item.
* @return The generated description.
*/
fun generateDescription(profile: ProfileItem): String {
// Hide xxx:xxx:***/xxx.xxx.xxx.***
val server = profile.server
val port = profile.serverPort
if (server.isNullOrBlank() && port.isNullOrBlank()) return ""
val addrPart = server?.let {
if (it.contains(":"))
it.split(":").take(2).joinToString(":", postfix = ":***")
else
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
} ?: ""
return "$addrPart : ${port ?: ""}"
}
}
@@ -1,140 +0,0 @@
package com.v2ray.ang.handler
import android.app.LocaleManager
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.LocaleManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.os.ConfigurationCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.enums.Language
/**
* Keeps the legacy in-app language preference synchronized with Android's per-app locale APIs.
*/
object AppLocaleManager {
/**
* Migrates the existing MMKV language preference and restores it before the first activity.
*/
fun initialize(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Api33.prepareMigration(context)
} else {
initializeCompat(context)
}
}
/**
* Completes the one-time handoff after AppCompat has attached the activity context.
*/
fun onActivityCreated(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Api33.completeMigration(context)
} else {
syncLegacyPreference(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag())
}
}
/**
* Applies a language selected in the app and lets AppCompat recreate the current activity.
*/
fun setApplicationLanguage(languageCode: String) {
val language = Language.fromCode(languageCode)
persistLegacyPreference(language)
AppCompatDelegate.setApplicationLocales(language.toLocaleList())
}
/**
* Returns a context suitable for resource access outside AppCompatActivity.
*/
fun localizedContext(context: Context): Context {
val localizedContext = ContextCompat.getContextForLanguage(context)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || localizedContext !== context) {
return localizedContext
}
// During the one-time upgrade from the old picker, AppCompat has not written its locale
// record yet. Use the still-current MMKV value for non-activity contexts on that launch.
val language = storedLanguage()
language.locale ?: return context
val configuration = Configuration(context.resources.configuration)
ConfigurationCompat.setLocales(configuration, language.toLocaleList())
return context.createConfigurationContext(configuration)
}
private fun initializeCompat(context: Context) {
val storedLocales = LocaleManagerCompat.getApplicationLocales(context)
if (storedLocales.isEmpty) {
val legacyLanguage = storedLanguage()
if (legacyLanguage != Language.AUTO) {
AppCompatDelegate.setApplicationLocales(legacyLanguage.toLocaleList())
}
} else {
syncLegacyPreference(storedLocales.get(0)?.toLanguageTag())
}
}
private fun storedLanguage(): Language = Language.fromCode(
MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
)
private fun syncLegacyPreference(languageTag: String?) {
persistLegacyPreference(Language.fromLanguageTag(languageTag))
}
private fun persistLegacyPreference(language: Language) {
val current = MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE)
if (current != language.code) {
MmkvManager.encodeSettings(AppConfig.PREF_LANGUAGE, language.code)
SettingsChangeManager.notifySettingChanged(AppConfig.PREF_LANGUAGE)
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private object Api33 {
fun prepareMigration(context: Context) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_APP_LOCALE_MIGRATED, false)) {
return
}
val localeManager = context.getSystemService(LocaleManager::class.java)
val applicationLocales = localeManager.applicationLocales
if (!applicationLocales.isEmpty) {
syncLegacyPreference(applicationLocales.get(0)?.toLanguageTag())
return
}
val legacyLanguage = storedLanguage()
if (legacyLanguage != Language.AUTO) {
// Make application and service contexts correct even before the first activity.
localeManager.applicationLocales = android.os.LocaleList(
requireNotNull(legacyLanguage.locale)
)
}
}
fun completeMigration(context: Context) {
val localeManager = context.getSystemService(LocaleManager::class.java)
if (!MmkvManager.decodeSettingsBool(AppConfig.PREF_APP_LOCALE_MIGRATED, false)) {
val frameworkLanguage = Language.fromLanguageTag(
localeManager.applicationLocales.get(0)?.toLanguageTag()
)
val language = frameworkLanguage.takeUnless { it == Language.AUTO }
?: storedLanguage()
// Android's migration guidance requires this one-time AppCompat handoff after
// Activity.onCreate(), including on Android 13 and newer.
AppCompatDelegate.setApplicationLocales(language.toLocaleList())
persistLegacyPreference(language)
MmkvManager.encodeSettings(AppConfig.PREF_APP_LOCALE_MIGRATED, true)
return
}
syncLegacyPreference(localeManager.applicationLocales.get(0)?.toLanguageTag())
}
}
}
@@ -1,79 +0,0 @@
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) }
}
}
@@ -1,811 +0,0 @@
package com.v2ray.ang.handler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
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.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
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
object MmkvManager {
//region private
private const val ID_MAIN = "MAIN"
private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
private const val ID_SERVER_RAW = "SERVER_RAW"
private const val ID_SERVER_AFF = "SERVER_AFF"
private const val ID_SUB = "SUB"
private const val ID_ASSET = "ASSET"
private const val ID_SETTING = "SETTING"
private const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
private const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
private const val KEY_SUB_SERVER_PREFIX = "SUB_SERVERS_"
private const val KEY_SUB_IDS = "SUB_IDS"
private const val KEY_WEBDAV_CONFIG = "WEBDAV_CONFIG"
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
//endregion
//region Server
/**
* Reads the legacy server list from KEY_ANG_CONFIGS for migration.
* This method is for migration purposes only.
*
* @return The JSON string of legacy server list, or null if not exists.
*/
fun readLegacyServerList(): String? {
return mainStorage.decodeString(KEY_ANG_CONFIGS)
}
/**
* Gets the selected server GUID.
*
* @return The selected server GUID.
*/
fun getSelectServer(): String? {
return mainStorage.decodeString(KEY_SELECTED_SERVER)
}
/**
* Sets the selected server GUID.
*
* @param guid The server GUID.
*/
fun setSelectServer(guid: String) {
mainStorage.encode(KEY_SELECTED_SERVER, guid)
}
/**
* Encodes the server list for a given subscription.
* Saves to the subscription's serverList (including default subscription for ungrouped servers).
*
* @param serverList The list of server GUIDs.
* @param subscriptionId The subscription ID.
*/
fun encodeServerList(serverList: MutableList<String>, subscriptionId: String) {
val subId = getSubscriptionId(subscriptionId)
val key = "$KEY_SUB_SERVER_PREFIX$subId"
mainStorage.encode(key, JsonUtil.toJson(serverList))
}
/**
* Decodes the server list for a given subscription.
* If subscriptionId is empty, returns ungrouped servers.
* Otherwise, returns servers from the specified subscription's serverList.
*
* @param subscriptionId The subscription ID.
* @return The list of server GUIDs.
*/
fun decodeServerList(subscriptionId: String): MutableList<String> {
val subId = getSubscriptionId(subscriptionId)
val key = "$KEY_SUB_SERVER_PREFIX$subId"
val json = mainStorage.decodeString(key)
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
JsonUtil.fromJsonSafe(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
}
}
/**
* Decodes all server list (merged from all subscriptions including default subscription).
* Use this when you need the complete server list.
*
* @return The list of all server GUIDs.
*/
fun decodeAllServerList(): MutableList<String> {
val allServers = mutableListOf<String>()
val subsList = decodeSubsList()
// If DEFAULT_SUBSCRIPTION_ID is not in the subscriptions list, add its servers
if (!subsList.contains(DEFAULT_SUBSCRIPTION_ID)) {
allServers.addAll(decodeServerList(DEFAULT_SUBSCRIPTION_ID))
}
// Add servers from all subscriptions
subsList.forEach { guid ->
allServers.addAll(decodeServerList(guid))
}
return allServers
}
/**
* Decodes the server configuration.
*
* @param guid The server GUID.
* @return The server configuration.
*/
fun decodeServerConfig(guid: String): ProfileItem? {
if (guid.isBlank()) {
return null
}
val json = profileFullStorage.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return JsonUtil.fromJsonSafe(json, ProfileItem::class.java)
}
/**
* Encodes the server configuration.
*
* @param guid The server GUID.
* @param config The server configuration.
* @return The server GUID.
*/
fun encodeServerConfig(guid: String, config: ProfileItem): String {
val key = guid.ifBlank { Utils.getUuid() }
profileFullStorage.encode(key, JsonUtil.toJson(config))
// Use default subscription for servers without subscription
val subId = getSubscriptionId(config.subscriptionId)
val serverList = decodeServerList(subId)
if (!serverList.contains(key)) {
serverList.add(0, key)
encodeServerList(serverList, subId)
if (getSelectServer().isNullOrBlank()) {
mainStorage.encode(KEY_SELECTED_SERVER, key)
}
}
return key
}
/**
* Encodes the server configuration directly without updating serverList.
*
* @param key The server GUID.
* @param configJson The server configuration JSON string.
*/
fun encodeProfileDirect(key: String, configJson: String) {
profileFullStorage.encode(key, configJson)
}
/**
* Removes the server configuration.
*
* @param guid The server GUID.
*/
fun removeServer(guid: String) {
if (guid.isBlank()) {
return
}
// Get config to determine which subscription to update
val config = decodeServerConfig(guid)
val subId = getSubscriptionId(config?.subscriptionId)
// Remove from appropriate server list
val serverList = decodeServerList(subId)
serverList.remove(guid)
encodeServerList(serverList, subId)
// Clean up storage
if (getSelectServer() == guid) {
mainStorage.remove(KEY_SELECTED_SERVER)
}
profileFullStorage.remove(guid)
serverAffStorage.remove(guid)
}
/**
* Removes the server configurations via subscription ID.
*
* @param subscriptionId The subscription ID.
*/
fun removeServerViaSubid(subscriptionId: String?) {
val subId = getSubscriptionId(subscriptionId)
val serverList = decodeServerList(subId)
// Remove all servers in the list
serverList.forEach { guid ->
if (getSelectServer() == guid) {
mainStorage.remove(KEY_SELECTED_SERVER)
}
profileFullStorage.remove(guid)
serverAffStorage.remove(guid)
}
serverList.clear()
encodeServerList(serverList, subId)
}
/**
* Removes multiple server configurations from a subscription.
*
* @param guids The list of server GUIDs.
* @param subscriptionId The subscription ID.
*/
fun removeServers(guids: List<String>, subscriptionId: String) {
if (guids.isEmpty()) return
val subId = getSubscriptionId(subscriptionId)
val serverList = decodeServerList(subId)
if (serverList.removeAll(guids)) {
encodeServerList(serverList, subId)
}
val selectedServer = getSelectServer()
guids.forEach { guid ->
if (selectedServer == guid) {
mainStorage.remove(KEY_SELECTED_SERVER)
}
profileFullStorage.remove(guid)
serverAffStorage.remove(guid)
serverRawStorage.remove(guid)
}
}
/**
* Decodes the server affiliation information.
*
* @param guid The server GUID.
* @return The server affiliation information.
*/
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
if (guid.isBlank()) {
return null
}
val json = serverAffStorage.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return JsonUtil.fromJsonSafe(json, ServerAffiliationInfo::class.java)
}
/**
* Encodes the server test delay in milliseconds.
*
* @param guid The server GUID.
* @param testResult The test delay in milliseconds.
*/
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
if (guid.isBlank()) {
return
}
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
aff.testDelayMillis = testResult
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
}
/**
* Clears all test delay results.
*
* @param keys The list of server GUIDs.
*/
fun clearAllTestDelayResults(keys: List<String>?) {
keys?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff ->
aff.testDelayMillis = 0
serverAffStorage.encode(key, JsonUtil.toJson(aff))
}
}
}
/**
* Removes all server configurations.
*
* @return The number of server configurations removed.
*/
fun removeAllServer(): Int {
val count = profileFullStorage.allKeys()?.count() ?: 0
profileFullStorage.clearAll()
serverAffStorage.clearAll()
serverRawStorage.clearAll()
decodeSubscriptions().forEach { sub ->
encodeServerList(mutableListOf(), sub.guid)
}
return count
}
/**
* Removes invalid server configurations.
*
* @param guid The server GUID.
* @return The number of server configurations removed.
*/
fun removeInvalidServer(guid: String): Int {
var count = 0
if (guid.isNotEmpty()) {
decodeServerAffiliationInfo(guid)?.let { aff ->
if (aff.testDelayMillis < 0L) {
removeServer(guid)
count++
}
}
} else {
serverAffStorage.allKeys()?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff ->
if (aff.testDelayMillis < 0L) {
removeServer(key)
count++
}
}
}
}
return count
}
/**
* Encodes the raw server configuration.
*
* @param guid The server GUID.
* @param config The raw server configuration.
*/
fun encodeServerRaw(guid: String, config: String) {
serverRawStorage.encode(guid, config)
}
/**
* Decodes the raw server configuration.
*
* @param guid The server GUID.
* @return The raw server configuration.
*/
fun decodeServerRaw(guid: String): String? {
return serverRawStorage.decodeString(guid)
}
//endregion
//region Subscriptions
private fun getSubscriptionId(subscriptionId: String?): String {
return subscriptionId?.ifEmpty { DEFAULT_SUBSCRIPTION_ID } ?: DEFAULT_SUBSCRIPTION_ID
}
/**
* Initializes the subscription list.
*/
private fun initSubsList() {
val subsList = decodeSubsList()
if (subsList.isNotEmpty()) {
return
}
subStorage.allKeys()?.forEach { key ->
subsList.add(key)
}
encodeSubsList(subsList)
}
/**
* Decodes the subscriptions.
*
* @return The list of subscriptions.
*/
fun decodeSubscriptions(): List<SubscriptionCache> {
initSubsList()
val subscriptions = mutableListOf<SubscriptionCache>()
decodeSubsList().forEach { key ->
val json = subStorage.decodeString(key)
if (!json.isNullOrBlank()) {
val item = JsonUtil.fromJsonSafe(json, SubscriptionItem::class.java) ?: SubscriptionItem()
subscriptions.add(SubscriptionCache(key, item))
}
}
return subscriptions
}
/**
* Removes the subscription.
*
* @param subid The subscription ID.
*/
fun removeSubscription(subid: String) {
subStorage.remove(subid)
val subsList = decodeSubsList()
subsList.remove(subid)
encodeSubsList(subsList)
removeServerViaSubid(subid)
}
/**
* Encodes the subscription.
*
* @param guid The subscription GUID.
* @param subItem The subscription item.
*/
fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
val key = guid.ifBlank { Utils.getUuid() }
subStorage.encode(key, JsonUtil.toJson(subItem))
val subsList = decodeSubsList()
if (!subsList.contains(key)) {
subsList.add(key)
encodeSubsList(subsList)
}
}
/**
* Decodes the subscription.
*
* @param subscriptionId The subscription ID.
* @return The subscription item.
*/
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
val json = subStorage.decodeString(subscriptionId) ?: return null
return JsonUtil.fromJsonSafe(json, SubscriptionItem::class.java)
}
/**
* Encodes the subscription list.
*
* @param subsList The list of subscription IDs.
*/
fun encodeSubsList(subsList: MutableList<String>) {
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
}
/**
* Decodes the subscription list.
*
* @return The list of subscription IDs.
*/
fun decodeSubsList(): MutableList<String> {
val json = mainStorage.decodeString(KEY_SUB_IDS)
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
JsonUtil.fromJsonSafe(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
}
}
//endregion
//region Asset
/**
* Decodes the asset URLs.
*
* @return The list of asset URLs.
*/
fun decodeAssetUrls(): List<AssetUrlCache> {
val assetUrlItems = mutableListOf<AssetUrlCache>()
assetStorage.allKeys()?.forEach { key ->
val json = assetStorage.decodeString(key)
if (!json.isNullOrBlank()) {
val item = JsonUtil.fromJsonSafe(json, AssetUrlItem::class.java) ?: AssetUrlItem()
assetUrlItems.add(AssetUrlCache(key, item))
}
}
return assetUrlItems.sortedBy { it.assetUrl.addedTime }
}
/**
* Removes the asset URL.
*
* @param assetid The asset ID.
*/
fun removeAssetUrl(assetid: String) {
assetStorage.remove(assetid)
}
/**
* Encodes the asset.
*
* @param assetid The asset ID.
* @param assetItem The asset item.
*/
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
val key = assetid.ifBlank { Utils.getUuid() }
assetStorage.encode(key, JsonUtil.toJson(assetItem))
}
/**
* Decodes the asset.
*
* @param assetid The asset ID.
* @return The asset item.
*/
fun decodeAsset(assetid: String): AssetUrlItem? {
val json = assetStorage.decodeString(assetid) ?: return null
return JsonUtil.fromJsonSafe(json, AssetUrlItem::class.java)
}
//endregion
//region Routing
/**
* Decodes the routing rulesets.
*
* @return The list of routing rulesets.
*/
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
if (ruleset.isNullOrEmpty()) return null
return JsonUtil.fromJsonSafe(ruleset, Array<RulesetItem>::class.java)?.toMutableList() ?: mutableListOf()
}
/**
* Encodes the routing rulesets.
*
* @param rulesetList The list of routing rulesets.
*/
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
if (rulesetList.isNullOrEmpty())
encodeSettings(PREF_ROUTING_RULESET, "")
else
encodeSettings(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
}
//endregion
//region settings
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: String?): Boolean {
return settingsStorage.encode(key, value)
}
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: Int): Boolean {
return settingsStorage.encode(key, value)
}
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: Long): Boolean {
return settingsStorage.encode(key, value)
}
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: Float): Boolean {
return settingsStorage.encode(key, value)
}
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: Boolean): Boolean {
return settingsStorage.encode(key, value)
}
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: MutableSet<String>): Boolean {
return settingsStorage.encode(key, value)
}
/**
* Decodes the settings string.
*
* @param key The settings key.
* @return The settings value.
*/
fun decodeSettingsString(key: String): String? {
return settingsStorage.decodeString(key)
}
/**
* Decodes the settings string.
*
* @param key The settings key.
* @param defaultValue The default value.
* @return The settings value.
*/
fun decodeSettingsString(key: String, defaultValue: String?): String? {
return settingsStorage.decodeString(key, defaultValue)
}
/**
* Decodes the settings integer.
*
* @param key The settings key.
* @param defaultValue The default value.
* @return The settings value.
*/
fun decodeSettingsInt(key: String, defaultValue: Int): Int {
return settingsStorage.decodeInt(key, defaultValue)
}
/**
* Decodes the settings long.
*
* @param key The settings key.
* @param defaultValue The default value.
* @return The settings value.
*/
fun decodeSettingsLong(key: String, defaultValue: Long): Long {
return settingsStorage.decodeLong(key, defaultValue)
}
/**
* Decodes the settings float.
*
* @param key The settings key.
* @param defaultValue The default value.
* @return The settings value.
*/
fun decodeSettingsFloat(key: String, defaultValue: Float): Float {
return settingsStorage.decodeFloat(key, defaultValue)
}
/**
* Decodes the settings boolean.
*
* @param key The settings key.
* @return The settings value.
*/
fun decodeSettingsBool(key: String): Boolean {
return settingsStorage.decodeBool(key, false)
}
/**
* Decodes the settings boolean.
*
* @param key The settings key.
* @param defaultValue The default value.
* @return The settings value.
*/
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
return settingsStorage.decodeBool(key, defaultValue)
}
/**
* Decodes the settings string set.
*
* @param key The settings key.
* @return The settings value.
*/
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
return settingsStorage.decodeStringSet(key)
}
/**
* Encodes the start on boot setting.
*
* @param startOnBoot Whether to start on boot.
*/
fun encodeStartOnBoot(startOnBoot: Boolean) {
encodeSettings(PREF_IS_BOOTED, startOnBoot)
}
/**
* Decodes the start on boot setting.
*
* @return Whether to start on boot.
*/
fun decodeStartOnBoot(): Boolean {
return decodeSettingsBool(PREF_IS_BOOTED, false)
}
//endregion
//region WebDAV
/**
* Encodes the WebDAV config as JSON into storage.
*/
fun encodeWebDavConfig(config: WebDavConfig): Boolean {
return mainStorage.encode(KEY_WEBDAV_CONFIG, JsonUtil.toJson(config))
}
/**
* Decodes the WebDAV config from storage.
*/
fun decodeWebDavConfig(): WebDavConfig? {
val json = mainStorage.decodeString(KEY_WEBDAV_CONFIG) ?: return null
return JsonUtil.fromJsonSafe(json, WebDavConfig::class.java)
}
//endregion
//region Compose helpers for Settings
/**
* MMKV-backed String state, auto-persists and notifies on change.
*/
@Composable
fun rememberMmkvString(
key: String,
default: String = ""
): MutableState<String> {
val state = remember(key) {
mutableStateOf(decodeSettingsString(key, default) ?: default)
}
LaunchedEffect(key) {
snapshotFlow { state.value }
.drop(1)
.distinctUntilChanged()
.collectLatest { value ->
encodeSettings(key, value)
SettingsChangeManager.notifySettingChanged(key)
}
}
return state
}
/**
* MMKV-backed Boolean state, auto-persists and notifies on change.
*/
@Composable
fun rememberMmkvBool(
key: String,
default: Boolean = false
): MutableState<Boolean> {
val state = remember(key) {
mutableStateOf(decodeSettingsBool(key, default))
}
LaunchedEffect(key) {
snapshotFlow { state.value }
.drop(1)
.distinctUntilChanged()
.collectLatest { value ->
encodeSettings(key, value)
SettingsChangeManager.notifySettingChanged(key)
}
}
return state
}
//endregion
}
@@ -1,290 +0,0 @@
package com.v2ray.ang.handler
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import androidx.annotation.RequiresApi
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.entities.ProfileItem
import com.v2ray.ang.extension.toSpeedString
import com.v2ray.ang.ui.main.MainActivity
import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.math.min
object NotificationManager {
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2
private const val NOTIFICATION_ICON_THRESHOLD = 3000
private const val QUERY_INTERVAL_MS = 3000L
private var lastQueryTime = 0L
private var mBuilder: NotificationCompat.Builder? = null
private var speedNotificationJob: Job? = null
private var mNotificationManager: NotificationManager? = null
/**
* Starts the speed notification.
* @param currentConfig The current profile configuration.
*/
fun startSpeedNotification() {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return
if (speedNotificationJob != null || CoreServiceManager.isRunning() == false) return
var lastZeroSpeed = false
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
lastZeroSpeed = updateSpeedNotificationOnce(lastZeroSpeed)
delay(QUERY_INTERVAL_MS)
}
}
}
/**
* Shows the notification.
* @param currentConfig The current profile configuration.
*/
fun showNotification(currentConfig: ProfileItem?) {
val service = getService() ?: return
// Reset last query time to avoid querying stats too soon after showing the notification
lastQueryTime = System.currentTimeMillis()
val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val startMainIntent = Intent(service, MainActivity::class.java)
val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
stopV2RayIntent.`package` = AppConfig.ANG_PACKAGE
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
restartV2RayIntent.`package` = AppConfig.ANG_PACKAGE
restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
mBuilder = NotificationCompat.Builder(service, channelId)
.setSmallIcon(R.drawable.ic_stat_name)
.setContentTitle(currentConfig?.remarks ?: service.getString(R.string.app_name))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setShowWhen(false)
.setOnlyAlertOnce(true)
.setContentIntent(contentPendingIntent)
.addAction(
R.drawable.ic_delete_24dp,
service.getString(R.string.notification_action_stop_v2ray),
stopV2RayPendingIntent
)
.addAction(
R.drawable.ic_restore_24dp,
service.getString(R.string.title_service_restart),
restartV2RayPendingIntent
)
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE)
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
}
/**
* Fulfills or refreshes the foreground-service contract before a start command can
* return early. A duplicate startForegroundService call still requires the service
* to enter foreground state promptly, even when the core is already running.
*/
fun ensureForeground() {
val service = getService() ?: return
val notification = mBuilder?.build()
if (notification == null) showNotification(null) else service.startForeground(NOTIFICATION_ID, notification)
}
/**
* Cancels the notification.
*/
fun cancelNotification() {
val service = getService() ?: return
service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
mBuilder = null
speedNotificationJob?.cancel()
speedNotificationJob = null
mNotificationManager = null
}
/**
* Stops the speed notification.
*/
fun stopSpeedNotification() {
speedNotificationJob?.let {
it.cancel()
speedNotificationJob = null
updateNotification("", 0, 0)
}
}
/**
* Creates a notification channel for Android O and above.
* @return The channel ID.
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String {
val channelId = AppConfig.RAY_NG_CHANNEL_ID
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
// Foreground-service notifications must remain visible; LOW is silent but valid.
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
chan.lightColor = Color.DKGRAY
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
getNotificationManager()?.createNotificationChannel(chan)
return channelId
}
/**
* Updates the notification with the given content text and traffic data.
* @param contentText The content text.
* @param proxyTraffic The proxy traffic.
* @param directTraffic The direct traffic.
*/
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
if (mBuilder != null) {
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
} else if (proxyTraffic > directTraffic) {
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
} else {
mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
}
mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
mBuilder?.setContentText(contentText)
getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
}
}
/**
* Gets the notification manager.
* @return The notification manager.
*/
private fun getNotificationManager(): NotificationManager? {
if (mNotificationManager == null) {
val service = getService() ?: return null
mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
return mNotificationManager
}
/**
* Appends the speed string to the given text.
* @param text The text to append to.
* @param name The name of the tag.
* @param up The uplink speed.
* @param down The downlink speed.
*/
private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
var n = name ?: "no tag"
n = n.take(min(n.length, 6))
text.append(n)
for (i in n.length..6 step 2) {
text.append("\t")
}
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
}
}
// Accumulate stats for all proxy outbounds (including custom subscription tags)
stat.tag != AppConfig.TAG_BLOCKED -> {
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.
*/
private fun getService(): Service? {
return CoreServiceManager.serviceControl?.get()?.getService()
}
}
Loaded 100 of 515 files, more files were not shown because too many files have changed in this diff. Show more