Compare commits

..

98 Commits

Author SHA1 Message Date
2dust df3f1ca3ef Merge pull request #620 from yuhan6665/up-version
Update dependencies
2020-09-29 08:05:46 +08:00
yuhan6665 c54d8fa43a Update dependencies
Fix a delegate annotation for Kotlin 1.4
2020-09-25 18:37:29 -04:00
2dust 5bbbdcf6f2 Merge pull request #610 from yuhan6665/fix-protect
Fix protect() when there is no service
2020-09-21 07:59:55 +08:00
yuhan6665 4abf20fa32 Fix protect() when there is no service 2020-09-20 19:00:14 -04:00
2dust 723727feb9 Merge pull request #607 from yuhan6665/help-proxy-mode
Help proxy mode
2020-09-19 18:56:15 +08:00
yuhan6665 2efd4b741c Add badge, wiki link and dev guide 2020-09-18 17:48:54 -04:00
yuhan6665 83aab0f880 Add help link in mode setting 2020-09-18 17:48:54 -04:00
2dust 66ea17877e Merge pull request #598 from yuhan6665/fix-process
Add missing process for ScSwitchActivity
2020-09-12 19:48:04 +08:00
2dust 26bc985368 Merge pull request #597 from yuhan6665/settings-ui
Settings ui
2020-09-12 19:47:48 +08:00
yuhan6665 5bbf40c784 Grey out per-app proxy in proxy only mode 2020-09-11 21:30:08 -04:00
yuhan6665 6d5c23245c Move version to drawer
Make it easier to check version.
Technically, version should not be selectable setting
2020-09-11 21:30:08 -04:00
yuhan6665 b148290211 Add missing process for ScSwitchActivity 2020-09-11 21:29:58 -04:00
2dust c473f9bb13 Merge pull request #587 from yuhan6665/proxy-mode
Proxy only mode
2020-09-06 08:09:10 +08:00
2dust c7c3d27f36 Merge pull request #584 from akiirui/fix-metered
V2RayVpnService.kt: fix OS marks VPN as metered
2020-09-06 08:08:58 +08:00
yuhan6665 bebc6fea13 Add mode in settings 2020-09-04 18:19:51 -04:00
yuhan6665 9271857b1e Remove VPN related logic in toggle components 2020-09-04 18:19:51 -04:00
yuhan6665 7ea78c1840 Make all toggle component run in daemon process
This commit make the two process with clear role:
 - Daemon process will run proxy core, keep minimum configuration
 and long running in the background, support toggle on/off
 - Main process will have all UI, all servers configuration
2020-09-04 18:19:51 -04:00
yuhan6665 ac668788b3 Add proxy only service 2020-09-04 18:19:51 -04:00
yuhan6665 adfcf0a5d9 Fix some trivial IDE warnings 2020-09-04 18:19:51 -04:00
yuhan6665 15b5595797 Refactor service
Break VpnService so that it only control vpn and tun2socks,
Other function is moved to a singleton service manager.
This code makes it possible to add proxy only service next.
2020-09-04 18:19:51 -04:00
Akatsuki 5a18296cb2 V2RayVpnService.kt: use setMetered
setMetered to false let VPN network to inherit its meteredness from its underlying networks.

Revert `addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)`
2020-09-03 02:07:11 +08:00
Akatsuki 1256edbaf5 V2RayVpnService.kt: fix OS marks VPN as metered
VPN apps targeting Build.VERSION_CODES.Q or above will be considered metered by default.

Ref:
https://developer.android.com/reference/android/net/VpnService.Builder#setMetered(boolean)
https://developer.android.com/about/versions/pie/android-9.0-changes-all#network-capabilities-vpn
2020-09-02 13:46:18 +08:00
2dust 870950e807 Merge pull request #577 from yuhan6665/fix-target-29
Fix release build with tun2socks
2020-08-30 07:21:24 +08:00
yuhan6665 e798cb3f42 Fix release build with tun2socks 2020-08-29 12:34:54 -04:00
2dust b970f0bcff Merge pull request #574 from yuhan6665/all-outbounds-speed
All outbounds speed
2020-08-29 20:33:18 +08:00
2dust 93b9a56428 Merge pull request #387 from yuhan6665/target-29
Target sdk 29
2020-08-29 20:32:59 +08:00
yuhan6665 eb8bd13266 Add speed notification for all outbounds 2020-08-28 21:52:44 -04:00
yuhan6665 d850c88c63 Support ordered set in DPreference
Note the ordered set need to be stored internally as String,
not string set
2020-08-28 21:52:43 -04:00
yuhan6665 6bee795c0d Update target sdk to 29 2020-08-27 20:19:00 -04:00
yuhan6665 d7d7e029e0 Fix compile warnings that will be error in sdk 29 2020-08-27 20:18:59 -04:00
2dust 8efdab43d7 Merge pull request #566 from yuhan6665/array-crash
Fix test crash if node is removed
2020-08-22 08:06:36 +08:00
yuhan6665 5e0235cf70 Fix test crash if node is removed
This should fix almost all issues when the testing is in progress
and the array is changed. However, there might be uncovered edge
cases since the vmess array is in both process and accessed by
multiple threads.
2020-08-21 19:47:33 -04:00
2dust 9f668b3da7 Merge pull request #559 from yuhan6665/custom-config-improve
Custom config improve
2020-08-16 20:26:17 +08:00
yuhan6665 b2437279dc Update UI for custom config
- add server address and port
- add share button (full configuration)
- update "Export all config to clipboard" text to indicate
it will not include custom config
2020-08-15 19:37:59 -04:00
yuhan6665 180b5efd93 Enable tcping test for custom config 2020-08-15 19:37:59 -04:00
yuhan6665 dc31380cc2 Refactor EConfigType to be an enum
Using enum will simplify logic and conveniently providing Int value
and a String name
2020-08-15 19:37:59 -04:00
2dust 1361e0dacf Merge pull request #555 from yuhan6665/per-app-list
Update latest per-app list
2020-08-15 20:40:10 +08:00
2dust a45eb66bd1 Merge pull request #554 from ShrdBB/master
Fix typo in strings.xml
2020-08-15 20:39:50 +08:00
yuhan6665 508102cebe Update latest per-app list
https://github.com/2dust/androidpackagenamelist/blob/master/proxy.txt
2020-08-14 20:45:30 -04:00
ShrdBB 1d86dbb9f3 fix typo in strings.xml 2020-08-14 12:48:46 +08:00
2dust 20ca554be2 Merge pull request #544 from yuhan6665/android-system
Add "Android System" in per-app vpn
2020-08-09 12:57:49 +08:00
yuhan6665 25ba455656 Add "Android System" in per-app vpn 2020-08-08 23:49:06 -04:00
2dust 17e7c62d53 Merge pull request #542 from yuhan6665/master
Sanitize project configuration
2020-08-08 15:35:16 +08:00
yuhan6665 6f0f2fdeda Sanitize project configuration
Make project compilable out of box. This commit address
the following:
- remove release key
- fix gitignore for Android Studio temp files
- add gradle wrapper properties
2020-08-07 21:37:00 -04:00
2dust 3c9c9b5a4c Merge pull request #531 from yuhan6665/emui-notification
Fix notification for Emui 4.1
2020-08-06 20:40:45 +08:00
2dust e13024d6bb Merge pull request #526 from Vixb1122/master
exclude ScSwitchActivity from recent list
2020-08-06 20:39:08 +08:00
2dust 9d58edd31f Merge pull request #537 from yuhan6665/revert
Revert "Refresh prepared domain every 30 minutes"
2020-08-06 20:38:50 +08:00
yuhan6665 af7dfc3a43 Revert "Refresh prepared domain every 30 minutes"
This reverts commit 903352ec9c.
2020-08-05 20:22:01 -04:00
yuhan6665 ca554e6ac4 Fix notification for Emui 4.1 2020-08-02 08:42:04 -04:00
xuezhixin ec391d8689 exclude ScSwitchActivity from recent list 2020-07-29 16:47:40 +08:00
2dust 6afd4d0549 Update AngConfigManager.kt 2020-07-27 21:00:34 +08:00
2dust 957cf85362 Merge pull request #517 from yuhan6665/bypass-private
Bypass private IP at VPN service
2020-07-25 19:42:12 +08:00
2dust 881152e10a Merge pull request #516 from cpdyj/master
add support to new version vmess link
2020-07-25 19:41:46 +08:00
2dust 6e47ebb27a Merge pull request #484 from DevRyz3n/drag-item-store-config
Optimize invoke timing of store config file after dragging item.
2020-07-25 19:41:13 +08:00
2dust a731e6c360 Merge pull request #453 from yuhan6665/widget
Widget
2020-07-25 19:40:39 +08:00
yuhan6665 d1466ba4b2 Bypass private IP at VPN service 2020-07-24 18:12:20 -04:00
iseki 617f28f399 Update AngConfigManager.kt 2020-07-24 11:23:46 +08:00
iseki c0ec0d9404 Update AngConfigManager.kt 2020-07-24 11:21:20 +08:00
iseki 3419dc8837 add support to new version vmess link
some information can be found at: https://github.com/v2ray/discussion/issues/720
2020-07-23 00:51:08 +08:00
2dust 786aaf823a Merge pull request #511 from yuhan6665/outbound-speed
Outbound speed
2020-07-19 08:19:03 +08:00
yuhan6665 1144183da4 Update notification icon based on traffic
Notification icon would be a good indication for current traffic
- going through proxy
- connect directly
- not going through v2rayNG
2020-07-18 18:48:47 -04:00
yuhan6665 5bf2fda990 Measure traffic from outbounds
With v2ray-core 4.26.0, traffic can be measured from outbounds.
Stats is shown separately for proxy and direct traffic.

In the future, it is possible to add stats for each server node and
even historic usage graph.
2020-07-18 18:47:59 -04:00
2dust 28639cc388 Merge pull request #468 from yuhan6665/cleanup-config
Remove legacy "connectionReuse" in config
2020-07-18 14:01:38 +08:00
2dust ca254b2aa1 Merge pull request #443 from yuhan6665/resume-speed
Fix speed display after screen turns on
2020-07-18 14:00:48 +08:00
2dust 9721879713 Merge pull request #432 from yuhan6665/drawer-ui
fix highlight issue in drawer ui
2020-07-18 13:59:47 +08:00
2dust 623c1807c5 Merge pull request #409 from yuhan6665/test-cleanup
Tcp test cleanup
2020-07-18 13:58:37 +08:00
2dust 739fa88ba7 Merge pull request #335 from yuhan6665/sub-index
Fix selected index when update subscription
2020-07-18 13:58:11 +08:00
2dust 85d6f00f8c Merge pull request #474 from yuhan6665/dns-cache-timeout
Refresh prepared domain every 30 minutes
2020-07-18 09:48:16 +08:00
yuhan6665 8986710453 Cancel async block when user test tcping again
Change async code to use Kotlin coroutine instead of Anko,
since Anko is deprecated and coroutine is the recommanded
approach now.
2020-07-16 19:42:43 -04:00
yuhan6665 f54faacbf6 Close connecting sockets when user test tcping again 2020-07-16 19:42:43 -04:00
r23 993ee0b8d2 Optimize invoke timing of store config file after dragging item. 2020-06-29 01:24:19 +08:00
yuhan6665 903352ec9c Refresh prepared domain every 30 minutes 2020-06-19 23:42:02 -04:00
yuhan6665 ad56106c08 Remove legacy "connectionReuse" in config
Also auto-refactor the data class naming to be proper camelcase
2020-06-13 23:38:33 -04:00
yuhan6665 91b8284afd Improve widget UI
Change widget color when clicked
2020-06-06 00:18:19 -04:00
yuhan6665 ef9e0cc0d2 Add back widget, fix implicit Intent 2020-06-06 00:18:19 -04:00
2dust 6577c46a31 Merge pull request #425 from yuhan6665/translation
Fix a traditional Chinese translation
2020-05-31 16:13:35 +08:00
yuhan6665 c6dab001b2 Fix traditional Chinese translation 2020-05-30 12:34:29 -04:00
yuhan6665 aea8369b8a Fix speed display after screen turns on
Also, use the tab to make the text less jumpy
2020-05-29 22:54:55 -04:00
yuhan6665 92d2cb35c4 Make drawer item unchecked
Current checked item is not consistent with the active activity.
In fact, we don't need checked state. This is a standard behavior.
You can find in apps like Google Playstore.
2020-05-24 11:13:12 -04:00
yuhan6665 6ce3d540e8 Remove config item in drawer
The drawer is attached to MainActivity. When user see the drawer, the
MainActivity must be active. Showing an menu item to launch itself is
confusing.
2020-05-24 11:09:11 -04:00
2dust c105d84b35 Merge pull request #300 from yuhan6665/master
Fix UI touch issues for Android TV
2020-04-12 10:04:52 +08:00
yuhan6665 8b149fb52f Fix selected index when update subscription 2020-04-03 18:58:41 -04:00
2dust 3ea04c076c Merge pull request #314 from yuhan6665/chrome-os
Add tags in AndroidManifest to support PlayStore filtering
2020-03-16 16:24:58 +08:00
2dust 98475460bf Merge pull request #313 from yuhan6665/lint-fix
Use annotation to fix lint error
2020-03-16 16:24:08 +08:00
yuhan6665 68ee61a753 Add tags in AndroidManifest to support PlayStore filtering
Google Playstore is using uses-feature and supports-screens
to filter devices.
By setting all screens to true and specifing features as
"not required" should make app compatible with more devices
Particularly it includes Chrome OS devices.
2020-03-14 14:07:31 -04:00
yuhan6665 90ba9ef2b7 Use annotation to fix lint error 2020-03-14 13:03:51 -04:00
yuhan6665 0ec114322e Fix UI touch issues for Android TV 2020-03-08 12:06:44 -04:00
2dust 76f52e7aa7 Merge pull request #287 from yuhan6665/multidex
Fix multidex for Android devices below L
2020-03-03 14:52:19 +08:00
2dust caa25ce424 Merge pull request #278 from yuhan6665/master
Add delete log button in logcat
2020-03-03 14:52:08 +08:00
yuhan6665 e33d7e9bcf Fix multidex for Android devices below L 2020-02-29 10:51:58 -05:00
yuhan6665 e120fce0b7 Add delete log button in logcat
Also, automatically scroll to bottom of the log if it is too long
2020-02-22 12:01:16 -05:00
2dust 6a90965c7b Update README.md 2019-11-14 13:09:31 +08:00
2dust 0542d913f2 Merge pull request #131 from galvasis193/feature-customediting
Give user the ability to edit/copy/paste custom config for convenience. Fix Import
2019-10-14 08:22:39 +08:00
Annoymous contributor 746254bb7e Give user the ability to edit/copy/paste custom config for convenience.
Fix import for go.Seq
2019-10-13 04:55:00 +00:00
2dust 2227ea3ed6 Merge pull request #129 from galvasis193/feature-ProxySharing
Add proxy sharing function. Fix potential security vulnerbility.
2019-10-12 09:40:19 +08:00
Annoymous contributor 4d0e9e2b44 Bugfix: When a setting is changed proxy will now auto restart to apply the change. Fix proxy service cannot be stopped bug. 2019-10-11 10:28:45 +00:00
Annoymous contributor 5cba8a228c Add proxy sharing. Fix potential unahthorized access from LAN/Internet. 2019-10-11 10:26:46 +00:00
2dust 2d8ed22dba mge 2019-10-11 14:17:10 +08:00
444 changed files with 13944 additions and 19160 deletions
-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的问题,请至这个链接讨论。
@@ -1,8 +1,3 @@
---
name: v2rayNG程序问题
about: 创建一个报告来帮助我们改进
---
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
@@ -19,8 +14,7 @@ about: 创建一个报告来帮助我们改进
### 日志信息
<details>
通过`adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main`获取日志。请自行删减日志中可能出现的敏感信息。
通过 `adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main` 获取日志。请自行删减日志中可能出现的敏感信息。
如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
```
-93
View File
@@ -1,93 +0,0 @@
name: Build APK
on:
push:
workflow_dispatch:
inputs:
XRAY_CORE_VERSION:
description: 'Xray core version or commit hash'
required: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Golang
uses: actions/setup-go@v5
with:
go-version: '1.23.2'
cache: false
- name: Patch Go use 600296
#https://go-review.googlesource.com/c/go/+/600296
run: |
cd "$(go env GOROOT)"
curl "https://go-review.googlesource.com/changes/go~600296/revisions/5/patch" | base64 -d | patch --verbose -p 1
- name: Install gomobile
run: |
go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240806205939-81131f6468ab
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Setup Android environment
uses: android-actions/setup-android@v3
- name: Build dependencies
run: |
mkdir ${{ github.workspace }}/build
cd ${{ github.workspace }}/build
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git
cd AndroidLibXrayLite
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
gomobile init
go mod tidy -v
gomobile bind -v -androidapi 21 -ldflags='-s -w' ./
cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/
- name: Build APK
run: |
cd ${{ github.workspace }}/V2rayNG
chmod 755 gradlew
./gradlew assembleDebug
- name: Upload arm64-v8a APK
uses: actions/upload-artifact@v4
if: ${{ success() }}
with:
name: arm64-v8a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
- name: Upload armeabi-v7a APK
uses: actions/upload-artifact@v4
if: ${{ success() }}
with:
name: armeabi-v7a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
- name: Upload x86 APK
uses: actions/upload-artifact@v4
if: ${{ success() }}
with:
name: x86-apk
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
- name: Upload Other APKs
uses: actions/upload-artifact@v4
with:
name: others-apk
path: |
${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
-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@v4
- name: Validate Fastlane Supply Metadata
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0
+5 -1
View File
@@ -1,5 +1,9 @@
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
*.dat
*.jks
V2rayNG/app/release/output.json
.idea/
.gradle/
.gradle/
+26
View File
@@ -0,0 +1,26 @@
sudo: required
language: go
go:
- "1.12"
go_import_path: github.com/2dust/AndroidLibV2rayLite
git:
depth: 5
addons:
apt:
update: true
before_script:
- sudo ntpdate -u time.google.com
- date
- make all
- make downloadGoMobile
script:
- make BuildMobile
after_success:
deploy:
provider: releases
api_key: ${GH_TOKEN}
file:
- libv2ray.aar
skip_cleanup: true
on:
tags: true
+64
View File
@@ -0,0 +1,64 @@
package CoreI
import (
v2core "v2ray.com/core"
)
type Status struct {
IsRunning bool
PackageName string
PackageCodePath string
Vpoint v2core.Server
}
func CheckVersion() int {
return 21
}
func (v *Status) GetDataDir() string {
return v.PackageName
}
func (v *Status) GetApp(name string) string {
return v.PackageCodePath + name
}
func (v *Status) GetTun2socksArgs(localDNS bool, enableIPv6 bool) (ret []string) {
ret = []string{"--netif-ipaddr",
"26.26.26.2",
"--netif-netmask",
"255.255.255.252",
"--socks-server-addr",
"127.0.0.1:10808",
"--tunmtu",
"1500",
"--loglevel",
"notice",
"--enable-udprelay",
"--sock-path",
v.GetDataDir() + "sock_path",
}
if enableIPv6 {
ret = append(ret, "--netif-ip6addr", "da26:2626::2")
}
if localDNS {
ret = append(ret, "--dnsgw", "127.0.0.1:10807")
}
return
}
func (v *Status) GetVPNSetupArg(localDNS bool, enableIPv6 bool) (ret string) {
ret = "m,1500 a,26.26.26.1,30 r,0.0.0.0,0"
if enableIPv6 {
ret += " a,da26:2626::1,126 r,::,0"
}
if localDNS {
ret += " d,26.26.26.2"
}
return
}
+34
View File
@@ -0,0 +1,34 @@
pb:
go get -u github.com/golang/protobuf/protoc-gen-go
@echo "pb Start"
asset:
bash gen_assets.sh download
mkdir assets
cp -v data/*.dat assets/
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geosite.dat > geosite.dat
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geoip.dat > geoip.dat
shippedBinary:
cd shippedBinarys; $(MAKE) shippedBinary
fetchDep:
-go get github.com/2dust/AndroidLibV2rayLite
go get github.com/2dust/AndroidLibV2rayLite
ANDROID_HOME=$(HOME)/android-sdk-linux
export ANDROID_HOME
PATH:=$(PATH):$(GOPATH)/bin
export PATH
downloadGoMobile:
go get golang.org/x/mobile/cmd/...
sudo apt-get install -qq libstdc++6:i386 lib32z1 expect
cd ~ ;curl -L https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/ubuntu-cli-install-android-sdk.sh | sudo bash - > /dev/null
ls ~
ls ~/android-sdk-linux/
gomobile init ;gomobile bind -v -tags json github.com/2dust/AndroidLibV2rayLite
BuildMobile:
@echo Stub
all: asset pb shippedBinary fetchDep
@echo DONE
@@ -0,0 +1,87 @@
package Escort
import (
"os"
"os/exec"
"time"
"log"
"github.com/2dust/AndroidLibV2rayLite/CoreI"
)
func (v *Escorting) EscortRun(proc string, pt []string, additionalEnv string, sendFd func() int) {
log.Println(proc, pt)
count := 0
for count <= 42 {
cmd := exec.Command(proc, pt...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if len(additionalEnv) > 0 {
//additionalEnv := "FOO=bar"
newEnv := append(os.Environ(), additionalEnv)
cmd.Env = newEnv
}
if err := cmd.Start(); err != nil {
log.Println("EscortRun cmd.Start err", err)
goto CMDERROR
}
if v.escortProcess == nil {
log.Println("EscortRun v.escortProcess nil")
break
}
*v.escortProcess = append(*v.escortProcess, cmd.Process)
log.Println("EscortRun Waiting....")
if count > 0 {
go func() {
time.Sleep(time.Second)
sendFd()
}()
}
if err := cmd.Wait(); err != nil {
log.Println("EscortRun cmd.Wait err:", err)
}
CMDERROR:
if v.Status.IsRunning {
log.Println("EscortRun Unexpected Exit, Restart now.")
count++
} else {
log.Println("EscortRun Exit")
break
}
}
}
func (v *Escorting) EscortingUp() {
if v.escortProcess != nil {
return
}
v.escortProcess = new([](*os.Process))
}
func (v *Escorting) EscortingDown() {
if v.escortProcess == nil {
return
}
log.Println("EscortingDown() Killing all escorted process ")
for _, pr := range *v.escortProcess {
pr.Kill()
if _, err := pr.Wait(); err != nil {
log.Println("EscortingDown pr.Wait err:", err)
}
}
v.escortProcess = nil
}
type Escorting struct {
escortProcess *[](*os.Process)
Status *CoreI.Status
}
-19
View File
@@ -1,20 +1 @@
# AndroidLibV2rayLite
### Preparation
- latest Ubuntu environment
- At lease 30G free space
- Get Repo [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite)
### Prepare Go
- Go to https://golang.org/doc/install and install latest go
- Make sure `go version` works as expected
### Prepare gomobile
- Go to https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile and install gomobile
- export PATH=$PATH:~/go/bin
- Make sure `gomobile init` works as expected
### Prepare NDK
- Go to https://developer.android.com/ndk/downloads and install latest NDK
- export PATH=$PATH:<wherever you ndk is located>
- Make sure `ndk-build -v` works as expected
### Make
- sudo apt install make
- Read and understand [build script](https://github.com/2dust/AndroidLibV2rayLite/blob/master/Makefile)
@@ -0,0 +1,279 @@
package VPN
import (
"context"
"errors"
"fmt"
"log"
"net"
"os"
"strings"
"sync"
"time"
"golang.org/x/sys/unix"
v2net "v2ray.com/core/common/net"
v2internet "v2ray.com/core/transport/internet"
)
type protectSet interface {
Protect(int) int
}
type resolved struct {
domain string
IPs []net.IP
Port int
ipIdx uint8
ipLock sync.Mutex
lastSwitched time.Time
}
// NextIP switch to another resolved result.
// there still be race-condition here if multiple err concurently occured
// may cause idx keep switching,
// but that's an outside error can hardly handled here
func (r *resolved) NextIP() {
r.ipLock.Lock()
defer r.ipLock.Unlock()
if len(r.IPs) > 1 {
// throttle, don't switch too quickly
now := time.Now()
if now.Sub(r.lastSwitched) < time.Second*5 {
log.Println("switch too quickly")
return
}
r.lastSwitched = now
r.ipIdx++
} else {
return
}
if r.ipIdx >= uint8(len(r.IPs)) {
r.ipIdx = 0
}
cur := r.currentIP()
log.Printf("switched to next IP: %s", cur)
}
func (r *resolved) currentIP() net.IP {
if len(r.IPs) > 0 {
return r.IPs[r.ipIdx]
}
return nil
}
// NewPreotectedDialer ...
func NewPreotectedDialer(p protectSet) *ProtectedDialer {
d := &ProtectedDialer{
// prefer native lookup on Android
resolver: &net.Resolver{PreferGo: false},
protectSet: p,
}
return d
}
// ProtectedDialer ...
type ProtectedDialer struct {
currentServer string
resolveChan chan struct{}
vServer *resolved
resolver *net.Resolver
protectSet
}
func (d *ProtectedDialer) IsVServerReady() bool {
return (d.vServer != nil)
}
func (d *ProtectedDialer) PrepareResolveChan() {
d.resolveChan = make(chan struct{})
}
func (d *ProtectedDialer) ResolveChan() <-chan struct{} {
return d.resolveChan
}
// simplicated version of golang: internetAddrList in src/net/ipsock.go
func (d *ProtectedDialer) lookupAddr(addr string) (*resolved, error) {
var (
err error
host, port string
portnum int
)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if host, port, err = net.SplitHostPort(addr); err != nil {
log.Printf("PrepareDomain SplitHostPort Err: %v", err)
return nil, err
}
if portnum, err = d.resolver.LookupPort(ctx, "tcp", port); err != nil {
log.Printf("PrepareDomain LookupPort Err: %v", err)
return nil, err
}
addrs, err := d.resolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
if len(addrs) == 0 {
return nil, fmt.Errorf("domain %s Failed to resolve", addr)
}
IPs := make([]net.IP, len(addrs))
for i, ia := range addrs {
IPs[i] = ia.IP
}
rs := &resolved{
domain: host,
IPs: IPs,
Port: portnum,
}
return rs, nil
}
// PrepareDomain caches direct v2ray server host
func (d *ProtectedDialer) PrepareDomain(domainName string, closeCh <-chan struct{}) {
log.Printf("Preparing Domain: %s", domainName)
d.currentServer = domainName
defer close(d.resolveChan)
maxRetry := 10
for {
if maxRetry == 0 {
log.Println("PrepareDomain maxRetry reached. exiting.")
return
}
resolved, err := d.lookupAddr(domainName)
if err != nil {
maxRetry--
log.Printf("PrepareDomain err: %v\n", err)
select {
case <-closeCh:
log.Printf("PrepareDomain exit due to v2ray closed")
return
case <-time.After(time.Second * 2):
}
continue
}
d.vServer = resolved
log.Printf("Prepare Result:\n Domain: %s\n Port: %d\n IPs: %v\n",
resolved.domain, resolved.Port, resolved.IPs)
return
}
}
func (d *ProtectedDialer) getFd(network v2net.Network) (fd int, err error) {
switch network {
case v2net.Network_TCP:
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
case v2net.Network_UDP:
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
default:
err = fmt.Errorf("unknow network")
}
return
}
// Dial exported as the protected dial method
func (d *ProtectedDialer) Dial(ctx context.Context,
src v2net.Address, dest v2net.Destination, sockopt *v2internet.SocketConfig) (net.Conn, error) {
network := dest.Network.SystemString()
Address := dest.NetAddr()
// v2ray server address,
// try to connect fixed IP if multiple IP parsed from domain,
// and switch to next IP if error occurred
if strings.Compare(Address, d.currentServer) == 0 {
if d.vServer == nil {
log.Println("Dial pending prepare ...", Address)
<-d.resolveChan
// user may close connection during PrepareDomain,
// fast return release resources.
if d.vServer == nil {
return nil, fmt.Errorf("fail to prepare domain %s", d.currentServer)
}
}
fd, err := d.getFd(dest.Network)
if err != nil {
return nil, err
}
curIP := d.vServer.currentIP()
conn, err := d.fdConn(ctx, curIP, d.vServer.Port, fd)
if err != nil {
d.vServer.NextIP()
return nil, err
}
log.Printf("Using Prepared: %s", curIP)
return conn, nil
}
// v2ray connecting to "domestic" servers, no caching results
log.Printf("Not Using Prepared: %s,%s", network, Address)
resolved, err := d.lookupAddr(Address)
if err != nil {
return nil, err
}
fd, err := d.getFd(dest.Network)
if err != nil {
return nil, err
}
// use the first resolved address.
// the result IP may vary, eg: IPv6 addrs comes first if client has ipv6 address
return d.fdConn(ctx, resolved.IPs[0], resolved.Port, fd)
}
func (d *ProtectedDialer) fdConn(ctx context.Context, ip net.IP, port int, fd int) (net.Conn, error) {
defer unix.Close(fd)
// call android VPN service to "protect" the fd connecting straight out
d.Protect(fd)
sa := &unix.SockaddrInet6{
Port: port,
}
copy(sa.Addr[:], ip)
if err := unix.Connect(fd, sa); err != nil {
log.Printf("fdConn unix.Connect err, Close Fd: %d Err: %v", fd, err)
return nil, err
}
file := os.NewFile(uintptr(fd), "Socket")
if file == nil {
// returned value will be nil if fd is not a valid file descriptor
return nil, errors.New("fdConn fd invalid")
}
defer file.Close()
//Closing conn does not affect file, and closing file does not affect conn.
conn, err := net.FileConn(file)
if err != nil {
log.Printf("fdConn FileConn Close Fd: %d Err: %v", fd, err)
return nil, err
}
return conn, nil
}
@@ -0,0 +1,151 @@
package VPN
import (
"bufio"
"context"
"fmt"
"net"
"sync"
"testing"
"time"
v2net "v2ray.com/core/common/net"
)
type fakeSupportSet struct{}
func (f fakeSupportSet) Protect(int) int {
return 0
}
func TestProtectedDialer_PrepareDomain(t *testing.T) {
type args struct {
domainName string
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
{"", args{"baidu.com:80"}},
// {"", args{"cloudflare.com:443"}},
// {"", args{"apple.com:443"}},
// {"", args{"110.110.110.110:443"}},
// {"", args{"[2002:1234::1]:443"}},
}
d := NewPreotectedDialer(fakeSupportSet{})
for _, tt := range tests {
ch := make(chan struct{})
t.Run(tt.name, func(t *testing.T) {
go d.PrepareDomain(tt.args.domainName, ch)
time.Sleep(time.Second)
go d.vServer.NextIP()
t.Log(d.vServer.currentIP())
})
}
time.Sleep(time.Second)
}
func TestProtectedDialer_Dial(t *testing.T) {
tests := []struct {
name string
wantErr bool
}{
// TODO: Add test cases.
{"baidu.com:80", false},
{"cloudflare.com:80", false},
{"172.16.192.11:80", true},
// {"172.16.192.10:80", true},
// {"[2fff:4322::1]:443", true},
// {"[fc00::1]:443", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ch := make(chan struct{})
d := NewPreotectedDialer(fakeSupportSet{})
d.currentServer = tt.name
go d.PrepareDomain(tt.name, ch)
var wg sync.WaitGroup
dial := func() {
defer wg.Done()
dest, _ := v2net.ParseDestination("tcp:" + tt.name)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
conn, err := d.Dial(ctx, nil, dest, nil)
if err != nil {
t.Log(err)
return
}
_host, _, _ := net.SplitHostPort(tt.name)
fmt.Fprintf(conn, fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s\r\n\r\n", _host))
status, err := bufio.NewReader(conn).ReadString('\n')
t.Logf("%#v, %#v\n", status, err)
conn.Close()
}
for n := 0; n < 3; n++ {
wg.Add(1)
go dial()
// time.Sleep(time.Millisecond * 10)
// d.pendingMap[tt.name] = make(chan struct{})
}
wg.Wait()
})
}
}
func Test_resolved_NextIP(t *testing.T) {
type fields struct {
domain string
IPs []net.IP
Port int
}
tests := []struct {
name string
fields fields
}{
// TODO: Add test cases.
{"test1",
fields{
domain: "www.baidu.com",
IPs: []net.IP{
net.ParseIP("1.2.3.4"),
net.ParseIP("4.3.2.1"),
net.ParseIP("1234::1"),
net.ParseIP("4321::1"),
},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &resolved{
domain: tt.fields.domain,
IPs: tt.fields.IPs,
Port: tt.fields.Port,
}
t.Logf("%v", r.IPs)
t.Logf("%v", r.currentIP())
r.NextIP()
t.Logf("%v", r.currentIP())
r.NextIP()
t.Logf("%v", r.currentIP())
r.NextIP()
t.Logf("%v", r.currentIP())
time.Sleep(3 * time.Second)
r.NextIP()
t.Logf("%v", r.currentIP())
time.Sleep(5 * time.Second)
r.NextIP()
t.Logf("%v", r.currentIP())
})
}
}
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
# Set magic variables for current file & dir
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
__base="$(basename ${__file} .sh)"
if [[ ! -d $NDK_HOME ]]; then
echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME"
exit 1
fi
TMPDIR=$(mktemp -d)
clear_tmp () {
rm -rf $TMPDIR
}
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; clear_tmp; exit 1' ERR INT
install -m644 $__dir/tun2socks.mk $TMPDIR/
pushd $TMPDIR
git clone --depth=1 https://github.com/shadowsocks/badvpn.git
git clone --depth=1 https://github.com/shadowsocks/libancillary.git
$NDK_HOME/ndk-build \
NDK_PROJECT_PATH=. \
APP_BUILD_SCRIPT=./tun2socks.mk \
APP_ABI=all \
APP_PLATFORM=android-19 \
NDK_LIBS_OUT=$TMPDIR/libs \
NDK_OUT=$TMPDIR/tmp \
APP_SHORT_COMMANDS=false LOCAL_SHORT_COMMANDS=false -B -j4
install -v -m755 libs/armeabi-v7a/tun2socks $__dir/shippedBinarys/ArchDep/arm/
install -v -m755 libs/arm64-v8a/tun2socks $__dir/shippedBinarys/ArchDep/arm64/
install -v -m755 libs/x86/tun2socks $__dir/shippedBinarys/ArchDep/386/
install -v -m755 libs/x86_64/tun2socks $__dir/shippedBinarys/ArchDep/amd64/
popd
pushd $__dir/shippedBinarys
make clean && make shippedBinary
popd
rm -rf $TMPDIR
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
# Set magic variables for current file & dir
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
__base="$(basename ${__file} .sh)"
DATADIR=${__dir}/data
compile_dat () {
local TMPDIR=$(mktemp -d)
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; rm -rf $TMPDIR; trap ERR; exit 1' ERR
local GEOSITE=${GOPATH}/src/github.com/v2ray/domain-list-community
if [[ -d ${GEOSITE} ]]; then
cd ${GEOSITE} && git pull
else
mkdir -p ${GEOSITE}
cd ${GEOSITE} && git clone https://github.com/v2ray/domain-list-community.git .
fi
go run main.go
if [[ -e dlc.dat ]]; then
rm -f $DATADIR/geosite.dat
mv dlc.dat $DATADIR/geosite.dat
echo "----------> geosite.dat updated."
else
echo "----------> geosite.dat failed to update."
fi
if [[ ! -x $GOPATH/bin/geoip ]]; then
go get -v -u github.com/v2ray/geoip
fi
cd $TMPDIR
curl -L -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country-CSV.zip
unzip -q GeoLite2-Country-CSV.zip
mkdir geoip && find . -name '*.csv' -exec mv -t ./geoip {} +
$GOPATH/bin/geoip \
--country=./geoip/GeoLite2-Country-Locations-en.csv \
--ipv4=./geoip/GeoLite2-Country-Blocks-IPv4.csv \
--ipv6=./geoip/GeoLite2-Country-Blocks-IPv6.csv
if [[ -e geoip.dat ]]; then
rm -f $DATADIR/geoip.dat
mv ./geoip.dat $DATADIR/geoip.dat
echo "----------> geoip.dat updated."
else
echo "----------> geoip.dat failed to update."
fi
trap ERR
return 0
}
download_dat () {
wget -qO - https://api.github.com/repos/v2ray/geoip/releases/latest \
| grep browser_download_url | cut -d '"' -f 4 \
| wget -i - -O $DATADIR/geoip.dat
wget -qO - https://api.github.com/repos/v2ray/domain-list-community/releases/latest \
| grep browser_download_url | cut -d '"' -f 4 \
| wget -i - -O $DATADIR/geosite.dat
}
ACTION="${1:-}"
if [[ -z $ACTION ]]; then
ACTION=download
fi
case $ACTION in
"download") download_dat;;
"compile") compile_dat;;
esac
+257
View File
@@ -0,0 +1,257 @@
package libv2ray
import (
"fmt"
"io"
"log"
"os"
"strings"
"sync"
"github.com/2dust/AndroidLibV2rayLite/CoreI"
"github.com/2dust/AndroidLibV2rayLite/Process/Escort"
"github.com/2dust/AndroidLibV2rayLite/VPN"
"github.com/2dust/AndroidLibV2rayLite/shippedBinarys"
mobasset "golang.org/x/mobile/asset"
v2core "v2ray.com/core"
v2filesystem "v2ray.com/core/common/platform/filesystem"
v2stats "v2ray.com/core/features/stats"
v2serial "v2ray.com/core/infra/conf/serial"
_ "v2ray.com/core/main/distro/all"
v2internet "v2ray.com/core/transport/internet"
v2applog "v2ray.com/core/app/log"
v2commlog "v2ray.com/core/common/log"
)
const (
v2Assert = "v2ray.location.asset"
assetperfix = "/dev/libv2rayfs0/asset"
)
/*V2RayPoint V2Ray Point Server
This is territory of Go, so no getter and setters!
*/
type V2RayPoint struct {
SupportSet V2RayVPNServiceSupportsSet
statsManager v2stats.Manager
dialer *VPN.ProtectedDialer
status *CoreI.Status
escorter *Escort.Escorting
v2rayOP *sync.Mutex
closeChan chan struct{}
PackageName string
PackageCodePath string
DomainName string
ConfigureFileContent string
EnableLocalDNS bool
ForwardIpv6 bool
}
/*V2RayVPNServiceSupportsSet To support Android VPN mode*/
type V2RayVPNServiceSupportsSet interface {
Setup(Conf string) int
Prepare() int
Shutdown() int
Protect(int) int
OnEmitStatus(int, string) int
SendFd() int
}
/*RunLoop Run V2Ray main loop
*/
func (v *V2RayPoint) RunLoop() (err error) {
v.v2rayOP.Lock()
defer v.v2rayOP.Unlock()
//Construct Context
v.status.PackageName = v.PackageName
v.status.PackageCodePath = v.PackageCodePath
if !v.status.IsRunning {
v.closeChan = make(chan struct{})
v.dialer.PrepareResolveChan()
go v.dialer.PrepareDomain(v.DomainName, v.closeChan)
go func() {
select {
// wait until resolved
case <-v.dialer.ResolveChan():
// shutdown VPNService if server name can not reolved
if !v.dialer.IsVServerReady() {
log.Println("vServer cannot resolved, shutdown")
v.StopLoop()
v.SupportSet.Shutdown()
}
// stop waiting if manually closed
case <-v.closeChan:
}
}()
err = v.pointloop()
}
return
}
/*StopLoop Stop V2Ray main loop
*/
func (v *V2RayPoint) StopLoop() (err error) {
v.v2rayOP.Lock()
defer v.v2rayOP.Unlock()
if v.status.IsRunning {
close(v.closeChan)
v.shutdownInit()
v.SupportSet.OnEmitStatus(0, "Closed")
}
return
}
//Delegate Funcation
func (v *V2RayPoint) GetIsRunning() bool {
return v.status.IsRunning
}
//Delegate Funcation
func (v V2RayPoint) QueryStats(tag string, direct string) int64 {
if v.statsManager == nil {
return 0
}
counter := v.statsManager.GetCounter(fmt.Sprintf("outbound>>>%s>>>traffic>>>%s", tag, direct))
if counter == nil {
return 0
}
return counter.Set(0)
}
func (v *V2RayPoint) shutdownInit() {
v.status.IsRunning = false
v.status.Vpoint.Close()
v.status.Vpoint = nil
v.statsManager = nil
v.escorter.EscortingDown()
}
func (v *V2RayPoint) pointloop() error {
if err := v.runTun2socks(); err != nil {
log.Println(err)
return err
}
log.Printf("EnableLocalDNS: %v\nForwardIpv6: %v\nDomainName: %s",
v.EnableLocalDNS,
v.ForwardIpv6,
v.DomainName)
log.Println("loading v2ray config")
config, err := v2serial.LoadJSONConfig(strings.NewReader(v.ConfigureFileContent))
if err != nil {
log.Println(err)
return err
}
log.Println("new v2ray core")
inst, err := v2core.New(config)
if err != nil {
log.Println(err)
return err
}
v.status.Vpoint = inst
v.statsManager = inst.GetFeature(v2stats.ManagerType()).(v2stats.Manager)
log.Println("start v2ray core")
v.status.IsRunning = true
if err := v.status.Vpoint.Start(); err != nil {
v.status.IsRunning = false
log.Println(err)
return err
}
v.SupportSet.Prepare()
v.SupportSet.Setup(v.status.GetVPNSetupArg(v.EnableLocalDNS, v.ForwardIpv6))
v.SupportSet.OnEmitStatus(0, "Running")
return nil
}
func initV2Env() {
if os.Getenv(v2Assert) != "" {
return
}
//Initialize asset API, Since Raymond Will not let notify the asset location inside Process,
//We need to set location outside V2Ray
os.Setenv(v2Assert, assetperfix)
//Now we handle read
v2filesystem.NewFileReader = func(path string) (io.ReadCloser, error) {
if strings.HasPrefix(path, assetperfix) {
p := path[len(assetperfix)+1:]
//is it overridden?
//by, ok := overridedAssets[p]
//if ok {
// return os.Open(by)
//}
return mobasset.Open(p)
}
return os.Open(path)
}
}
//Delegate Funcation
func TestConfig(ConfigureFileContent string) error {
initV2Env()
_, err := v2serial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
return err
}
/*NewV2RayPoint new V2RayPoint*/
func NewV2RayPoint(s V2RayVPNServiceSupportsSet) *V2RayPoint {
initV2Env()
// inject our own log writer
v2applog.RegisterHandlerCreator(v2applog.LogType_Console,
func(lt v2applog.LogType,
options v2applog.HandlerCreatorOptions) (v2commlog.Handler, error) {
return v2commlog.NewLogger(createStdoutLogWriter()), nil
})
dialer := VPN.NewPreotectedDialer(s)
v2internet.UseAlternativeSystemDialer(dialer)
status := &CoreI.Status{}
return &V2RayPoint{
SupportSet: s,
v2rayOP: new(sync.Mutex),
status: status,
dialer: dialer,
escorter: &Escort.Escorting{Status: status},
}
}
func (v V2RayPoint) runTun2socks() error {
shipb := shippedBinarys.FirstRun{Status: v.status}
if err := shipb.CheckAndExport(); err != nil {
log.Println(err)
return err
}
v.escorter.EscortingUp()
go v.escorter.EscortRun(
v.status.GetApp("libtun2socks.so"),
v.status.GetTun2socksArgs(v.EnableLocalDNS, v.ForwardIpv6), "",
v.SupportSet.SendFd)
return nil
}
/*CheckVersion int
This func will return libv2ray binding version.
*/
func CheckVersion() int {
return CoreI.CheckVersion()
}
/*CheckVersionX string
This func will return libv2ray binding version and V2Ray version used.
*/
func CheckVersionX() string {
return fmt.Sprintf("Libv2rayLite V%d, Core V%s", CheckVersion(), v2core.Version())
}
+3
View File
@@ -0,0 +1,3 @@
package libv2ray
//go:generate make all
+1
View File
@@ -0,0 +1 @@
readme.txt
@@ -0,0 +1,13 @@
Platdep=shippedBinary.386 shippedBinary.amd64 shippedBinary.arm64 shippedBinary.arm
shippedBinaryDep:
go get -u github.com/jteeuwen/go-bindata/...
shippedBinary.%:
go-bindata -nometadata -nomemcopy -pkg shippedBinarys -o ./binary_$*.go -tags $* ArchIndep/ ArchDep/$*/
shippedBinary:shippedBinaryDep $(Platdep)
@echo "Done"
clean:
-rm binary*
@@ -0,0 +1,69 @@
package shippedBinarys
import (
"log"
"os"
"strconv"
"github.com/2dust/AndroidLibV2rayLite/CoreI"
)
type FirstRun struct {
Status *CoreI.Status
}
func (v *FirstRun) checkIfRcExist() error {
datadir := v.Status.GetDataDir()
if _, err := os.Stat(datadir + strconv.Itoa(CoreI.CheckVersion())); !os.IsNotExist(err) {
log.Println("file exists")
return nil
}
IndepDir, err := AssetDir("ArchIndep")
log.Println(IndepDir)
if err != nil {
return err
}
for _, fn := range IndepDir {
log.Println(datadir+"ArchIndep/"+fn)
err := RestoreAsset(datadir, "ArchIndep/"+fn)
log.Println(err)
//GrantPremission
os.Chmod(datadir+"ArchIndep/"+fn, 0700)
log.Println(os.Remove(datadir + fn))
log.Println(os.Symlink(datadir+"ArchIndep/"+fn, datadir + fn))
}
DepDir, err := AssetDir("ArchDep")
log.Println(DepDir)
if err != nil {
return err
}
for _, fn := range DepDir {
DepDir2, err := AssetDir("ArchDep/" + fn)
log.Println("ArchDep/" + fn)
if err != nil {
return err
}
for _, FND := range DepDir2 {
log.Println(datadir+"ArchDep/"+fn+"/"+FND)
RestoreAsset(datadir, "ArchDep/"+fn+"/"+FND)
os.Chmod(datadir+"ArchDep/"+fn+"/"+FND, 0700)
log.Println(os.Remove(datadir + FND))
log.Println(os.Symlink(datadir+"ArchDep/"+fn+"/"+FND, datadir+FND))
}
}
s, _ := os.Create(datadir + strconv.Itoa(CoreI.CheckVersion()))
s.Close()
return nil
}
func (v *FirstRun) CheckAndExport() error {
return v.checkIfRcExist()
}
+128
View File
@@ -0,0 +1,128 @@
# Copyright (C) 2009 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.
#
#
LOCAL_PATH := $(call my-dir)
ROOT_PATH := $(LOCAL_PATH)
########################################################
## libancillary
########################################################
include $(CLEAR_VARS)
ANCILLARY_SOURCE := fd_recv.c fd_send.c
LOCAL_MODULE := libancillary
#LOCAL_CFLAGS += -I$(LOCAL_PATH)/libancillary
LOCAL_C_INCLUDES := $(LOCAL_PATH)/libancillary
LOCAL_SRC_FILES := $(addprefix libancillary/, $(ANCILLARY_SOURCE))
include $(BUILD_STATIC_LIBRARY)
########################################################
## tun2socks
########################################################
include $(CLEAR_VARS)
LOCAL_CFLAGS := -std=gnu99
LOCAL_CFLAGS += -DBADVPN_THREADWORK_USE_PTHREAD -DBADVPN_LINUX -DBADVPN_BREACTOR_BADVPN -D_GNU_SOURCE
LOCAL_CFLAGS += -DBADVPN_USE_SIGNALFD -DBADVPN_USE_EPOLL
LOCAL_CFLAGS += -DBADVPN_LITTLE_ENDIAN -DBADVPN_THREAD_SAFE
LOCAL_CFLAGS += -DNDEBUG -DANDROID
LOCAL_CFLAGS += -I
LOCAL_STATIC_LIBRARIES := libancillary
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/badvpn/libancillary \
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv4 \
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv6 \
$(LOCAL_PATH)/badvpn/lwip/src/include \
$(LOCAL_PATH)/badvpn/lwip/custom \
$(LOCAL_PATH)/badvpn \
$(LOCAL_PATH)/libancillary
TUN2SOCKS_SOURCES := \
base/BLog_syslog.c \
system/BReactor_badvpn.c \
system/BSignal.c \
system/BConnection_common.c \
system/BConnection_unix.c \
system/BTime.c \
system/BUnixSignal.c \
system/BNetwork.c \
flow/StreamRecvInterface.c \
flow/PacketRecvInterface.c \
flow/PacketPassInterface.c \
flow/StreamPassInterface.c \
flow/SinglePacketBuffer.c \
flow/BufferWriter.c \
flow/PacketBuffer.c \
flow/PacketStreamSender.c \
flow/PacketPassConnector.c \
flow/PacketProtoFlow.c \
flow/PacketPassFairQueue.c \
flow/PacketProtoEncoder.c \
flow/PacketProtoDecoder.c \
socksclient/BSocksClient.c \
tuntap/BTap.c \
lwip/src/core/udp.c \
lwip/src/core/memp.c \
lwip/src/core/init.c \
lwip/src/core/pbuf.c \
lwip/src/core/tcp.c \
lwip/src/core/tcp_out.c \
lwip/src/core/netif.c \
lwip/src/core/def.c \
lwip/src/core/ip.c \
lwip/src/core/mem.c \
lwip/src/core/tcp_in.c \
lwip/src/core/stats.c \
lwip/src/core/inet_chksum.c \
lwip/src/core/timeouts.c \
lwip/src/core/ipv4/icmp.c \
lwip/src/core/ipv4/igmp.c \
lwip/src/core/ipv4/ip4_addr.c \
lwip/src/core/ipv4/ip4_frag.c \
lwip/src/core/ipv4/ip4.c \
lwip/src/core/ipv4/autoip.c \
lwip/src/core/ipv6/ethip6.c \
lwip/src/core/ipv6/inet6.c \
lwip/src/core/ipv6/ip6_addr.c \
lwip/src/core/ipv6/mld6.c \
lwip/src/core/ipv6/dhcp6.c \
lwip/src/core/ipv6/icmp6.c \
lwip/src/core/ipv6/ip6.c \
lwip/src/core/ipv6/ip6_frag.c \
lwip/src/core/ipv6/nd6.c \
lwip/custom/sys.c \
tun2socks/tun2socks.c \
base/DebugObject.c \
base/BLog.c \
base/BPending.c \
system/BDatagram_unix.c \
flowextra/PacketPassInactivityMonitor.c \
tun2socks/SocksUdpGwClient.c \
udpgw_client/UdpGwClient.c
LOCAL_MODULE := tun2socks
LOCAL_LDLIBS := -ldl -llog
LOCAL_SRC_FILES := $(addprefix badvpn/, $(TUN2SOCKS_SOURCES))
include $(BUILD_SYSTEM)/build-executable.mk
@@ -0,0 +1,79 @@
#!/bin/bash
# Thanks to https://gist.github.com/wenzhixin/43cf3ce909c24948c6e7
# Execute this script in your home directory. Lines 17 and 21 will prompt you for a y/n
# Install Oracle JDK 8
apt-get update
apt-get install -y openjdk-8-jdk
apt-get install -y unzip make expect # NDK stuff
# Get SDK tools (link from https://developer.android.com/studio/index.html#downloads)
wget -q https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
mkdir android-sdk-linux
unzip sdk*.zip -d android-sdk-linux
# Get NDK (https://developer.android.com/ndk/downloads/index.html)
# wget -q https://dl.google.com/android/repository/android-ndk-r15c-linux-x86_64.zip
# unzip android-ndk*.zip >> /dev/null
ACCEPT_LICENSES_URL=https://gist.githubusercontent.com/xiaokangwang/1489fd223d26581bfec92adb3cb0088e/raw/328eb6925099df5aae3e76790f8232f0fc378f8b/accept-licenses
ACCEPT_LICENSES_ITEM="android-sdk-license-bcbbd656|intel-android-sysimage-license-1ea702d1|android-sdk-license-2742d1c5"
# Let it update itself and install some stuff
cd android-sdk-linux/tools
curl -L -o accept-licenses $ACCEPT_LICENSES_URL
chmod +x accept-licenses
./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui" $ACCEPT_LICENSES_ITEM >/dev/null
# Download every build-tools version that has ever existed
# This will save you time! Thank me later for this
#./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui --filter 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27" $ACCEPT_LICENSES_ITEM
PACKAGE_PARSE_URL=https://gist.githubusercontent.com/xiaokangwang/06268fb23034ed94bc301880e862da09/raw/afd95cbbe2f8c1d9e7b0277b7c5ef39af756a6ee/parse.awk
reduceout=https://gist.githubusercontent.com/xiaokangwang/4684bdb5c3415b943f52aa4803386480/raw/b46dab1cc60f02c0d87f88f01e27157034218faa/out.awk
cd bin
curl -L -o parse.awk $PACKAGE_PARSE_URL
curl -L -o reduce.awk $reduceout
sudo apt-get install gawk
./sdkmanager --verbose --list |awk -f parse.awk > ~/package_to_install
readarray -t filenames < $HOME/package_to_install
cat $HOME/package_to_install
yes|./sdkmanager --verbose "${filenames[@]}" |awk -f reduce.awk
# If you need additional packages for your app, check available packages with:
# ./android list sdk --all
# install certain packages with:
# ./android update sdk --no-ui --all --filter 1,2,3,<...>,N
# where N is the number of the package in the list (see previous command)
./sdkmanager "ndk-bundle"
# Add the directory containing executables in PATH so that they can be found
echo 'export ANDROID_HOME=$HOME/android-sdk-linux' >> ~/.bashrc
echo 'export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools' >> ~/.bashrc
# echo 'export NDK_HOME=$HOME/android-ndk-r15c' >> ~/.bashrc
# echo 'export ANDROID_NDK_HOME=$NDK_HOME' >> ~/.bashrc
source ~/.bashrc
# Make sure you can execute 32 bit executables if this is 64 bit machine, otherwise skip this
dpkg --add-architecture i386
apt-get update
apt-get install -y libc6:i386 libstdc++6:i386 zlib1g:i386
+32
View File
@@ -0,0 +1,32 @@
package libv2ray
// This struct creates our own log writer without datatime stamp
// As Android adds time stamps on each line
import (
"log"
"os"
v2commlog "v2ray.com/core/common/log"
)
type consoleLogWriter struct {
logger *log.Logger
}
func (w *consoleLogWriter) Write(s string) error {
w.logger.Print(s)
return nil
}
func (w *consoleLogWriter) Close() error {
return nil
}
// This logger won't print data/time stamps
func createStdoutLogWriter() v2commlog.WriterCreator {
return func() v2commlog.Writer {
return &consoleLogWriter{
logger: log.New(os.Stdout, "", 0)}
}
}
+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 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
+7 -18
View File
@@ -1,36 +1,25 @@
# 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 V2Ray client for Android
[![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.0-blue.svg)](https://kotlinlang.org)
[![API](https://img.shields.io/badge/API-17%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/jelly-bean#android-4.2)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-1.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)
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
</a>
### Telegram Channel
[github_2dust](https://t.me/github_2dust)
### Usage
#### Geoip and Geosite
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
See our [wiki](https://github.com/2dust/v2rayNG/wiki)
### Development guide
Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite).
For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile) and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
The aar can be compiled from the Golang project under AndroidLibV2rayLite folder. For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile)
and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
v2rayNG can run on Android Emulators. For WSA, VPN permission need to be granted via
`appops set [package name] ACTIVATE_VPN allow`
v2rayNG can run on Android Emulators, with minimum Android 5.0
-1
View File
@@ -7,4 +7,3 @@
/captures
*.apk
signing.properties
*.aar
+117
View File
@@ -0,0 +1,117 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion Integer.parseInt("$compileSdkVer")
buildToolsVersion buildToolsVer
compileOptions {
targetCompatibility = "8"
sourceCompatibility = "8"
}
defaultConfig {
applicationId "com.v2ray.ang"
minSdkVersion 17
targetSdkVersion Integer.parseInt("$targetSdkVer")
multiDexEnabled true
versionCode 212
versionName "1.0.2"
}
buildTypes {
release {
minifyEnabled false
zipAlignEnabled false
shrinkResources false
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
zipAlignEnabled false
shrinkResources false
}
}
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')
testImplementation 'junit:junit:4.13'
implementation project(':dpreference')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
// 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"
implementation "com.android.support:multidex:1.0.3"
implementation 'com.android.support.constraint:constraint-layout:2.0.1'
// 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.6'
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(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'
}
}
-151
View File
@@ -1,151 +0,0 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("com.google.android.gms.oss-licenses-plugin")
}
android {
namespace = "com.v2ray.ang"
compileSdk = 35
defaultConfig {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 35
versionCode = 622
versionName = "1.9.26"
multiDexEnabled = true
splits {
abi {
isEnable = true
include(
"arm64-v8a",
"armeabi-v7a",
"x86_64",
"x86"
)
isUniversalApk = true
}
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
sourceSets {
getByName("main") {
jniLibs.srcDirs("libs")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
applicationVariants.all {
val variant = this
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 {
viewBinding = true
buildConfig = true
}
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)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.preference.ktx)
implementation(libs.recyclerview)
// UI Libraries
implementation(libs.material)
implementation(libs.toastcompat)
implementation(libs.editorkit)
implementation(libs.flexbox)
// Data and Storage Libraries
implementation(libs.mmkv.static)
implementation(libs.gson)
// Reactive and Utility Libraries
implementation(libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxpermissions)
// Language and Processing Libraries
implementation(libs.language.base)
implementation(libs.language.json)
// Intent and Utility Libraries
implementation(libs.quickie.bundled)
implementation(libs.core)
// AndroidX Lifecycle and Architecture Components
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.runtime.ktx)
// Background Task Libraries
implementation(libs.work.runtime.ktx)
implementation(libs.work.multiprocess)
// Multidex Support
implementation(libs.multidex)
// Testing Libraries
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
testImplementation(libs.org.mockito.mockito.inline)
testImplementation(libs.mockito.kotlin)
// Oss Licenses
implementation(libs.play.services.oss.licenses)
}
+1
View File
@@ -0,0 +1 @@
https://github.com/2dust/v2rayNG/tree/master/AndroidLibV2rayLite
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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>
+54 -174
View File
@@ -1,72 +1,48 @@
<?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" />
android:anyDensity="true"
android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:xlargeScreens="true"/>
<uses-sdk
android:minSdkVersion="21"
tools:overrideLibrary="com.blacksquircle.ui.editorkit" />
<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.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="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_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" />
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
<!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
android:minSdkVersion="34" />
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".AngApplication"
android:allowBackup="true"
android:banner="@mipmap/ic_banner"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppThemeDayNight"
android:usesCleartextTraffic="true"
tools:targetApi="m">
android:theme="@style/AppTheme">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/AppThemeDayNight.NoActionBar">
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" />
@@ -78,81 +54,38 @@
</activity>
<activity
android:name=".ui.ServerActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged" />
<activity
android:name=".ui.ServerCustomConfigActivity"
android:exported="false"
android:name=".ui.Server2Activity"
android:windowSoftInputMode="stateUnchanged" />
<activity
android:name=".ui.SettingsActivity"
android:exported="false" />
android:name=".ui.Server3Activity"
android:windowSoftInputMode="stateUnchanged" />
<activity
android:name=".ui.PerAppProxyActivity"
android:exported="false" />
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.ScannerActivity"
android:exported="false" />
<activity
android:name=".ui.LogcatActivity"
android:exported="false" />
<activity
android:name=".ui.RoutingSettingActivity"
android:exported="false" />
<activity
android:name=".ui.RoutingEditActivity"
android:exported="false" />
<activity
android:name=".ui.SubSettingActivity"
android:exported="false" />
<activity
android:name=".ui.UserAssetActivity"
android:exported="false" />
<activity
android:name=".ui.UserAssetUrlActivity"
android:exported="false" />
android:name=".ui.RoutingSettingsActivity"
android:windowSoftInputMode="stateUnchanged" />
<activity android:name=".ui.SubSettingActivity" />
<activity
android:name=".ui.SubEditActivity"
android:exported="false" />
<activity
android:name=".ui.ScScannerActivity"
android:exported="false" />
<activity android:name=".ui.SubEditActivity" />
<activity android:name=".ui.ScScannerActivity" />
<activity
android:name=".ui.ScSwitchActivity"
android:excludeFromRecents="true"
android:exported="false"
android:process=":RunSoLibV2RayDaemon"
android:theme="@style/AppTheme.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.AboutActivity"
android:exported="false" />
<service
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">
@@ -162,106 +95,53 @@
<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.V2RayProxyOnlyService"
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 android:name=".service.V2RayProxyOnlyService"
android:exported="false"
android:label="@string/app_name"
android:process=":RunSoLibV2RayDaemon">
</service>
<service
android:name=".service.V2RayTestService"
android:exported="false"
android:process=":RunSoLibV2RayDaemon" />
<receiver
android:name=".receiver.WidgetProvider"
android:exported="true"
android:process=":RunSoLibV2RayDaemon">
<receiver android:name=".receiver.WidgetProvider"
android:process=":RunSoLibV2RayDaemon">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/app_widget_provider" />
android:name="android.appwidget.provider"
android:resource="@xml/app_widget_provider" />
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="com.v2ray.ang.action.widget.click" />
<action android:name="com.v2ray.ang.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" />
</intent-filter>
</receiver>
<service
android:name=".service.QSTileService"
android:exported="true"
android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name"
android:label="@string/app_tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:process=":RunSoLibV2RayDaemon">
android:name=".service.QSTileService"
android:icon="@drawable/ic_v"
android:label="@string/app_tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:process=":RunSoLibV2RayDaemon">
<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.TaskerActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher">
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"
android:process=":RunSoLibV2RayDaemon">
<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>
<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>
@@ -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,149 +0,0 @@
[
{
"remarks": "绕过bittorrent",
"outboundTag": "direct",
"protocol": [
"bittorrent"
]
},
{
"remarks": "Google cn",
"outboundTag": "proxy",
"domain": [
"domain:googleapis.cn",
"domain:gstatic.com"
]
},
{
"remarks": "阻断udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"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,34 +0,0 @@
[
{
"remarks": "阻断udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "最终代理",
"port": "0-65535",
"outboundTag": "proxy"
}
]
@@ -1,108 +0,0 @@
[
{
"remarks": "Google cn",
"outboundTag": "proxy",
"domain": [
"domain:googleapis.cn",
"domain:gstatic.com"
]
},
{
"remarks": "阻断udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"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"
]
},
{
"remarks": "最终代理",
"port": "0-65535",
"outboundTag": "proxy"
}
]
@@ -1,49 +0,0 @@
[
{
"remarks": "Block udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "Block ads and trackers",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"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"
]
},
{
"remarks": "Final Agent",
"port": "0-65535",
"outboundTag": "proxy"
}
]
@@ -54,7 +54,7 @@
"users": [
{
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
"alterId": 0,
"alterId": 64,
"security": "auto",
"level": 8
}
@@ -81,9 +81,7 @@
},
{
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIP"
},
"settings": {},
"tag": "direct"
},
{
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 28 KiB

@@ -1,48 +0,0 @@
package com.v2ray.ang
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.multidex.MultiDexApplication
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.SettingsManager
import com.v2ray.ang.util.Utils
class AngApplication : MultiDexApplication() {
companion object {
//const val PREF_LAST_VERSION = "pref_last_version"
lateinit var application: AngApplication
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
application = this
}
private val workManagerConfiguration: Configuration = Configuration.Builder()
.setDefaultProcessName("${ANG_PACKAGE}:bg")
.build()
override fun onCreate() {
super.onCreate()
// LeakCanary.install(this)
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
// firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
// if (firstRun)
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
MMKV.initialize(this)
Utils.setNightMode()
// Initialize WorkManager with the custom configuration
WorkManager.initialize(this, workManagerConfiguration)
SettingsManager.initRoutingRulesets(this)
}
}
@@ -1,186 +0,0 @@
package com.v2ray.ang
object AppConfig {
/** The application's package name. */
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
/** Directory names used in the app's file system. */
const val DIR_ASSETS = "assets"
const val DIR_BACKUPS = "backups"
/** Legacy configuration keys. */
const val ANG_CONFIG = "ang_config"
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
/** 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_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 SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
const val PREF_LANGUAGE = "pref_language"
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
const val PREF_SOCKS_PORT = "pref_socks_port"
const val PREF_REMOTE_DNS = "pref_remote_dns"
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
const val PREF_LOGLEVEL = "pref_core_loglevel"
const val PREF_MODE = "pref_mode"
const val PREF_IS_BOOTED = "pref_is_booted"
/** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
/** Protocol identifiers. */
const val PROTOCOL_FREEDOM: String = "freedom"
/** Broadcast actions. */
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.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"
/** Network-related constants. */
const val UPLINK = "uplink"
const val DOWNLINK = "downlink"
/** URLs for various resources. */
const val androidpackagenamelistUrl =
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
const val v2rayCustomRoutingListUrl =
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
const val v2rayNGIssues = "$v2rayNGUrl/issues"
const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode"
const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md"
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
const val TgChannelUrl = "https://t.me/github_2dust"
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
const val DelayTestUrl2 = "https://www.google.com/generate_204"
/** DNS server addresses. */
const val DNS_PROXY = "1.1.1.1"
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"
/** 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_SUCCESS = 61
const val MSG_MEASURE_CONFIG = 7
const val MSG_MEASURE_CONFIG_SUCCESS = 71
const val MSG_MEASURE_CONFIG_CANCEL = 72
/** Notification channel IDs and names. */
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
const val RAY_NG_CHANNEL_NAME = "v2rayNG Background Service"
const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel"
const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service"
/** Protocols Scheme **/
const val VMESS = "vmess://"
const val CUSTOM = ""
const val SHADOWSOCKS = "ss://"
const val SOCKS = "socks://"
const val HTTP = "http://"
const val VLESS = "vless://"
const val TROJAN = "trojan://"
const val WIREGUARD = "wireguard://"
const val TUIC = "tuic://"
const val HYSTERIA2 = "hysteria2://"
const val HY2 = "hy2://"
/** Give a good name to this, IDK*/
const val VPN = "VPN"
// 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_DNSPOD_DOMAIN = "dot.pub"
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one"
const val DNS_GOOGLE_DOMAIN = "dns.google"
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto"
const val DEFAULT_LEVEL = 8
const val DEFAULT_NETWORK = "tcp"
const val TLS = "tls"
const val REALITY = "reality"
const val HEADER_TYPE_HTTP = "http"
}
@@ -1,11 +0,0 @@
package com.v2ray.ang.dto
import android.graphics.drawable.Drawable
data class AppInfo(
val appName: String,
val packageName: String,
val appIcon: Drawable,
val isSystemApp: Boolean,
var isSelected: Int
)
@@ -1,8 +0,0 @@
package com.v2ray.ang.dto
data class AssetUrlItem(
var remarks: String = "",
var url: String = "",
val addedTime: Long = System.currentTimeMillis(),
var lastUpdated: Long = -1
)
@@ -1,9 +0,0 @@
package com.v2ray.ang.dto
data class ConfigResult(
var status: Boolean,
var guid: String? = null,
var content: String = "",
var domainPort: String? = null,
)
@@ -1,21 +0,0 @@
package com.v2ray.ang.dto
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),
HTTP(10, AppConfig.HTTP);
companion object {
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
}
}
@@ -1,40 +0,0 @@
package com.v2ray.ang.dto
data class Hysteria2Bean(
val server: String?,
val auth: String?,
val lazy: Boolean? = true,
val obfs: ObfsBean? = null,
val socks5: Socks5Bean? = null,
val http: Socks5Bean? = null,
val tls: TlsBean? = null,
val transport: TransportBean? = null,
) {
data class ObfsBean(
val type: String?,
val salamander: SalamanderBean?
) {
data class SalamanderBean(
val password: String?,
)
}
data class Socks5Bean(
val listen: String?,
)
data class TlsBean(
val sni: String?,
val insecure: Boolean?,
val pinSHA256: String?,
)
data class TransportBean(
val type: String?,
val udp: TransportUdpBean?
) {
data class TransportUdpBean(
val hopInterval: String?,
)
}
}
@@ -1,19 +0,0 @@
package com.v2ray.ang.dto
enum class Language(val code: String) {
AUTO("auto"),
ENGLISH("en"),
CHINA("zh-rCN"),
TRADITIONAL_CHINESE("zh-rTW"),
VIETNAMESE("vi"),
RUSSIAN("ru"),
PERSIAN("fa"),
BANGLA("bn"),
BAKHTIARI("bqi-rIR");
companion object {
fun fromCode(code: String): Language {
return entries.find { it.code == code } ?: AUTO
}
}
}
@@ -1,17 +0,0 @@
package com.v2ray.ang.dto
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");
companion object {
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
}
}
@@ -1,113 +0,0 @@
package com.v2ray.ang.dto
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.util.Utils
data class ProfileItem(
val configVersion: Int = 4,
val configType: EConfigType,
var subscriptionId: String = "",
var addedTime: Long = System.currentTimeMillis(),
var remarks: String = "",
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 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 security: String? = null,
var sni: String? = null,
var alpn: String? = null,
var fingerPrint: String? = null,
var insecure: Boolean? = null,
var publicKey: String? = null,
var shortId: String? = null,
var spiderX: 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,
var pinSHA256: String? = null,
) {
companion object {
fun create(configType: EConfigType): ProfileItem {
return ProfileItem(configType = configType)
}
}
fun getAllOutboundTags(): MutableList<String> {
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
}
fun getServerAddressAndPort(): String {
return Utils.getIpv6Address(server) + ":" + serverPort
}
override fun equals(other: Any?): Boolean {
if (other == null) return false
val obj = other as ProfileItem
return (this.server == obj.server
&& this.serverPort == obj.serverPort
&& this.password == obj.password
&& this.method == obj.method
&& this.flow == obj.flow
&& this.username == obj.username
&& this.network == obj.network
&& this.headerType == obj.headerType
&& this.host == obj.host
&& this.path == obj.path
&& this.seed == obj.seed
&& this.quicSecurity == obj.quicSecurity
&& this.quicKey == obj.quicKey
&& this.mode == obj.mode
&& this.serviceName == obj.serviceName
&& this.authority == obj.authority
&& this.xhttpMode == obj.xhttpMode
&& this.security == obj.security
&& this.sni == obj.sni
&& this.alpn == obj.alpn
&& this.fingerPrint == obj.fingerPrint
&& this.publicKey == obj.publicKey
&& this.shortId == obj.shortId
&& this.secretKey == obj.secretKey
&& this.localAddress == obj.localAddress
&& this.reserved == obj.reserved
&& this.mtu == obj.mtu
&& this.obfsPassword == obj.obfsPassword
&& this.portHopping == obj.portHopping
&& this.portHoppingInterval == obj.portHoppingInterval
&& this.pinSHA256 == obj.pinSHA256
)
}
}
@@ -1,9 +0,0 @@
package com.v2ray.ang.dto
data class ProfileLiteItem(
val configType: EConfigType,
var subscriptionId: String = "",
var remarks: String = "",
var server: String?,
var serverPort: Int?,
)
@@ -1,20 +0,0 @@
package com.v2ray.ang.dto
enum class RoutingType(val fileName: String) {
WHITE("custom_routing_white"),
BLACK("custom_routing_black"),
GLOBAL("custom_routing_global"),
WHITE_IRAN("custom_routing_white_iran");
companion object {
fun fromIndex(index: Int): RoutingType {
return when (index) {
0 -> WHITE
1 -> BLACK
2 -> GLOBAL
3 -> WHITE_IRAN
else -> WHITE
}
}
}
}
@@ -1,13 +0,0 @@
package com.v2ray.ang.dto
data class RulesetItem(
var remarks: String? = "",
var ip: List<String>? = null,
var domain: 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,10 +0,0 @@
package com.v2ray.ang.dto
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
fun getTestDelayString(): String {
if (testDelayMillis == 0L) {
return ""
}
return testDelayMillis.toString() + "ms"
}
}
@@ -1,86 +0,0 @@
package com.v2ray.ang.dto
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_PROXY
data class ServerConfig(
val configVersion: Int = 3,
val configType: EConfigType,
var subscriptionId: String = "",
val addedTime: Long = System.currentTimeMillis(),
var remarks: String = "",
val outboundBean: V2rayConfig.OutboundBean? = null,
var fullConfig: V2rayConfig? = null
) {
companion object {
fun create(configType: EConfigType): ServerConfig {
when (configType) {
EConfigType.VMESS,
EConfigType.VLESS ->
return ServerConfig(
configType = configType,
outboundBean = V2rayConfig.OutboundBean(
protocol = configType.name.lowercase(),
settings = V2rayConfig.OutboundBean.OutSettingsBean(
vnext = listOf(
V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())
)
)
),
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
)
)
EConfigType.CUSTOM ->
return ServerConfig(configType = configType)
EConfigType.SHADOWSOCKS,
EConfigType.SOCKS,
EConfigType.HTTP,
EConfigType.TROJAN,
EConfigType.HYSTERIA2 ->
return ServerConfig(
configType = configType,
outboundBean = V2rayConfig.OutboundBean(
protocol = configType.name.lowercase(),
settings = V2rayConfig.OutboundBean.OutSettingsBean(
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())
),
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
)
)
EConfigType.WIREGUARD ->
return ServerConfig(
configType = configType,
outboundBean = V2rayConfig.OutboundBean(
protocol = configType.name.lowercase(),
settings = V2rayConfig.OutboundBean.OutSettingsBean(
secretKey = "",
peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
)
)
)
}
}
}
fun getProxyOutbound(): V2rayConfig.OutboundBean? {
if (configType != EConfigType.CUSTOM) {
return outboundBean
}
return fullConfig?.getProxyOutbound()
}
fun getAllOutboundTags(): MutableList<String> {
if (configType != EConfigType.CUSTOM) {
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
}
fullConfig?.let { config ->
return config.outbounds.map { it.tag }.toMutableList()
}
return mutableListOf()
}
}
@@ -1,6 +0,0 @@
package com.v2ray.ang.dto
data class ServersCache(
val guid: String,
val profile: ProfileItem
)
@@ -1,15 +0,0 @@
package com.v2ray.ang.dto
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,
val updateInterval: Int? = null,
var prevProfile: String? = null,
var nextProfile: String? = null,
var filter: String? = null,
)
@@ -1,727 +0,0 @@
package com.v2ray.ang.dto
import android.text.TextUtils
import com.google.gson.GsonBuilder
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.*
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.*
import com.v2ray.ang.util.Utils
import java.lang.reflect.Type
data class V2rayConfig(
var remarks: String? = null,
var stats: Any? = null,
val log: LogBean,
var policy: PolicyBean?,
val inbounds: ArrayList<InboundBean>,
var outbounds: ArrayList<OutboundBean>,
var dns: DnsBean,
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,
val error: String,
var loglevel: String?,
val dnsLog: Boolean? = null
)
data class InboundBean(
var tag: String,
var port: Int,
var protocol: String,
var listen: String? = null,
val settings: Any? = null,
val sniffing: SniffingBean?,
val streamSettings: Any? = null,
val allocate: Any? = null
) {
data class InSettingsBean(
val auth: String? = null,
val udp: Boolean? = null,
val userLevel: Int? = null,
val address: String? = null,
val port: Int? = null,
val network: String? = null
)
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 proxySettings: Any? = null,
val sendThrough: String? = null,
var mux: MuxBean? = MuxBean(false)
) {
companion object {
fun create(configType: EConfigType): OutboundBean? {
return when (configType) {
EConfigType.VMESS,
EConfigType.VLESS ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
vnext = listOf(
VnextBean(
users = listOf(UsersBean())
)
)
),
streamSettings = StreamSettingsBean()
)
EConfigType.SHADOWSOCKS,
EConfigType.SOCKS,
EConfigType.HTTP,
EConfigType.TROJAN,
EConfigType.HYSTERIA2 ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
servers = listOf(ServersBean())
),
streamSettings = StreamSettingsBean()
)
EConfigType.WIREGUARD ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
secretKey = "",
peers = listOf(WireGuardBean())
)
)
EConfigType.CUSTOM -> null
}
}
}
data class OutSettingsBean(
var vnext: List<VnextBean>? = null,
var fragment: FragmentBean? = null,
var noises: List<NoiseBean>? = null,
var servers: List<ServersBean>? = null,
/*Blackhole*/
var response: Response? = null,
/*DNS*/
val network: String? = null,
var address: Any? = null,
val port: Int? = null,
/*Freedom*/
var domainStrategy: String? = null,
val redirect: String? = null,
val userLevel: Int? = null,
/*Loopback*/
val inboundTag: String? = null,
/*Wireguard*/
var secretKey: String? = null,
val peers: List<WireGuardBean>? = null,
var reserved: List<Int>? = null,
var mtu: Int? = null,
var obfsPassword: String? = null,
) {
data class VnextBean(
var address: String = "",
var port: Int = AppConfig.DEFAULT_PORT,
var users: List<UsersBean>
) {
data class UsersBean(
var id: String = "",
var alterId: Int? = null,
var security: String? = null,
var level: Int = AppConfig.DEFAULT_LEVEL,
var encryption: String? = null,
var flow: String? = null
)
}
data class FragmentBean(
var packets: String? = null,
var length: String? = null,
var interval: String? = null
)
data class NoiseBean(
var type: String? = null,
var packet: String? = null,
var delay: String? = null
)
data class ServersBean(
var address: String = "",
var method: String? = null,
var ota: Boolean = false,
var password: String? = null,
var port: Int = AppConfig.DEFAULT_PORT,
var level: Int = AppConfig.DEFAULT_LEVEL,
val email: String? = null,
var flow: String? = null,
val ivCheck: Boolean? = null,
var users: List<SocksUsersBean>? = null
) {
data class SocksUsersBean(
var user: String = "",
var pass: String = "",
var level: Int = AppConfig.DEFAULT_LEVEL
)
}
data class Response(var type: String)
data class WireGuardBean(
var publicKey: String = "",
var preSharedKey: String = "",
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 hy2steriaSettings: Hy2steriaSettingsBean? = 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: String? = 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,
var header: HeaderBean = HeaderBean(),
var seed: String? = null
) {
data class HeaderBean(var type: String = "none")
}
data class WsSettingsBean(
var path: String? = null,
var headers: HeadersBean = HeadersBean(),
val maxEarlyData: Int? = null,
val useBrowserForwarding: Boolean? = null,
val acceptProxyProtocol: Boolean? = null
) {
data class HeadersBean(var Host: String = "")
}
data class HttpupgradeSettingsBean(
var path: String? = null,
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
)
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,
// REALITY settings
val show: Boolean = false,
var publicKey: String? = null,
var shortId: String? = null,
var spiderX: 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 Hy2steriaSettingsBean(
var password: String? = null,
var use_udp_extension: Boolean? = true,
var congestion: Hy2CongestionBean? = null
) {
data class Hy2CongestionBean(
var type: String? = "bbr",
var up_mbps: Int? = null,
var down_mbps: Int? = null,
)
}
fun populateTransportSettings(
transport: String,
headerType: String?,
host: String?,
path: String?,
seed: String?,
quicSecurity: String?,
key: String?,
mode: String?,
serviceName: String?,
authority: String?
): String? {
var sni: String? = null
network = if (transport.isEmpty()) NetworkType.TCP.type else transport
when (network) {
NetworkType.TCP.type -> {
val tcpSetting = TcpSettingsBean()
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
tcpSetting.header.request = requestObj
sni = requestObj.headers.Host?.getOrNull(0)
}
} else {
tcpSetting.header.type = "none"
sni = host
}
tcpSettings = tcpSetting
}
NetworkType.KCP.type -> {
val kcpsetting = KcpSettingsBean()
kcpsetting.header.type = headerType ?: "none"
if (seed.isNullOrEmpty()) {
kcpsetting.seed = null
} else {
kcpsetting.seed = seed
}
kcpSettings = kcpsetting
}
NetworkType.WS.type -> {
val wssetting = WsSettingsBean()
wssetting.headers.Host = host.orEmpty()
sni = host
wssetting.path = path ?: "/"
wsSettings = wssetting
}
NetworkType.HTTP_UPGRADE.type -> {
val httpupgradeSetting = HttpupgradeSettingsBean()
httpupgradeSetting.host = host.orEmpty()
sni = host
httpupgradeSetting.path = path ?: "/"
httpupgradeSettings = httpupgradeSetting
}
NetworkType.XHTTP.type -> {
val xhttpSetting = XhttpSettingsBean()
xhttpSetting.host = host.orEmpty()
sni = host
xhttpSetting.path = path ?: "/"
xhttpSettings = xhttpSetting
}
NetworkType.H2.type, NetworkType.HTTP.type -> {
network = NetworkType.H2.type
val h2Setting = HttpSettingsBean()
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
sni = h2Setting.host.getOrNull(0)
h2Setting.path = path ?: "/"
httpSettings = h2Setting
}
// "quic" -> {
// val quicsetting = QuicSettingBean()
// quicsetting.security = quicSecurity ?: "none"
// quicsetting.key = key.orEmpty()
// quicsetting.header.type = headerType ?: "none"
// quicSettings = quicsetting
// }
NetworkType.GRPC.type -> {
val grpcSetting = GrpcSettingsBean()
grpcSetting.multiMode = mode == "multi"
grpcSetting.serviceName = serviceName.orEmpty()
grpcSetting.authority = authority.orEmpty()
grpcSetting.idle_timeout = 60
grpcSetting.health_check_timeout = 20
sni = authority
grpcSettings = grpcSetting
}
}
return sni
}
fun populateTlsSettings(
streamSecurity: String,
allowInsecure: Boolean,
sni: String?,
fingerprint: String?,
alpns: String?,
publicKey: String?,
shortId: String?,
spiderX: String?
) {
security = if (streamSecurity.isEmpty()) null else streamSecurity
if (security == null) return
val tlsSetting = TlsSettingsBean(
allowInsecure = allowInsecure,
serverName = if (sni.isNullOrEmpty()) null else sni,
fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
shortId = if (shortId.isNullOrEmpty()) null else shortId,
spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
)
if (security == AppConfig.TLS) {
tlsSettings = tlsSetting
realitySettings = null
} else if (security == AppConfig.REALITY) {
tlsSettings = null
realitySettings = tlsSetting
}
}
}
data class MuxBean(
var enabled: Boolean,
var concurrency: Int = 8,
var xudpConcurrency: Int = 8,
var xudpProxyUDP443: String = "",
)
fun getServerAddress(): String? {
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.first()?.address
} else 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.HYSTERIA2.name, true)
) {
return settings?.servers?.first()?.address
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
}
return null
}
fun getServerPort(): Int? {
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.first()?.port
} else 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.HYSTERIA2.name, true)
) {
return settings?.servers?.first()?.port
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
}
return null
}
fun getServerAddressAndPort(): String {
val address = getServerAddress().orEmpty()
val port = getServerPort()
return Utils.getIpv6Address(address) + ":" + port
}
fun getPassword(): String? {
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.first()?.users?.first()?.id
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
) {
return settings?.servers?.first()?.password
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.HTTP.name, true)
) {
return settings?.servers?.first()?.users?.first()?.pass
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.secretKey
}
return null
}
fun getSecurityEncryption(): String? {
return when {
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method
else -> null
}
}
fun getTransportSettingDetails(): List<String?>? {
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)
) {
val transport = streamSettings?.network ?: return null
return when (transport) {
NetworkType.TCP.type -> {
val tcpSetting = streamSettings?.tcpSettings ?: return null
listOf(
tcpSetting.header.type,
tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(),
tcpSetting.header.request?.path?.joinToString(",").orEmpty()
)
}
NetworkType.KCP.type -> {
val kcpSetting = streamSettings?.kcpSettings ?: return null
listOf(
kcpSetting.header.type,
"",
kcpSetting.seed.orEmpty()
)
}
NetworkType.WS.type -> {
val wsSetting = streamSettings?.wsSettings ?: return null
listOf(
"",
wsSetting.headers.Host,
wsSetting.path
)
}
NetworkType.HTTP_UPGRADE.type -> {
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
listOf(
"",
httpupgradeSetting.host,
httpupgradeSetting.path
)
}
NetworkType.XHTTP.type -> {
val xhttpSettings = streamSettings?.xhttpSettings ?: return null
listOf(
"",
xhttpSettings.host,
xhttpSettings.path
)
}
NetworkType.H2.type -> {
val h2Setting = streamSettings?.httpSettings ?: return null
listOf(
"",
h2Setting.host.joinToString(","),
h2Setting.path
)
}
// "quic" -> {
// val quicSetting = streamSettings?.quicSettings ?: return null
// listOf(
// quicSetting.header.type,
// quicSetting.security,
// quicSetting.key
// )
// }
NetworkType.GRPC.type -> {
val grpcSetting = streamSettings?.grpcSettings ?: return null
listOf(
if (grpcSetting.multiMode == true) "multi" else "gun",
grpcSetting.authority.orEmpty(),
grpcSetting.serviceName
)
}
else -> null
}
}
return null
}
}
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 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,
)
}
data class RoutingBean(
var domainStrategy: String,
var domainMatcher: String? = null,
var rules: ArrayList<RulesBean>,
val balancers: List<Any>? = null
) {
data class RulesBean(
var type: String = "field",
var ip: ArrayList<String>? = null,
var domain: ArrayList<String>? = null,
var outboundTag: String = "",
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 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 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 toPrettyPrinting(): String {
return GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
object : TypeToken<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
)
.create()
.toJson(this)
}
}
@@ -1,19 +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 = ""
)
@@ -1,98 +0,0 @@
package com.v2ray.ang.extension
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import com.v2ray.ang.AngApplication
import me.drakeet.support.toast.ToastCompat
import org.json.JSONObject
import java.io.Serializable
import java.net.URI
import java.net.URLConnection
val Context.v2RayApplication: AngApplication?
get() = applicationContext as? AngApplication
fun Context.toast(message: Int) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
}
fun Context.toast(message: CharSequence) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
}
fun JSONObject.putOpt(pair: Pair<String, Any?>) {
put(pair.first, pair.second)
}
fun JSONObject.putOpt(pairs: Map<String, Any?>) {
pairs.forEach { put(it.key, it.value) }
}
const val THRESHOLD = 1000L
const val DIVISOR = 1024.0
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
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("%.1f %s", size, units[unitIndex])
}
val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
contentLengthLong
} else {
contentLength.toLong()
}
val URI.idnHost: String
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
fun String.toLongEx(): Long = toLongOrNull() ?: 0
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
callback()
if (onetime) context.unregisterReceiver(this)
}
}.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}, Context.RECEIVER_EXPORTED)
} else {
registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
})
}
}
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
}
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
}
inline fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
@@ -1,21 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.JsonUtil
object CustomFmt : FmtBase() {
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,125 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.util.Utils
import java.net.URI
open class FmtBase {
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
val query = if (dicQuery != null)
("?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
else ""
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(userInfo ?: ""),
Utils.getIpv6Address(config.server),
config.serverPort
)
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
}
fun getQueryParam(uri: URI): Map<String, String> {
return uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
}
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
config.network = queryParam["type"] ?: NetworkType.TCP.type
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
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.security = queryParam["security"]
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
config.security = null
}
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
allowInsecure
} else {
queryParam["allowInsecure"].orEmpty() == "1"
}
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
config.publicKey = queryParam["pbk"]
config.shortId = queryParam["sid"]
config.spiderX = queryParam["spx"]
config.flow = queryParam["flow"]
}
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
val dicQuery = HashMap<String, String>()
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() }
config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() }
config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() }
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
val networkType = NetworkType.fromString(config.network)
dicQuery["type"] = networkType.type
when (networkType) {
NetworkType.TCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
}
NetworkType.KCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
}
NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
}
NetworkType.XHTTP -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
config.xhttpExtra.let { if (it.isNotNullEmpty()) dicQuery["extra"] = it.orEmpty() }
}
NetworkType.HTTP, NetworkType.H2 -> {
dicQuery["type"] = "http"
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
}
// NetworkType.QUIC -> {
// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
// config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
// config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
// }
NetworkType.GRPC -> {
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
}
}
return dicQuery
}
}
@@ -1,28 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.isNotNullEmpty
import kotlin.text.orEmpty
object HttpFmt : FmtBase() {
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HTTP)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = profileItem.username.orEmpty()
socksUsersBean.pass = profileItem.password.orEmpty()
server.users = listOf(socksUsersBean)
}
}
return outboundBean
}
}
@@ -1,120 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.Hysteria2Bean
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
object Hysteria2Fmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.security = AppConfig.TLS
if (!uri.rawQuery.isNullOrEmpty()) {
val queryParam = getQueryParam(uri)
config.security = queryParam["security"] ?: AppConfig.TLS
config.insecure = if (queryParam["insecure"].isNullOrEmpty()) {
allowInsecure
} else {
queryParam["insecure"].orEmpty() == "1"
}
config.sni = queryParam["sni"]
config.alpn = queryParam["alpn"]
config.obfsPassword = queryParam["obfs-password"]
config.portHopping = queryParam["mport"]
config.pinSHA256 = queryParam["pinSHA256"]
}
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>()
config.security.let { if (it != null) dicQuery["security"] = it }
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
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.pinSHA256.isNotNullEmpty()) {
dicQuery["pinSHA256"] = config.pinSHA256.orEmpty()
}
return toUri(config, config.password, dicQuery)
}
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
Hysteria2Bean.ObfsBean(
type = "salamander",
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
password = config.obfsPassword
)
)
val transport = if (config.portHopping.isNullOrEmpty()) null else
Hysteria2Bean.TransportBean(
type = "udp",
udp = Hysteria2Bean.TransportBean.TransportUdpBean(
hopInterval = (config.portHoppingInterval ?: "30") + "s"
)
)
val server =
if (config.portHopping.isNullOrEmpty())
config.getServerAddressAndPort()
else
Utils.getIpv6Address(config.server) + ":" + config.portHopping
val bean = Hysteria2Bean(
server = server,
auth = config.password,
obfs = obfs,
transport = transport,
socks5 = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
http = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
tls = Hysteria2Bean.TlsBean(
sni = config.sni ?: config.server,
insecure = config.insecure,
pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256
)
)
return bean
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
return outboundBean
}
}
@@ -1,139 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
return parseSip002(str) ?: parseLegacy(str)
}
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.urlDecode(uri.fragment.orEmpty())
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
}
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.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
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
}
fun toUri(config: ProfileItem): String {
val pw = "${config.method}:${config.password}"
return toUri(config, Utils.encode(pw), null)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.method = profileItem.method
}
val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}
@@ -1,62 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object SocksFmt : FmtBase() {
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.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
if (uri.userInfo?.isEmpty() == false) {
val result = Utils.decode(uri.userInfo).split(":", limit = 2)
if (result.count() == 2) {
config.username = result.first()
config.password = result.last()
}
}
return config
}
fun toUri(config: ProfileItem): String {
val pw =
if (config.username.isNotNullEmpty())
"${config.username}:${config.password}"
else
":"
return toUri(config, Utils.encode(pw), null)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = profileItem.username.orEmpty()
socksUsersBean.pass = profileItem.password.orEmpty()
server.users = listOf(socksUsersBean)
}
}
return outboundBean
}
}
@@ -1,81 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object TrojanFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
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 = allowInsecure
} else {
val queryParam = getQueryParam(uri)
getItemFormQuery(config, queryParam, allowInsecure)
config.security = queryParam["security"] ?: AppConfig.TLS
}
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
return toUri(config, config.password, dicQuery)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.flow = profileItem.flow
}
val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}
@@ -1,83 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VLESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = queryParam["encryption"] ?: "none"
getItemFormQuery(config, queryParam, allowInsecure)
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
dicQuery["encryption"] = config.method ?: "none"
return toUri(config, config.password, dicQuery)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VLESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].encryption = profileItem.method
vnext.users[0].flow = profileItem.flow
}
val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}
@@ -1,183 +0,0 @@
package com.v2ray.ang.fmt
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object VmessFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
return parseVmessStd(str)
}
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
return null
}
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
// 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)
) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.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 ?: 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.insecure = allowInsecure
config.sni = vmessQRCode.sni
config.fingerPrint = vmessQRCode.fp
config.alpn = vmessQRCode.alpn
return config
}
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.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
vmessQRCode.tls = config.security.orEmpty()
vmessQRCode.sni = config.sni.orEmpty()
vmessQRCode.fp = config.fingerPrint.orEmpty()
vmessQRCode.alpn = config.alpn.orEmpty()
val json = JsonUtil.toJson(vmessQRCode)
return Utils.encode(json)
}
fun parseVmessStd(str: String): ProfileItem? {
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = AppConfig.DEFAULT_SECURITY
getItemFormQuery(config, queryParam, allowInsecure)
return config
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VMESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].security = profileItem.method
}
val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
null,
null,
null
)
return outboundBean
}
}
@@ -1,124 +0,0 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object WireguardFmt : FmtBase() {
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.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.secretKey = uri.userInfo.orEmpty()
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
config.publicKey = queryParam["publickey"].orEmpty()
config.preSharedKey = queryParam["presharedkey"].orEmpty()
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = (queryParam["reserved"] ?: "0,0,0")
return config
}
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"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.publicKey = peerParams["publickey"].orEmpty()
config.preSharedKey = peerParams["presharedkey"].orEmpty()
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
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey.orEmpty()
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
}
wireguard.mtu = profileItem.mtu
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
}
return outboundBean
}
fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] = config.publicKey.orEmpty()
if (config.reserved != null) {
dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty()
}
dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty()
if (config.mtu != null) {
dicQuery["mtu"] = config.mtu.toString()
}
if (config.preSharedKey != null) {
dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty()
}
return toUri(config, config.secretKey, dicQuery)
}
}
@@ -1,406 +0,0 @@
package com.v2ray.ang.handler
import android.content.Context
import android.graphics.Bitmap
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2
import com.v2ray.ang.R
import com.v2ray.ang.dto.*
import com.v2ray.ang.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.VlessFmt
import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils
import java.net.URI
object AngConfigManager {
/**
* parse config form qrcode or...
*/
private fun parseConfig(
str: String?,
subid: String,
subItem: SubscriptionItem?,
removedSelectedServer: ProfileItem?
): Int {
try {
if (str == null || TextUtils.isEmpty(str)) {
return R.string.toast_none_data
}
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
VmessFmt.parse(str)
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
ShadowsocksFmt.parse(str)
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
SocksFmt.parse(str)
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
TrojanFmt.parse(str)
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
VlessFmt.parse(str)
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
WireguardFmt.parse(str)
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
Hysteria2Fmt.parse(str)
} else {
null
}
if (config == null) {
return R.string.toast_incorrect_protocol
}
//filter
if (subItem?.filter != null && subItem.filter?.isNotEmpty() == true && config.remarks.isNotEmpty()) {
val matched = Regex(pattern = subItem.filter ?: "")
.containsMatchIn(input = config.remarks)
if (!matched) return -1
}
config.subscriptionId = subid
val guid = MmkvManager.encodeServerConfig("", config)
if (removedSelectedServer != null &&
config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort
) {
MmkvManager.setSelectServer(guid)
}
} catch (e: Exception) {
e.printStackTrace()
return -1
}
return 0
}
/**
* share config
*/
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.CUSTOM -> ""
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
EConfigType.SOCKS -> SocksFmt.toUri(config)
EConfigType.HTTP -> ""
EConfigType.VLESS -> VlessFmt.toUri(config)
EConfigType.TROJAN -> TrojanFmt.toUri(config)
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
}
} catch (e: Exception) {
e.printStackTrace()
return ""
}
}
/**
* share2Clipboard
*/
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) {
e.printStackTrace()
return -1
}
return 0
}
/**
* share2Clipboard
*/
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()
} catch (e: Exception) {
e.printStackTrace()
return -1
}
}
/**
* share2QRCode
*/
fun share2QRCode(guid: String): Bitmap? {
try {
val conf = shareConfig(guid)
if (TextUtils.isEmpty(conf)) {
return null
}
return QRCodeDecoder.createQRCode(conf)
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
/**
* shareFullContent2Clipboard
*/
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
try {
if (guid == null) return -1
val result = V2rayConfigManager.getV2rayConfig(context, guid)
if (result.status) {
val config = MmkvManager.decodeServerConfig(guid)
if (config?.configType == EConfigType.HYSTERIA2) {
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort)
Utils.setClipboard(context, JsonUtil.toJsonPretty(hy2Config) + "\n" + result.content)
return 0
}
Utils.setClipboard(context, result.content)
} else {
return -1
}
} catch (e: Exception) {
e.printStackTrace()
return -1
}
return 0
}
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)
}
var countSub = parseBatchSubscription(server)
if (countSub <= 0) {
countSub = parseBatchSubscription(Utils.decode(server))
}
if (countSub > 0) {
updateConfigViaSubAll()
}
return count to countSub
}
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) {
e.printStackTrace()
}
return 0
}
fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
try {
if (servers == null) {
return 0
}
val removedSelectedServer =
if (!TextUtils.isEmpty(subid) && !append) {
MmkvManager.decodeServerConfig(
MmkvManager.getSelectServer().orEmpty()
)?.let {
if (it.subscriptionId == subid) {
return@let it
}
return@let null
}
} else {
null
}
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
val subItem = MmkvManager.decodeSubscription(subid)
var count = 0
servers.lines()
.distinct()
.reversed()
.forEach {
val resId = parseConfig(it, subid, subItem, removedSelectedServer)
if (resId == 0) {
count++
}
}
return count
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
fun parseCustomConfigServer(server: String?, subid: String): 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)
if (serverList.isNotEmpty()) {
var count = 0
for (srv in serverList.reversed()) {
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
count += 1
}
return count
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
// For compatibility
val config = CustomFmt.parse(server) ?: return 0
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
e.printStackTrace()
}
return 0
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
try {
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
e.printStackTrace()
}
return 0
} else {
return 0
}
}
fun updateConfigViaSubAll(): Int {
var count = 0
try {
MmkvManager.decodeSubscriptions().forEach {
count += updateConfigViaSub(it)
}
} catch (e: Exception) {
e.printStackTrace()
return 0
}
return count
}
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
try {
if (TextUtils.isEmpty(it.first)
|| TextUtils.isEmpty(it.second.remarks)
|| TextUtils.isEmpty(it.second.url)
) {
return 0
}
if (!it.second.enabled) {
return 0
}
val url = Utils.idnToASCII(it.second.url)
if (!Utils.isValidUrl(url)) {
return 0
}
Log.d(AppConfig.ANG_PACKAGE, url)
var configText = try {
val httpPort = SettingsManager.getHttpPort()
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
} catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……")
//e.printStackTrace()
""
}
if (configText.isEmpty()) {
configText = try {
Utils.getUrlContentWithCustomUserAgent(url)
} catch (e: Exception) {
e.printStackTrace()
""
}
}
if (configText.isEmpty()) {
return 0
}
return parseConfigViaSub(configText, it.first, false)
} catch (e: Exception) {
e.printStackTrace()
return 0
}
}
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)
}
return count
}
private fun importUrlAsSubscription(url: String): Int {
val subscriptions = MmkvManager.decodeSubscriptions()
subscriptions.forEach {
if (it.second.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
}
}
@@ -1,191 +0,0 @@
package com.v2ray.ang.handler
import android.util.Log
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object MigrateManager {
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
fun migrateServerConfig2Profile(): Boolean {
if (serverStorage.count().toInt() == 0) {
return false
}
val serverList = serverStorage.allKeys() ?: return false
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
for (guid in serverList) {
var configOld = decodeServerConfigOld(guid) ?: continue
var config = decodeServerConfig(guid)
if (config != null) {
serverStorage.remove(guid)
continue
}
config = migrateServerConfig2ProfileSub(configOld) ?: continue
config.subscriptionId = configOld.subscriptionId
MmkvManager.encodeServerConfig(guid, config)
//check and remove old
decodeServerConfig(guid) ?: continue
serverStorage.remove(guid)
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
}
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
return true
}
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
return when (configOld.getProxyOutbound()?.protocol) {
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.VLESS.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.TROJAN.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.SHADOWSOCKS.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.SOCKS.name.lowercase() -> migrate2ProfileSocks(configOld)
EConfigType.HTTP.name.lowercase() -> migrate2ProfileHttp(configOld)
EConfigType.WIREGUARD.name.lowercase() -> migrate2ProfileWireguard(configOld)
EConfigType.HYSTERIA2.name.lowercase() -> migrate2ProfileHysteria2(configOld)
EConfigType.CUSTOM.name.lowercase() -> migrate2ProfileCustom(configOld)
else -> null
}
}
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(configOld.configType)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.method = outbound.getSecurityEncryption()
config.password = outbound.getPassword()
config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow
config.network = outbound?.streamSettings?.network ?: NetworkType.TCP.type
outbound.getTransportSettingDetails()?.let { transportDetails ->
config.headerType = transportDetails[0].orEmpty()
config.host = transportDetails[1].orEmpty()
config.path = transportDetails[2].orEmpty()
}
config.seed = outbound?.streamSettings?.kcpSettings?.seed
config.quicSecurity = outbound?.streamSettings?.quicSettings?.security
config.quicKey = outbound?.streamSettings?.quicSettings?.key
config.mode = if (outbound?.streamSettings?.grpcSettings?.multiMode == true) "multi" else "gun"
config.serviceName = outbound?.streamSettings?.grpcSettings?.serviceName
config.authority = outbound?.streamSettings?.grpcSettings?.authority
config.security = outbound.streamSettings?.security
val tlsSettings = outbound?.streamSettings?.realitySettings ?: outbound?.streamSettings?.tlsSettings
config.insecure = tlsSettings?.allowInsecure
config.sni = tlsSettings?.serverName
config.fingerPrint = tlsSettings?.fingerprint
config.alpn = Utils.removeWhiteSpace(tlsSettings?.alpn?.joinToString(",")).toString()
config.publicKey = tlsSettings?.publicKey
config.shortId = tlsSettings?.shortId
config.spiderX = tlsSettings?.spiderX
return config
}
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.SOCKS)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
config.password = outbound.getPassword()
return config
}
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.HTTP)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
config.password = outbound.getPassword()
return config
}
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
outbound.settings?.let { wireguard ->
config.secretKey = wireguard.secretKey
config.localAddress = Utils.removeWhiteSpace((wireguard.address as List<*>).joinToString(",")).toString()
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
config.mtu = wireguard.mtu
config.reserved = Utils.removeWhiteSpace(wireguard.reserved?.joinToString(",")).toString()
}
return config
}
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.HYSTERIA2)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.password = outbound.getPassword()
config.security = AppConfig.TLS
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
config.insecure = tlsSetting.allowInsecure
config.sni = tlsSetting.serverName
config.alpn = Utils.removeWhiteSpace(tlsSetting.alpn?.joinToString(",")).orEmpty()
}
config.obfsPassword = outbound.settings?.obfsPassword
return config
}
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.CUSTOM)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
return config
}
private fun decodeServerConfigOld(guid: String): ServerConfig? {
if (guid.isBlank()) {
return null
}
val json = serverStorage.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return JsonUtil.fromJson(json, ServerConfig::class.java)
}
}
@@ -1,374 +0,0 @@
package com.v2ray.ang.handler
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerAffiliationInfo
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object MmkvManager {
//region private
//private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
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_IDS = "SUB_IDS"
//private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
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
fun getSelectServer(): String? {
return mainStorage.decodeString(KEY_SELECTED_SERVER)
}
fun setSelectServer(guid: String) {
mainStorage.encode(KEY_SELECTED_SERVER, guid)
}
fun encodeServerList(serverList: MutableList<String>) {
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
}
fun decodeServerList(): MutableList<String> {
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
}
}
fun decodeServerConfig(guid: String): ProfileItem? {
if (guid.isBlank()) {
return null
}
val json = profileFullStorage.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return JsonUtil.fromJson(json, ProfileItem::class.java)
}
// fun decodeProfileConfig(guid: String): ProfileLiteItem? {
// if (guid.isBlank()) {
// return null
// }
// val json = profileStorage.decodeString(guid)
// if (json.isNullOrBlank()) {
// return null
// }
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
// }
fun encodeServerConfig(guid: String, config: ProfileItem): String {
val key = guid.ifBlank { Utils.getUuid() }
profileFullStorage.encode(key, JsonUtil.toJson(config))
val serverList = decodeServerList()
if (!serverList.contains(key)) {
serverList.add(0, key)
encodeServerList(serverList)
if (getSelectServer().isNullOrBlank()) {
mainStorage.encode(KEY_SELECTED_SERVER, key)
}
}
// val profile = ProfileLiteItem(
// configType = config.configType,
// subscriptionId = config.subscriptionId,
// remarks = config.remarks,
// server = config.getProxyOutbound()?.getServerAddress(),
// serverPort = config.getProxyOutbound()?.getServerPort(),
// )
// profileStorage.encode(key, JsonUtil.toJson(profile))
return key
}
fun removeServer(guid: String) {
if (guid.isBlank()) {
return
}
if (getSelectServer() == guid) {
mainStorage.remove(KEY_SELECTED_SERVER)
}
val serverList = decodeServerList()
serverList.remove(guid)
encodeServerList(serverList)
profileFullStorage.remove(guid)
//profileStorage.remove(guid)
serverAffStorage.remove(guid)
}
fun removeServerViaSubid(subid: String) {
if (subid.isBlank()) {
return
}
profileFullStorage.allKeys()?.forEach { key ->
decodeServerConfig(key)?.let { config ->
if (config.subscriptionId == subid) {
removeServer(key)
}
}
}
}
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
if (guid.isBlank()) {
return null
}
val json = serverAffStorage.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
}
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
if (guid.isBlank()) {
return
}
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
aff.testDelayMillis = testResult
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
}
fun clearAllTestDelayResults(keys: List<String>?) {
keys?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff ->
aff.testDelayMillis = 0
serverAffStorage.encode(key, JsonUtil.toJson(aff))
}
}
}
fun removeAllServer(): Int {
val count = profileFullStorage.allKeys()?.count() ?: 0
mainStorage.clearAll()
profileFullStorage.clearAll()
//profileStorage.clearAll()
serverAffStorage.clearAll()
return count
}
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
}
fun encodeServerRaw(guid: String, config: String) {
serverRawStorage.encode(guid, config)
}
fun decodeServerRaw(guid: String): String? {
return serverRawStorage.decodeString(guid)
}
//endregion
//region Subscriptions
private fun initSubsList() {
val subsList = decodeSubsList()
if (subsList.isNotEmpty()) {
return
}
subStorage.allKeys()?.forEach { key ->
subsList.add(key)
}
encodeSubsList(subsList)
}
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
initSubsList()
val subscriptions = mutableListOf<Pair<String, SubscriptionItem>>()
decodeSubsList().forEach { key ->
val json = subStorage.decodeString(key)
if (!json.isNullOrBlank()) {
subscriptions.add(Pair(key, JsonUtil.fromJson(json, SubscriptionItem::class.java)))
}
}
return subscriptions
}
fun removeSubscription(subid: String) {
subStorage.remove(subid)
val subsList = decodeSubsList()
subsList.remove(subid)
encodeSubsList(subsList)
removeServerViaSubid(subid)
}
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)
}
}
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
val json = subStorage.decodeString(subscriptionId) ?: return null
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
}
fun encodeSubsList(subsList: MutableList<String>) {
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
}
fun decodeSubsList(): MutableList<String> {
val json = mainStorage.decodeString(KEY_SUB_IDS)
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
}
}
//endregion
//region Asset
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
assetStorage.allKeys()?.forEach { key ->
val json = assetStorage.decodeString(key)
if (!json.isNullOrBlank()) {
assetUrlItems.add(Pair(key, JsonUtil.fromJson(json, AssetUrlItem::class.java)))
}
}
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
}
fun removeAssetUrl(assetid: String) {
assetStorage.remove(assetid)
}
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
val key = assetid.ifBlank { Utils.getUuid() }
assetStorage.encode(key, JsonUtil.toJson(assetItem))
}
fun decodeAsset(assetid: String): AssetUrlItem? {
val json = assetStorage.decodeString(assetid) ?: return null
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
}
//endregion
//region Routing
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
if (ruleset.isNullOrEmpty()) return null
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
}
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
if (rulesetList.isNullOrEmpty())
encodeSettings(PREF_ROUTING_RULESET, "")
else
encodeSettings(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
}
//endregion
fun encodeSettings(key: String, value: String?): Boolean {
return settingsStorage.encode(key, value)
}
fun encodeSettings(key: String, value: Int): Boolean {
return settingsStorage.encode(key, value)
}
fun encodeSettings(key: String, value: Boolean): Boolean {
return settingsStorage.encode(key, value)
}
fun encodeSettings(key: String, value: MutableSet<String>): Boolean {
return settingsStorage.encode(key, value)
}
fun decodeSettingsString(key: String): String? {
return settingsStorage.decodeString(key)
}
fun decodeSettingsString(key: String, defaultValue: String?): String? {
return settingsStorage.decodeString(key, defaultValue)
}
fun decodeSettingsBool(key: String): Boolean {
return settingsStorage.decodeBool(key, false)
}
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
return settingsStorage.decodeBool(key, defaultValue)
}
fun decodeSettingsInt(key: String, defaultValue: Int): Int {
return settingsStorage.decodeInt(key, defaultValue)
}
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
return settingsStorage.decodeStringSet(key)
}
//endregion
//region Others
fun encodeStartOnBoot(startOnBoot: Boolean) {
MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot)
}
fun decodeStartOnBoot(): Boolean {
return decodeSettingsBool(PREF_IS_BOOTED, false)
}
//endregion
}
@@ -1,186 +0,0 @@
package com.v2ray.ang.handler
import android.content.Context
import android.content.res.AssetManager
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RoutingType
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerList
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.Utils.parseInt
import java.io.File
import java.io.FileOutputStream
import java.util.Collections
import kotlin.Int
object SettingsManager {
fun initRoutingRulesets(context: Context) {
val exist = MmkvManager.decodeRoutingRulesets()
if (exist.isNullOrEmpty()) {
val rulesetList = getPresetRoutingRulesets(context)
MmkvManager.encodeRoutingRulesets(rulesetList)
}
}
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
val fileName = RoutingType.fromIndex(index).fileName
val assets = Utils.readTextFromAssets(context, fileName)
if (TextUtils.isEmpty(assets)) {
return null
}
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
}
fun resetRoutingRulesetsFromPresets(context: Context, index: Int) {
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
resetRoutingRulesetsCommon(rulesetList)
}
fun resetRoutingRulesets(content: String?): Boolean {
if (content.isNullOrEmpty()) {
return false
}
try {
val rulesetList = JsonUtil.fromJson(content, Array<RulesetItem>::class.java).toMutableList()
if (rulesetList.isNullOrEmpty()) {
return false
}
resetRoutingRulesetsCommon(rulesetList)
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
if (key.locked == true) {
rulesetNew.add(key)
}
}
rulesetNew.addAll(rulesetList)
MmkvManager.encodeRoutingRulesets(rulesetNew)
}
fun getRoutingRuleset(index: Int): RulesetItem? {
if (index < 0) return null
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return null
return rulesetList[index]
}
fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) {
if (ruleset == null) return
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return
if (index < 0 || index >= rulesetList.count()) {
rulesetList.add(0, ruleset)
} else {
rulesetList[index] = ruleset
}
MmkvManager.encodeRoutingRulesets(rulesetList)
}
fun removeRoutingRuleset(index: Int) {
if (index < 0) return
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return
rulesetList.removeAt(index)
MmkvManager.encodeRoutingRulesets(rulesetList)
}
fun routingRulesetsBypassLan(): Boolean {
val rulesetItems = MmkvManager.decodeRoutingRulesets()
val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any {
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
}
return exist == true
}
fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return
Collections.swap(rulesetList, fromPosition, toPosition)
MmkvManager.encodeRoutingRulesets(rulesetList)
}
fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
val subsList = MmkvManager.decodeSubsList()
if (subsList.isNullOrEmpty()) return
Collections.swap(subsList, fromPosition, toPosition)
MmkvManager.encodeSubsList(subsList)
}
fun getServerViaRemarks(remarks: String?): ProfileItem? {
if (remarks == null) {
return null
}
val serverList = decodeServerList()
for (guid in serverList) {
val profile = decodeServerConfig(guid)
if (profile != null && profile.remarks == remarks) {
return profile
}
}
return null
}
fun getSocksPort(): Int {
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
}
fun getHttpPort(): Int {
return getSocksPort() + (if (Utils.isXray()) 0 else 1)
}
fun initAssets(context: Context, assets: AssetManager) {
val extFolder = Utils.userAssetPath(context)
try {
val geo = arrayOf("geosite.dat", "geoip.dat")
assets.list("")
?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() }
?.forEach {
val target = File(extFolder, it)
assets.open(it).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
Log.i(
ANG_PACKAGE,
"Copied from apk assets folder to ${target.absolutePath}"
)
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
}
}
}
@@ -1,636 +0,0 @@
package com.v2ray.ang.handler
import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_DNSPOD_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_DNSPOD_DOMAIN
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_QUAD9_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_QUAD9_DOMAIN
import com.v2ray.ang.AppConfig.DNS_YANDEX_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_YANDEX_DOMAIN
import com.v2ray.ang.AppConfig.GEOIP_CN
import com.v2ray.ang.AppConfig.GEOSITE_CN
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.GOOGLEAPIS_CN_DOMAIN
import com.v2ray.ang.AppConfig.GOOGLEAPIS_COM_DOMAIN
import com.v2ray.ang.AppConfig.HEADER_TYPE_HTTP
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
import com.v2ray.ang.fmt.HttpFmt
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.VlessFmt
import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object V2rayConfigManager {
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
val domainPort = config.getServerAddressAndPort()
return ConfigResult(true, guid, raw, domainPort)
}
val result = getV2rayNonCustomConfig(context, config)
//Log.d(ANG_PACKAGE, result.content)
result.guid = guid
return result
} catch (e: Exception) {
e.printStackTrace()
return ConfigResult(false)
}
}
private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult {
val result = ConfigResult(false)
val address = config.server ?: return result
if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
return result
}
}
//取得默认配置
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
if (TextUtils.isEmpty(assets)) {
return result
}
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel =
MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.remarks = config.remarks
inbounds(v2rayConfig)
val isPlugin = config.configType == EConfigType.HYSTERIA2
val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
routing(v2rayConfig)
fakedns(v2rayConfig)
dns(v2rayConfig)
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
customLocalDns(v2rayConfig)
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
v2rayConfig.stats = null
v2rayConfig.policy = null
}
result.status = true
result.content = v2rayConfig.toPrettyPrinting()
result.domainPort = if (retMore.first) retMore.second else retOut.second
return result
}
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
try {
val socksPort = SettingsManager.getSocksPort()
v2rayConfig.inbounds.forEach { curInbound ->
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
//bind all inbounds to localhost if the user requests
curInbound.listen = LOOPBACK
}
}
v2rayConfig.inbounds[0].port = socksPort
val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
val sniffAllTlsAndHttp =
MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
v2rayConfig.inbounds[0].sniffing?.routeOnly =
MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
if (!sniffAllTlsAndHttp) {
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
}
if (fakedns) {
v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
}
if (Utils.isXray()) {
v2rayConfig.inbounds.removeAt(1)
} else {
val httpPort = SettingsManager.getHttpPort()
v2rayConfig.inbounds[1].port = httpPort
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun outbounds(v2rayConfig: V2rayConfig, config: ProfileItem, isPlugin: Boolean): Pair<Boolean, String>? {
if (isPlugin) {
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
val outboundNew = V2rayConfig.OutboundBean(
mux = null,
protocol = EConfigType.SOCKS.name.lowercase(),
settings = V2rayConfig.OutboundBean.OutSettingsBean(
servers = listOf(
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean(
address = LOOPBACK,
port = socksPort
)
)
)
)
if (v2rayConfig.outbounds.isNotEmpty()) {
v2rayConfig.outbounds[0] = outboundNew
} else {
v2rayConfig.outbounds.add(outboundNew)
}
return Pair(true, outboundNew.getServerAddressAndPort())
}
val outbound = getProxyOutbound(config) ?: return null
val ret = updateOutboundWithGlobalSettings(outbound)
if (!ret) return null
if (v2rayConfig.outbounds.isNotEmpty()) {
v2rayConfig.outbounds[0] = outbound
} else {
v2rayConfig.outbounds.add(outbound)
}
updateOutboundFragment(v2rayConfig)
return Pair(true, config.getServerAddressAndPort())
}
private fun fakedns(v2rayConfig: V2rayConfig) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
) {
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
}
}
private fun routing(v2rayConfig: V2rayConfig): Boolean {
try {
v2rayConfig.routing.domainStrategy =
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
?: "IPIfNonMatch"
val rulesetItems = MmkvManager.decodeRoutingRulesets()
rulesetItems?.forEach { key ->
routingUserRule(key, v2rayConfig)
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun routingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
try {
if (item == null || !item.enabled) {
return
}
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
v2rayConfig.routing.rules.add(rule)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun userRule2Domain(tag: String): ArrayList<String> {
val domain = ArrayList<String>()
val rulesetItems = MmkvManager.decodeRoutingRulesets()
rulesetItems?.forEach { key ->
if (key != null && key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
key.domain?.forEach {
if (it != GEOSITE_PRIVATE
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
) {
domain.add(it)
}
}
}
}
return domain
}
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
try {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
val geositeCn = arrayListOf(GEOSITE_CN)
val proxyDomain = userRule2Domain(TAG_PROXY)
val directDomain = userRule2Domain(TAG_DIRECT)
// fakedns with all domains to make it always top priority
v2rayConfig.dns.servers?.add(
0,
V2rayConfig.DnsBean.ServersBean(
address = "fakedns",
domains = geositeCn.plus(proxyDomain).plus(directDomain)
)
)
}
// DNS inbound对象
val remoteDns = Utils.getRemoteDnsServers()
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
port = 53,
network = "tcp,udp"
)
val localDnsPort = Utils.parseInt(
MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT),
AppConfig.PORT_LOCAL_DNS.toInt()
)
v2rayConfig.inbounds.add(
V2rayConfig.InboundBean(
tag = "dns-in",
port = localDnsPort,
listen = LOOPBACK,
protocol = "dokodemo-door",
settings = dnsInboundSettings,
sniffing = null
)
)
}
// DNS outbound对象
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
v2rayConfig.outbounds.add(
V2rayConfig.OutboundBean(
protocol = "dns",
tag = "dns-out",
settings = null,
streamSettings = null,
mux = null
)
)
}
// DNS routing tag
v2rayConfig.routing.rules.add(
0, RulesBean(
inboundTag = arrayListOf("dns-in"),
outboundTag = "dns-out",
domain = null
)
)
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun dns(v2rayConfig: V2rayConfig): Boolean {
try {
val hosts = mutableMapOf<String, Any>()
val servers = ArrayList<Any>()
//remote Dns
val remoteDns = Utils.getRemoteDnsServers()
val proxyDomain = userRule2Domain(TAG_PROXY)
remoteDns.forEach {
servers.add(it)
}
if (proxyDomain.size > 0) {
servers.add(
V2rayConfig.DnsBean.ServersBean(
address = remoteDns.first(),
domains = proxyDomain,
)
)
}
// domestic DNS
val domesticDns = Utils.getDomesticDnsServers()
val directDomain = userRule2Domain(TAG_DIRECT)
val isCnRoutingMode = directDomain.contains(GEOSITE_CN)
val geoipCn = arrayListOf(GEOIP_CN)
if (directDomain.size > 0) {
servers.add(
V2rayConfig.DnsBean.ServersBean(
address = domesticDns.first(),
domains = directDomain,
expectIPs = if (isCnRoutingMode) geoipCn else null,
skipFallback = true
)
)
}
if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add(
0, RulesBean(
outboundTag = TAG_DIRECT,
port = "53",
ip = arrayListOf(domesticDns.first()),
domain = null
)
)
}
//block dns
val blkDomain = userRule2Domain(TAG_BLOCKED)
if (blkDomain.size > 0) {
hosts.putAll(blkDomain.map { it to LOOPBACK })
}
// hardcode googleapi rule to fix play store problems
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
// hardcode popular Android Private DNS rule to fix localhost DNS problem
hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
hosts[DNS_CLOUDFLARE_DOMAIN] = DNS_CLOUDFLARE_ADDRESSES
hosts[DNS_DNSPOD_DOMAIN] = DNS_DNSPOD_ADDRESSES
hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
hosts[DNS_QUAD9_DOMAIN] = DNS_QUAD9_ADDRESSES
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
// DNS dns对象
v2rayConfig.dns = V2rayConfig.DnsBean(
servers = servers,
hosts = hosts
)
// DNS routing
if (Utils.isPureIpAddress(remoteDns.first())) {
v2rayConfig.routing.rules.add(
0, RulesBean(
outboundTag = TAG_PROXY,
port = "53",
ip = arrayListOf(remoteDns.first()),
domain = null
)
)
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.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)
) {
muxEnabled = false
} else if (protocol.equals(EConfigType.VLESS.name, true)
&& outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true
) {
muxEnabled = false
} else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
muxEnabled = false
}
if (muxEnabled == true) {
outbound.mux?.enabled = true
outbound.mux?.concurrency =
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_CONCURRENCY, 8)
outbound.mux?.xudpConcurrency =
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY, 16)
outbound.mux?.xudpProxyUDP443 =
MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
} else {
outbound.mux?.enabled = false
outbound.mux?.concurrency = -1
}
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
var localTunAddr = if (outbound.settings?.address == null) {
listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6)
} else {
outbound.settings?.address as List<*>
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) != true) {
localTunAddr = listOf(localTunAddr.first())
}
outbound.settings?.address = localTunAddr
}
if (outbound.streamSettings?.network == DEFAULT_NETWORK
&& outbound.streamSettings?.tcpSettings?.header?.type == HEADER_TYPE_HTTP
) {
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
val requestString: String by lazy {
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
}
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
requestString,
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
)
outbound.streamSettings?.tcpSettings?.header?.request?.path =
if (path.isNullOrEmpty()) {
listOf("/")
} else {
path
}
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
try {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
return true
}
if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS
&& v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY
) {
return true
}
val fragmentOutbound =
V2rayConfig.OutboundBean(
protocol = PROTOCOL_FREEDOM,
tag = TAG_FRAGMENT,
mux = null
)
var packets =
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY
&& packets == "tlshello"
) {
packets = "1-3"
} else if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.TLS
&& packets != "tlshello"
) {
packets = "tlshello"
}
fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean(
fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean(
packets = packets,
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
?: "50-100",
interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
?: "10-20"
),
noises = listOf(
V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
type = "rand",
packet = "10-20",
delay = "10-16",
)
),
)
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
TcpNoDelay = true,
mark = 255
)
)
v2rayConfig.outbounds.add(fragmentOutbound)
//proxy chain
v2rayConfig.outbounds[0].streamSettings?.sockopt =
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
dialerProxy = TAG_FRAGMENT
)
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun moreOutbounds(
v2rayConfig: V2rayConfig,
subscriptionId: String,
isPlugin: Boolean
): Pair<Boolean, String> {
val returnPair = Pair(false, "")
var domainPort: String = ""
if (isPlugin) {
return returnPair
}
//fragment proxy
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
return returnPair
}
if (subscriptionId.isEmpty()) {
return returnPair
}
try {
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return returnPair
//current proxy
val outbound = v2rayConfig.outbounds[0]
//Previous proxy
val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
if (prevNode != null) {
val prevOutbound = getProxyOutbound(prevNode)
if (prevOutbound != null) {
updateOutboundWithGlobalSettings(prevOutbound)
prevOutbound.tag = TAG_PROXY + "2"
v2rayConfig.outbounds.add(prevOutbound)
outbound.streamSettings?.sockopt =
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
dialerProxy = prevOutbound.tag
)
domainPort = prevNode.getServerAddressAndPort()
}
}
//Next proxy
val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
if (nextNode != null) {
val nextOutbound = getProxyOutbound(nextNode)
if (nextOutbound != null) {
updateOutboundWithGlobalSettings(nextOutbound)
nextOutbound.tag = TAG_PROXY
v2rayConfig.outbounds.add(0, nextOutbound)
outbound.tag = TAG_PROXY + "1"
nextOutbound.streamSettings?.sockopt =
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
dialerProxy = outbound.tag
)
}
}
} catch (e: Exception) {
e.printStackTrace()
return returnPair
}
if (domainPort.isNotEmpty()) {
return Pair(true, domainPort)
}
return returnPair
}
fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
return when (profileItem.configType) {
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
EConfigType.CUSTOM -> null
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toOutbound(profileItem)
EConfigType.SOCKS -> SocksFmt.toOutbound(profileItem)
EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toOutbound(profileItem)
EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
}
}
}
@@ -13,41 +13,48 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper
package com.v2ray.ang.helper;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
/**
* Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
*
* @author Paul Burke (ipaulpro)
*/
interface ItemTouchHelperAdapter {
public interface ItemTouchHelperAdapter {
/**
* Called when an item has been dragged far enough to trigger a move. This is called every time
* an item is shifted, and **not** at the end of a "drop" event.<br></br>
* <br></br>
* Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
* adjusting the underlying data to reflect this move.
*
* @param fromPosition The start position of the moved item.
* @param toPosition Then resolved position of the moved item.
* @return True if the item was moved to the new adapter position.
* @see RecyclerView.getAdapterPositionFor
* @see RecyclerView.ViewHolder.getAdapterPosition
*
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
boolean onItemMove(int fromPosition, int toPosition);
fun onItemMoveCompleted()
void onItemMoveCompleted();
/**
* Called when an item has been dismissed by a swipe.<br></br>
* <br></br>
* Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after
* Called when an item has been dismissed by a swipe.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
* adjusting the underlying data to reflect this removal.
*
* @param position The position of the item dismissed.
* @see RecyclerView.getAdapterPositionFor
* @see RecyclerView.ViewHolder.getAdapterPosition
*
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
fun onItemDismiss(position: Int)
void onItemDismiss(int position);
}
@@ -13,26 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper
import androidx.recyclerview.widget.ItemTouchHelper
package com.v2ray.ang.helper;
import android.support.v7.widget.helper.ItemTouchHelper;
/**
* Interface to notify an item ViewHolder of relevant callbacks from [ ].
* Interface to notify an item ViewHolder of relevant callbacks from {@link
* ItemTouchHelper.Callback}.
*
* @author Paul Burke (ipaulpro)
*/
interface ItemTouchHelperViewHolder {
public interface ItemTouchHelperViewHolder {
/**
* Called when the [ItemTouchHelper] first registers an item as being moved or swiped.
* Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
* Implementations should update the item view to indicate it's active state.
*/
fun onItemSelected()
void onItemSelected();
/**
* Called when the [ItemTouchHelper] has completed the move or swipe, and the active item
* Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
* state should be cleared.
*/
fun onItemClear()
void onItemClear();
}
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2015 Paul Burke
*
* 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.v2ray.ang.helper;
import android.support.v7.widget.RecyclerView;
/**
* Listener for manual initiation of a drag.
*/
public interface OnStartDragListener {
/**
* Called when a view is requesting a start of a drag.
*
* @param viewHolder The holder of the view to drag.
*/
void onStartDrag(RecyclerView.ViewHolder viewHolder);
}
@@ -0,0 +1,125 @@
/*
* Copyright (C) 2015 Paul Burke
*
* 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.v2ray.ang.helper;
import android.graphics.Canvas;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
/**
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
* </br/>
* Expects the <code>RecyclerView.Adapter</code> to listen for {@link
* ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> to implement
* {@link ItemTouchHelperViewHolder}.
*
* @author Paul Burke (ipaulpro)
*/
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
public static final float ALPHA_FULL = 1.0f;
private final ItemTouchHelperAdapter mAdapter;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
mAdapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
// Set movement flags based on the layout manager
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
final int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
} else {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
// Notify the adapter of the move
mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
// Notify the adapter of the dismissal
mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
// Fade out the view as it is swiped out of the parent's bounds
final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
viewHolder.itemView.setAlpha(alpha);
viewHolder.itemView.setTranslationX(dX);
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
// We only want the active item to change
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
if (viewHolder instanceof ItemTouchHelperViewHolder) {
// Let the view holder know that this item is being moved or dragged
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemSelected();
}
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
mAdapter.onItemMoveCompleted();
viewHolder.itemView.setAlpha(ALPHA_FULL);
if (viewHolder instanceof ItemTouchHelperViewHolder) {
// Tell the view holder it's time to restore the idle state
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemClear();
}
}
}
@@ -1,148 +0,0 @@
/*
* Copyright (C) 2015 Paul Burke
*
* 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.v2ray.ang.helper
import android.animation.ValueAnimator
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.graphics.Canvas
import android.view.animation.DecelerateInterpolator
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.sign
/**
* An implementation of [ItemTouchHelper.Callback] that enables basic drag & drop and
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br></br>
*
* Expects the `RecyclerView.Adapter` to listen for [ ] callbacks and the `RecyclerView.ViewHolder` to implement
* [ItemTouchHelperViewHolder].
*
* @author Paul Burke (ipaulpro)
*/
class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
private var mReturnAnimator: ValueAnimator? = null
override fun isLongPressDragEnabled(): Boolean = true
override fun isItemViewSwipeEnabled(): Boolean = true
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags: Int
val swipeFlags: Int
if (recyclerView.layoutManager is GridLayoutManager) {
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
} else {
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
}
return makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(
recyclerView: RecyclerView,
source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return if (source.itemViewType != target.itemViewType) {
false
} else {
mAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition)
true
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// Do not delete; simply return item to original position
returnViewToOriginalPosition(viewHolder)
}
override fun onChildDraw(
c: Canvas, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val maxSwipeDistance = viewHolder.itemView.width * SWIPE_THRESHOLD
val swipeAmount = abs(dX)
val direction = sign(dX)
// Limit maximum swipe distance
val translationX = min(swipeAmount, maxSwipeDistance) * direction
val alpha = ALPHA_FULL - min(swipeAmount, maxSwipeDistance) / maxSwipeDistance
viewHolder.itemView.translationX = translationX
viewHolder.itemView.alpha = alpha
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
returnViewToOriginalPosition(viewHolder)
}
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
}
private fun returnViewToOriginalPosition(viewHolder: RecyclerView.ViewHolder) {
mReturnAnimator?.takeIf { it.isRunning }?.cancel()
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.translationX, 0f).apply {
addUpdateListener { animation ->
val value = animation.animatedValue as Float
viewHolder.itemView.translationX = value
viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD))
}
interpolator = DecelerateInterpolator()
duration = ANIMATION_DURATION
start()
}
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is ItemTouchHelperViewHolder) {
viewHolder.onItemSelected()
}
super.onSelectedChanged(viewHolder, actionState)
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = ALPHA_FULL
if (viewHolder is ItemTouchHelperViewHolder) {
viewHolder.onItemClear()
}
mAdapter.onItemMoveCompleted()
}
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
return 1.1f // Set a value greater than 1 to prevent default swipe delete
}
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
return defaultValue * 10 // Increase swipe escape velocity to make swipe harder to trigger
}
companion object {
private const val ALPHA_FULL = 1.0f
private const val SWIPE_THRESHOLD = 0.25f
private const val ANIMATION_DURATION: Long = 200
}
}
@@ -1,32 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.content.pm.ResolveInfo
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init {
check(resolveInfo.providerInfo != null)
}
override val componentInfo get() = resolveInfo.providerInfo!!
}
@@ -1,43 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.graphics.drawable.Drawable
abstract class Plugin {
abstract val id: String
abstract val label: CharSequence
abstract val version: Int
abstract val versionName: String
open val icon: Drawable? get() = null
open val defaultConfig: String? get() = null
open val packageName: String get() = ""
open val directBootAware: Boolean get() = true
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return id == (other as Plugin).id
}
override fun hashCode() = id.hashCode()
}
@@ -1,33 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
object PluginContract {
const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN"
const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY"
const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id"
const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path"
const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable"
const val COLUMN_PATH = "path"
const val COLUMN_MODE = "mode"
const val SCHEME = "plugin"
}
@@ -1,54 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.content.Intent
import android.content.pm.PackageManager
import com.v2ray.ang.AngApplication
class PluginList : ArrayList<Plugin>() {
init {
addAll(
AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
)
.filter { it.providerInfo.exported }.map { NativePlugin(it) })
}
val lookup = mutableMapOf<String, Plugin>().apply {
for (plugin in this@PluginList.toList()) {
fun check(old: Plugin?) {
if (old != null && old != plugin) {
this@PluginList.remove(old)
}
/* if (old != null && old !== plugin) {
val packages = this@PluginList.filter { it.id == plugin.id }
.joinToString { it.packageName }
val message = "Conflicting plugins found from: $packages"
Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
throw IllegalStateException(message)
}*/
}
check(put(plugin.id, plugin))
}
}
}
@@ -1,233 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-AngApplication@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.ProviderInfo
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.system.Os
import android.widget.Toast
import androidx.core.os.bundleOf
import com.v2ray.ang.AngApplication
import com.v2ray.ang.extension.listenForPackageChanges
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
import java.io.File
import java.io.FileNotFoundException
object PluginManager {
class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin)
private var receiver: BroadcastReceiver? = null
private var cachedPlugins: PluginList? = null
fun fetchPlugins() = synchronized(this) {
if (receiver == null) receiver = AngApplication.application.listenForPackageChanges {
synchronized(this) {
receiver = null
cachedPlugins = null
}
}
if (cachedPlugins == null) cachedPlugins = PluginList()
cachedPlugins!!
}
private fun buildUri(id: String, authority: String) = Uri.Builder()
.scheme(PluginContract.SCHEME)
.authority(authority)
.path("/$id")
.build()
data class InitResult(
val path: String,
)
@Throws(Throwable::class)
fun init(pluginId: String): InitResult? {
if (pluginId.isEmpty()) return null
var throwable: Throwable? = null
try {
val result = initNative(pluginId)
if (result != null) return result
} catch (t: Throwable) {
if (throwable == null) throwable = t //Logs.w(t)
}
throw throwable ?: PluginNotFoundException(pluginId)
}
private fun initNative(pluginId: String): InitResult? {
var flags = PackageManager.GET_META_DATA
if (Build.VERSION.SDK_INT >= 24) {
flags =
flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
}
var providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags
)
.filter { it.providerInfo.exported }
if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags
)
.filter { it.providerInfo.exported }
}
if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags
)
.filter { it.providerInfo.exported }
}
if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags
)
.filter { it.providerInfo.exported }
}
if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
).filter {
it.providerInfo.exported &&
it.providerInfo.metaData.containsKey(METADATA_KEY_ID) &&
it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId
}
if (providers.size > 1) {
providers = listOf(providers[0]) // What if there is more than one?
}
}
if (providers.isEmpty()) return null
if (providers.size > 1) {
val message =
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show()
throw IllegalStateException(message)
}
val provider = providers.single().providerInfo
var failure: Throwable? = null
try {
initNativeFaster(provider)?.also { return InitResult(it) }
} catch (t: Throwable) {
// Logs.w("Initializing native plugin faster mode failed")
failure = t
}
val uri = Uri.Builder().apply {
scheme(ContentResolver.SCHEME_CONTENT)
authority(provider.authority)
}.build()
try {
return initNativeFast(
AngApplication.application.contentResolver,
pluginId,
uri
)?.let { InitResult(it) }
} catch (t: Throwable) {
// Logs.w("Initializing native plugin fast mode failed")
failure?.also { t.addSuppressed(it) }
failure = t
}
try {
return initNativeSlow(
AngApplication.application.contentResolver,
pluginId,
uri
)?.let { InitResult(it) }
} catch (t: Throwable) {
failure?.also { t.addSuppressed(it) }
throw t
}
}
private fun initNativeFaster(provider: ProviderInfo): String? {
return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)
?.let { relativePath ->
File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
check(canExecute())
}.absolutePath
}
}
private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? {
return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf())
?.getString(PluginContract.EXTRA_ENTRY)?.also {
check(File(it).canExecute())
}
}
@SuppressLint("Recycle")
private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? {
var initialized = false
fun entryNotFound(): Nothing =
throw IndexOutOfBoundsException("Plugin entry binary not found")
val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin")
(cr.query(
uri,
arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
null,
null,
null
)
?: return null).use { cursor ->
if (!cursor.moveToFirst()) entryNotFound()
pluginDir.deleteRecursively()
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
val pluginDirPath = pluginDir.absolutePath + '/'
do {
val path = cursor.getString(0)
val file = File(pluginDir, path)
check(file.absolutePath.startsWith(pluginDirPath))
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
}
Os.chmod(
file.absolutePath, when (cursor.getType(1)) {
Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
else -> throw IllegalArgumentException("File mode should be of type int")
}
)
if (path == pluginId) initialized = true
} while (cursor.moveToNext())
}
if (!initialized) entryNotFound()
return File(pluginDir, pluginId).absolutePath
}
fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) {
is String -> value
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
.getString(value)
null -> null
else -> error("meta-data $key has invalid type ${value.javaClass}")
}
}
@@ -1,51 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.Build
import com.v2ray.ang.AngApplication
import com.v2ray.ang.plugin.PluginManager.loadString
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
protected abstract val componentInfo: ComponentInfo
override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
override val version by lazy {
getPackageInfo(componentInfo.packageName).versionCode
}
override val versionName: String by lazy {
getPackageInfo(componentInfo.packageName).versionName!!
}
override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager)
override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager)
override val packageName: String get() = componentInfo.packageName
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
fun getPackageInfo(packageName: String) = AngApplication.application.packageManager.getPackageInfo(
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
)!!
}
@@ -1,18 +0,0 @@
package com.v2ray.ang.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//Check if context is not null and action is the one we want
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
//Check if flag is true and a server is selected
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
//Start v2ray
V2RayServiceManager.startV2Ray(context)
}
}

Some files were not shown because too many files have changed in this diff Show More