Repository: SaeedDev94/Xray
Branch: master
Commit: 69bf223e8c24
Files: 221
Total size: 320.1 KB
Directory structure:
gitextract_0tifmtqk/
├── .github/
│ └── workflows/
│ └── release.yml
├── .gitignore
├── .gitmodules
├── Dockerfile
├── LICENSE
├── README.md
├── XrayCore/
│ ├── go.mod
│ ├── go.sum
│ ├── lib/
│ │ ├── core.go
│ │ ├── env.go
│ │ ├── error.go
│ │ └── test.go
│ └── main.go
├── app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── libs/
│ │ └── .gitignore
│ ├── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── assets/
│ │ │ └── .gitignore
│ │ ├── java/
│ │ │ └── io/
│ │ │ └── github/
│ │ │ └── saeeddev94/
│ │ │ └── xray/
│ │ │ ├── Settings.kt
│ │ │ ├── Xray.kt
│ │ │ ├── activity/
│ │ │ │ ├── AppsRoutingActivity.kt
│ │ │ │ ├── AssetsActivity.kt
│ │ │ │ ├── ConfigsActivity.kt
│ │ │ │ ├── LinksActivity.kt
│ │ │ │ ├── LinksManagerActivity.kt
│ │ │ │ ├── LogsActivity.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── ProfileActivity.kt
│ │ │ │ ├── ScannerActivity.kt
│ │ │ │ └── SettingsActivity.kt
│ │ │ ├── adapter/
│ │ │ │ ├── AppsRoutingAdapter.kt
│ │ │ │ ├── ConfigAdapter.kt
│ │ │ │ ├── LinkAdapter.kt
│ │ │ │ ├── ProfileAdapter.kt
│ │ │ │ └── SettingAdapter.kt
│ │ │ ├── component/
│ │ │ │ └── EmptySubmitSearchView.kt
│ │ │ ├── database/
│ │ │ │ ├── Config.kt
│ │ │ │ ├── ConfigDao.kt
│ │ │ │ ├── Link.kt
│ │ │ │ ├── LinkDao.kt
│ │ │ │ ├── Profile.kt
│ │ │ │ ├── ProfileDao.kt
│ │ │ │ └── XrayDatabase.kt
│ │ │ ├── dto/
│ │ │ │ ├── AppList.kt
│ │ │ │ ├── ProfileList.kt
│ │ │ │ └── XrayConfig.kt
│ │ │ ├── fragment/
│ │ │ │ └── LinkFormFragment.kt
│ │ │ ├── helper/
│ │ │ │ ├── ConfigHelper.kt
│ │ │ │ ├── DownloadHelper.kt
│ │ │ │ ├── FileHelper.kt
│ │ │ │ ├── HttpHelper.kt
│ │ │ │ ├── IntentHelper.kt
│ │ │ │ ├── JsonHelper.kt
│ │ │ │ ├── LinkHelper.kt
│ │ │ │ ├── NetworkStateHelper.kt
│ │ │ │ ├── ProfileTouchHelper.kt
│ │ │ │ └── TransparentProxyHelper.kt
│ │ │ ├── receiver/
│ │ │ │ ├── BootReceiver.kt
│ │ │ │ └── VpnActionReceiver.kt
│ │ │ ├── repository/
│ │ │ │ ├── ConfigRepository.kt
│ │ │ │ ├── LinkRepository.kt
│ │ │ │ └── ProfileRepository.kt
│ │ │ ├── service/
│ │ │ │ ├── TProxyService.kt
│ │ │ │ └── VpnTileService.kt
│ │ │ └── viewmodel/
│ │ │ ├── ConfigViewModel.kt
│ │ │ ├── LinkViewModel.kt
│ │ │ └── ProfileViewModel.kt
│ │ ├── jni/
│ │ │ ├── Android.mk
│ │ │ └── Application.mk
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── baseline_adb.xml
│ │ │ ├── baseline_add.xml
│ │ │ ├── baseline_alt_route.xml
│ │ │ ├── baseline_config.xml
│ │ │ ├── baseline_content_copy.xml
│ │ │ ├── baseline_delete.xml
│ │ │ ├── baseline_done.xml
│ │ │ ├── baseline_download.xml
│ │ │ ├── baseline_edit.xml
│ │ │ ├── baseline_file_open.xml
│ │ │ ├── baseline_folder_open.xml
│ │ │ ├── baseline_link.xml
│ │ │ ├── baseline_refresh.xml
│ │ │ ├── baseline_settings.xml
│ │ │ ├── baseline_vpn_key.xml
│ │ │ ├── baseline_vpn_lock.xml
│ │ │ └── ic_xray.xml
│ │ ├── drawable-v26/
│ │ │ └── ic_launcher.xml
│ │ ├── layout/
│ │ │ ├── activity_apps_routing.xml
│ │ │ ├── activity_assets.xml
│ │ │ ├── activity_configs.xml
│ │ │ ├── activity_links.xml
│ │ │ ├── activity_logs.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_profile.xml
│ │ │ ├── activity_scanner.xml
│ │ │ ├── activity_settings.xml
│ │ │ ├── item_recycler_exclude.xml
│ │ │ ├── item_recycler_main.xml
│ │ │ ├── layout_link_form.xml
│ │ │ ├── layout_link_item.xml
│ │ │ ├── layout_tun_routes.xml
│ │ │ ├── loading_dialog.xml
│ │ │ ├── tab_advanced_settings.xml
│ │ │ ├── tab_basic_settings.xml
│ │ │ └── tab_config.xml
│ │ ├── menu/
│ │ │ ├── menu_apps_routing.xml
│ │ │ ├── menu_configs.xml
│ │ │ ├── menu_drawer.xml
│ │ │ ├── menu_links.xml
│ │ │ ├── menu_logs.xml
│ │ │ ├── menu_main.xml
│ │ │ ├── menu_profile.xml
│ │ │ └── menu_settings.xml
│ │ ├── values/
│ │ │ ├── array.xml
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ └── xml/
│ │ └── network_security_config.xml
│ └── versionCode.txt
├── build-xray.sh
├── build.gradle.kts
├── buildGo.sh
├── buildXrayCore.sh
├── buildXrayHelper.sh
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── metadata/
│ └── en-US/
│ ├── changelogs/
│ │ ├── 1004.txt
│ │ ├── 1014.txt
│ │ ├── 1024.txt
│ │ ├── 1034.txt
│ │ ├── 104.txt
│ │ ├── 1044.txt
│ │ ├── 1054.txt
│ │ ├── 1064.txt
│ │ ├── 114.txt
│ │ ├── 124.txt
│ │ ├── 134.txt
│ │ ├── 144.txt
│ │ ├── 154.txt
│ │ ├── 164.txt
│ │ ├── 174.txt
│ │ ├── 184.txt
│ │ ├── 194.txt
│ │ ├── 204.txt
│ │ ├── 214.txt
│ │ ├── 224.txt
│ │ ├── 234.txt
│ │ ├── 244.txt
│ │ ├── 254.txt
│ │ ├── 264.txt
│ │ ├── 274.txt
│ │ ├── 284.txt
│ │ ├── 294.txt
│ │ ├── 304.txt
│ │ ├── 314.txt
│ │ ├── 324.txt
│ │ ├── 334.txt
│ │ ├── 344.txt
│ │ ├── 354.txt
│ │ ├── 364.txt
│ │ ├── 374.txt
│ │ ├── 384.txt
│ │ ├── 394.txt
│ │ ├── 404.txt
│ │ ├── 414.txt
│ │ ├── 424.txt
│ │ ├── 504.txt
│ │ ├── 514.txt
│ │ ├── 524.txt
│ │ ├── 534.txt
│ │ ├── 54.txt
│ │ ├── 544.txt
│ │ ├── 604.txt
│ │ ├── 614.txt
│ │ ├── 624.txt
│ │ ├── 634.txt
│ │ ├── 64.txt
│ │ ├── 644.txt
│ │ ├── 654.txt
│ │ ├── 664.txt
│ │ ├── 674.txt
│ │ ├── 684.txt
│ │ ├── 694.txt
│ │ ├── 704.txt
│ │ ├── 714.txt
│ │ ├── 724.txt
│ │ ├── 734.txt
│ │ ├── 74.txt
│ │ ├── 744.txt
│ │ ├── 754.txt
│ │ ├── 764.txt
│ │ ├── 774.txt
│ │ ├── 784.txt
│ │ ├── 794.txt
│ │ ├── 804.txt
│ │ ├── 814.txt
│ │ ├── 824.txt
│ │ ├── 834.txt
│ │ ├── 844.txt
│ │ ├── 854.txt
│ │ ├── 864.txt
│ │ ├── 874.txt
│ │ ├── 884.txt
│ │ ├── 894.txt
│ │ ├── 904.txt
│ │ ├── 914.txt
│ │ ├── 924.txt
│ │ ├── 934.txt
│ │ ├── 944.txt
│ │ ├── 954.txt
│ │ ├── 964.txt
│ │ ├── 974.txt
│ │ ├── 984.txt
│ │ └── 994.txt
│ ├── full_description.txt
│ └── short_description.txt
└── settings.gradle.kts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/release.yml
================================================
name: Release CI
on:
push:
tags:
- '*'
jobs:
build-arm32:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build arm32 docker
run: docker build -t arm32 -f Dockerfile .
- name: Compile arm32 build
run: |
docker run --rm \
-v /opt/dist:/opt/dist \
-e DIST_DIR='/opt/dist' \
-e RELEASE_TAG="$GITHUB_REF_NAME" \
-e NATIVE_ARCH="arm" \
-e ABI_ID=1 \
-e ABI_TARGET="armeabi-v7a" \
-e KS_FILE="${{ secrets.KS_FILE }}" \
-e KS_PASSWORD="${{ secrets.KS_PASSWORD }}" \
-e KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \
-e KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \
arm32
- name: Upload arm32 artifact
uses: actions/upload-artifact@v4
with:
name: arm32-build
path: /opt/dist
build-arm64:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build arm64 docker
run: docker build -t arm64 -f Dockerfile .
- name: Compile arm64 build
run: |
docker run --rm \
-v /opt/dist:/opt/dist \
-e DIST_DIR='/opt/dist' \
-e RELEASE_TAG="$GITHUB_REF_NAME" \
-e NATIVE_ARCH="arm64" \
-e ABI_ID=2 \
-e ABI_TARGET="arm64-v8a" \
-e KS_FILE="${{ secrets.KS_FILE }}" \
-e KS_PASSWORD="${{ secrets.KS_PASSWORD }}" \
-e KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \
-e KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \
arm64
- name: Upload arm64 artifact
uses: actions/upload-artifact@v4
with:
name: arm64-build
path: /opt/dist
build-x86:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build x86 docker
run: docker build -t x86 -f Dockerfile .
- name: Compile x86 build
run: |
docker run --rm \
-v /opt/dist:/opt/dist \
-e DIST_DIR='/opt/dist' \
-e RELEASE_TAG="$GITHUB_REF_NAME" \
-e NATIVE_ARCH="386" \
-e ABI_ID=3 \
-e ABI_TARGET="x86" \
-e KS_FILE="${{ secrets.KS_FILE }}" \
-e KS_PASSWORD="${{ secrets.KS_PASSWORD }}" \
-e KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \
-e KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \
x86
- name: Upload x86 artifact
uses: actions/upload-artifact@v4
with:
name: x86-build
path: /opt/dist
build-amd64:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build amd64 docker
run: docker build -t amd64 -f Dockerfile .
- name: Compile amd64 build
run: |
docker run --rm \
-v /opt/dist:/opt/dist \
-e DIST_DIR='/opt/dist' \
-e RELEASE_TAG="$GITHUB_REF_NAME" \
-e NATIVE_ARCH="amd64" \
-e ABI_ID=4 \
-e ABI_TARGET="x86_64" \
-e KS_FILE="${{ secrets.KS_FILE }}" \
-e KS_PASSWORD="${{ secrets.KS_PASSWORD }}" \
-e KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \
-e KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \
amd64
- name: Upload amd64 artifact
uses: actions/upload-artifact@v4
with:
name: amd64-build
path: /opt/dist
publish:
runs-on: ubuntu-latest
needs:
- build-arm32
- build-arm64
- build-x86
- build-amd64
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download arm32 artifact
uses: actions/download-artifact@v4
with:
name: arm32-build
path: dist
- name: Download arm64 artifact
uses: actions/download-artifact@v4
with:
name: arm64-build
path: dist
- name: Download x86 artifact
uses: actions/download-artifact@v4
with:
name: x86-build
path: dist
- name: Download amd64 artifact
uses: actions/download-artifact@v4
with:
name: amd64-build
path: dist
- name: Set VERSION_CODE
run: |
ALL_VARIANTS=4
VERSION_CODE=$(cat "$GITHUB_WORKSPACE/app/versionCode.txt")
((VERSION_CODE += ALL_VARIANTS))
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
- name: Publish release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
prerelease: false
draft: false
files: "dist/*.apk"
body_path: ${{ github.workspace }}/metadata/en-US/changelogs/${{ env.VERSION_CODE }}.txt
================================================
FILE: .gitignore
================================================
*.iml
.idea
.kotlin
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
================================================
FILE: .gitmodules
================================================
[submodule "app/src/main/jni/hev-socks5-tunnel"]
path = app/src/main/jni/hev-socks5-tunnel
url = https://github.com/heiher/hev-socks5-tunnel.git
[submodule "XrayCore/libXray"]
path = XrayCore/libXray
url = https://github.com/XTLS/libXray.git
[submodule "XrayCore/Xray-core"]
path = XrayCore/Xray-core
url = https://github.com/XTLS/Xray-core.git
[submodule "XrayHelper"]
path = XrayHelper
url = https://github.com/SaeedDev94/XrayHelper.git
================================================
FILE: Dockerfile
================================================
FROM debian:trixie
ENV LANG=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive
COPY build-xray.sh /build-xray.sh
ENTRYPOINT ["/build-xray.sh"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 SaeedDev94
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Xray
This is a simple GUI client for XTLS/Xray-core
# Screenshots



# APK variants guide
- arm32 => versionCode + 1
- arm64 => versionCode + 2
- x86 => versionCode + 3
- amd64 => versionCode + 4
# Download
[](https://github.com/SaeedDev94/Xray/actions)
================================================
FILE: XrayCore/go.mod
================================================
module XrayCore
go 1.26.2
replace github.com/xtls/xray-core => ./Xray-core
replace github.com/xtls/libxray => ./libXray
require (
github.com/xtls/libxray v0.0.0-00010101000000-000000000000
github.com/xtls/xray-core v1.260327.0
)
require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pires/go-proxyproto v0.12.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect
github.com/sagernet/sing v0.5.1 // indirect
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.44.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
golang.zx2c4.com/wireguard/windows v0.6.1 // indirect
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)
================================================
FILE: XrayCore/go.sum
================================================
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 h1:cbF95uMsQwCwAzH2i8+2lNO2TReoELLuqeeMfyBjFbY=
github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
golang.zx2c4.com/wireguard/windows v0.6.1 h1:XMaKojH1Hs/raMrmnir4n35nTvzvWj7NmSYzHn2F4qU=
golang.zx2c4.com/wireguard/windows v0.6.1/go.mod h1:04aqInu5GYuTFvMuDw/rKBAF7mHrltW/3rekpfbbZDM=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 h1:aLsVTW0lZ8+IY5u/ERjZSCvAmhuR7slKzyha3YikDNA=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478/go.mod h1:YJAzKjfHIUHb9T+bfu8L7mthAp7VVXQBUs1PLdBWS7M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
================================================
FILE: XrayCore/lib/core.go
================================================
package lib
import (
"github.com/xtls/xray-core/common/cmdarg"
"github.com/xtls/xray-core/core"
_ "github.com/xtls/xray-core/main/distro/all"
)
var coreServer *core.Instance
func Server(config string) (*core.Instance, error) {
file := cmdarg.Arg{config}
json, err := core.LoadConfig("json", file)
if err != nil {
return nil, err
}
server, err := core.New(json)
if err != nil {
return nil, err
}
return server, nil
}
func Start(dir string, config string) (err error) {
SetEnv(dir)
coreServer, err = Server(config)
if err != nil {
return
}
if err = coreServer.Start(); err != nil {
return
}
return nil
}
func Stop() error {
if coreServer != nil {
err := coreServer.Close()
coreServer = nil
if err != nil {
return err
}
}
return nil
}
func Version() string {
return core.Version()
}
================================================
FILE: XrayCore/lib/env.go
================================================
package lib
import "os"
func SetEnv(dir string) {
os.Setenv("xray.location.asset", dir)
}
================================================
FILE: XrayCore/lib/error.go
================================================
package lib
func WrapError(err error) string {
if err != nil {
return err.Error()
}
return ""
}
================================================
FILE: XrayCore/lib/test.go
================================================
package lib
func Test(dir string, config string) error {
SetEnv(dir)
_, err := Server(config)
if err != nil {
return err
}
return nil
}
================================================
FILE: XrayCore/main.go
================================================
package XrayCore
import (
"github.com/xtls/xray-core/infra/conf"
"github.com/xtls/libxray/nodep"
"github.com/xtls/libxray/share"
"XrayCore/lib"
)
func Test(dir string, config string) string {
err := lib.Test(dir, config)
return lib.WrapError(err)
}
func Start(dir string, config string) string {
err := lib.Start(dir, config)
return lib.WrapError(err)
}
func Stop() string {
err := lib.Stop()
return lib.WrapError(err)
}
func Version() string {
return lib.Version()
}
func Json(link string) string {
var response nodep.CallResponse[*conf.Config]
xrayJson, err := share.ConvertShareLinksToXrayJson(link)
return response.EncodeToBase64(xrayJson, err)
}
================================================
FILE: app/.gitignore
================================================
/build
/release
================================================
FILE: app/build.gradle.kts
================================================
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.google.ksp)
}
val abiId: String by project
val abiTarget: String by project
fun calcVersionCode(): Int {
val versionCodeFile = file("versionCode.txt")
val versionCode = versionCodeFile.readText().trim().toInt()
return versionCode + abiId.toInt()
}
android {
namespace = "io.github.saeeddev94.xray"
compileSdk = 36
defaultConfig {
applicationId = "io.github.saeeddev94.xray"
minSdk = 26
targetSdk = 36
versionCode = calcVersionCode()
versionName = "12.2.0"
}
buildFeatures {
buildConfig = true
viewBinding = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
externalNativeBuild {
ndkVersion = "28.2.13676358"
ndkBuild {
path = file("src/main/jni/Android.mk")
}
}
splits {
abi {
isEnable = true
isUniversalApk = false
reset()
//noinspection ChromeOsAbiSupport
include(*abiTarget.split(",").toTypedArray())
}
}
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
signingConfigs {
create("release") {
storeFile = file("/tmp/xray.jks")
storePassword = System.getenv("KS_PASSWORD")
keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_PASSWORD")
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
}
kotlin {
compilerOptions {
languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21
}
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.blacksquircle.ui.editorkit)
implementation(libs.blacksquircle.ui.language.json)
implementation(libs.google.material)
implementation(libs.topjohnwu.libsu.core)
implementation(libs.yuriy.budiyev.code.scanner)
}
================================================
FILE: app/libs/.gitignore
================================================
*.aar
*.jar
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/assets/.gitignore
================================================
xrayhelper
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/Settings.kt
================================================
package io.github.saeeddev94.xray
import android.content.Context
import androidx.core.content.edit
import java.io.File
class Settings(private val context: Context) {
private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
/** Active Link ID */
var selectedLink: Long
get() = sharedPreferences.getLong("selectedLink", 0L)
set(value) = sharedPreferences.edit { putLong("selectedLink", value) }
/** Active Profile ID */
var selectedProfile: Long
get() = sharedPreferences.getLong("selectedProfile", 0L)
set(value) = sharedPreferences.edit { putLong("selectedProfile", value) }
/** The time of last refresh */
var lastRefreshLinks: Long
get() = sharedPreferences.getLong("lastRefreshLinks", 0L)
set(value) = sharedPreferences.edit { putLong("lastRefreshLinks", value) }
/** XrayHelper Version Code */
var xrayHelperVersionCode: Int
get() = sharedPreferences.getInt("xrayHelperVersionCode", 0)
set(value) = sharedPreferences.edit { putInt("xrayHelperVersionCode", value) }
/**
* Apps Routing
* Mode: true -> exclude, false -> include
* Default: exclude
*/
var appsRoutingMode: Boolean
get() = sharedPreferences.getBoolean("appsRoutingMode", true)
set(value) = sharedPreferences.edit { putBoolean("appsRoutingMode", value) }
var appsRouting: String
get() = sharedPreferences.getString("excludedApps", "")!!
set(value) = sharedPreferences.edit { putString("excludedApps", value) }
/** Tun Routes */
var tunRoutes: Set
get() = sharedPreferences.getStringSet(
"tunRoutes",
context.resources.getStringArray(R.array.publicIpAddresses).toSet()
)!!
set(value) = sharedPreferences.edit { putStringSet("tunRoutes", value) }
/** Basic */
var socksAddress: String
get() = sharedPreferences.getString("socksAddress", "127.0.0.1")!!
set(value) = sharedPreferences.edit { putString("socksAddress", value) }
var socksPort: String
get() = sharedPreferences.getString("socksPort", "10808")!!
set(value) = sharedPreferences.edit { putString("socksPort", value) }
var socksUsername: String
get() = sharedPreferences.getString("socksUsername", "")!!
set(value) = sharedPreferences.edit { putString("socksUsername", value) }
var socksPassword: String
get() = sharedPreferences.getString("socksPassword", "")!!
set(value) = sharedPreferences.edit { putString("socksPassword", value) }
var geoIpAddress: String
get() = sharedPreferences.getString(
"geoIpAddress",
"https://github.com/v2fly/geoip/releases/latest/download/geoip.dat"
)!!
set(value) = sharedPreferences.edit { putString("geoIpAddress", value) }
var geoSiteAddress: String
get() = sharedPreferences.getString(
"geoSiteAddress",
"https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
)!!
set(value) = sharedPreferences.edit { putString("geoSiteAddress", value) }
var pingAddress: String
get() = sharedPreferences.getString("pingAddress", "https://www.google.com")!!
set(value) = sharedPreferences.edit { putString("pingAddress", value) }
var pingTimeout: Int
get() = sharedPreferences.getInt("pingTimeout", 5)
set(value) = sharedPreferences.edit { putInt("pingTimeout", value) }
var refreshLinksInterval: Int
get() = sharedPreferences.getInt("refreshLinksInterval", 60)
set(value) = sharedPreferences.edit { putInt("refreshLinksInterval", value) }
var bypassLan: Boolean
get() = sharedPreferences.getBoolean("bypassLan", true)
set(value) = sharedPreferences.edit { putBoolean("bypassLan", value) }
var enableIpV6: Boolean
get() = sharedPreferences.getBoolean("enableIpV6", true)
set(value) = sharedPreferences.edit { putBoolean("enableIpV6", value) }
var socksUdp: Boolean
get() = sharedPreferences.getBoolean("socksUdp", true)
set(value) = sharedPreferences.edit { putBoolean("socksUdp", value) }
var tun2socks: Boolean
get() = sharedPreferences.getBoolean("tun2socks", true)
set(value) = sharedPreferences.edit { putBoolean("tun2socks", value) }
var bootAutoStart: Boolean
get() = sharedPreferences.getBoolean("bootAutoStart", false)
set(value) = sharedPreferences.edit { putBoolean("bootAutoStart", value) }
var refreshLinksOnOpen: Boolean
get() = sharedPreferences.getBoolean("refreshLinksOnOpen", false)
set(value) = sharedPreferences.edit { putBoolean("refreshLinksOnOpen", value) }
/** Advanced */
var primaryDns: String
get() = sharedPreferences.getString("primaryDns", "1.1.1.1")!!
set(value) = sharedPreferences.edit { putString("primaryDns", value) }
var secondaryDns: String
get() = sharedPreferences.getString("secondaryDns", "1.0.0.1")!!
set(value) = sharedPreferences.edit { putString("secondaryDns", value) }
var primaryDnsV6: String
get() = sharedPreferences.getString("primaryDnsV6", "2606:4700:4700::1111")!!
set(value) = sharedPreferences.edit { putString("primaryDnsV6", value) }
var secondaryDnsV6: String
get() = sharedPreferences.getString("secondaryDnsV6", "2606:4700:4700::1001")!!
set(value) = sharedPreferences.edit { putString("secondaryDnsV6", value) }
var tunName: String
get() = sharedPreferences.getString("tunName", "tun0")!!
set(value) = sharedPreferences.edit { putString("tunName", value) }
var tunMtu: Int
get() = sharedPreferences.getInt("tunMtu", 8500)
set(value) = sharedPreferences.edit { putInt("tunMtu", value) }
var tunAddress: String
get() = sharedPreferences.getString("tunAddress", "10.10.10.10")!!
set(value) = sharedPreferences.edit { putString("tunAddress", value) }
var tunPrefix: Int
get() = sharedPreferences.getInt("tunPrefix", 32)
set(value) = sharedPreferences.edit { putInt("tunPrefix", value) }
var tunAddressV6: String
get() = sharedPreferences.getString("tunAddressV6", "fc00::1")!!
set(value) = sharedPreferences.edit { putString("tunAddressV6", value) }
var tunPrefixV6: Int
get() = sharedPreferences.getInt("tunPrefixV6", 128)
set(value) = sharedPreferences.edit { putInt("tunPrefixV6", value) }
var hotspotInterface
get() = sharedPreferences.getString("hotspotInterface", "wlan2")!!
set(value) = sharedPreferences.edit { putString("hotspotInterface", value) }
var tetheringInterface
get() = sharedPreferences.getString("tetheringInterface", "rndis0")!!
set(value) = sharedPreferences.edit { putString("tetheringInterface", value) }
var tproxyAddress: String
get() = sharedPreferences.getString("tproxyAddress", "127.0.0.1")!!
set(value) = sharedPreferences.edit { putString("tproxyAddress", value) }
var tproxyPort: Int
get() = sharedPreferences.getInt("tproxyPort", 10888)
set(value) = sharedPreferences.edit { putInt("tproxyPort", value) }
var tproxyBypassWiFi: Set
get() = sharedPreferences.getStringSet("tproxyBypassWiFi", mutableSetOf())!!
set(value) = sharedPreferences.edit { putStringSet("tproxyBypassWiFi", value) }
var tproxyAutoConnect: Boolean
get() = sharedPreferences.getBoolean("tproxyAutoConnect", false)
set(value) = sharedPreferences.edit { putBoolean("tproxyAutoConnect", value) }
var tproxyHotspot: Boolean
get() = sharedPreferences.getBoolean("tproxyHotspot", false)
set(value) = sharedPreferences.edit { putBoolean("tproxyHotspot", value) }
var tproxyTethering: Boolean
get() = sharedPreferences.getBoolean("tproxyTethering", false)
set(value) = sharedPreferences.edit { putBoolean("tproxyTethering", value) }
var transparentProxy: Boolean
get() = sharedPreferences.getBoolean("transparentProxy", false)
set(value) = sharedPreferences.edit { putBoolean("transparentProxy", value) }
fun baseDir(): File = context.filesDir
fun xrayCoreFile(): File = File(baseDir(), "xray")
fun xrayHelperFile(): File = File(baseDir(), "xrayhelper")
fun testConfig(): File = File(baseDir(), "test.json")
fun xrayConfig(): File = File(baseDir(), "config.json")
fun tun2socksConfig(): File = File(baseDir(), "tun2socks.yml")
fun xrayHelperConfig(): File = File(baseDir(), "config.yml")
fun xrayCorePid(): File = File(baseDir(), "core.pid")
fun networkMonitorPid(): File = File(baseDir(), "monitor.pid")
fun networkMonitorScript(): File = File(baseDir(), "monitor.sh")
fun xrayCoreLogs(): File = File(baseDir(), "error.log")
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/Xray.kt
================================================
package io.github.saeeddev94.xray
import android.app.Application
import io.github.saeeddev94.xray.database.XrayDatabase
import io.github.saeeddev94.xray.repository.ConfigRepository
import io.github.saeeddev94.xray.repository.LinkRepository
import io.github.saeeddev94.xray.repository.ProfileRepository
class Xray : Application() {
private val xrayDatabase by lazy { XrayDatabase.ref(this) }
val configRepository by lazy { ConfigRepository(xrayDatabase.configDao()) }
val linkRepository by lazy { LinkRepository(xrayDatabase.linkDao()) }
val profileRepository by lazy { ProfileRepository(xrayDatabase.profileDao()) }
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/AppsRoutingActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.adapter.AppsRoutingAdapter
import io.github.saeeddev94.xray.databinding.ActivityAppsRoutingBinding
import io.github.saeeddev94.xray.dto.AppList
import io.github.saeeddev94.xray.helper.TransparentProxyHelper
import io.github.saeeddev94.xray.service.TProxyService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AppsRoutingActivity : AppCompatActivity() {
private val settings by lazy { Settings(applicationContext) }
private val transparentProxyHelper by lazy { TransparentProxyHelper(this, settings) }
private lateinit var binding: ActivityAppsRoutingBinding
private lateinit var appsList: RecyclerView
private lateinit var appsRoutingAdapter: AppsRoutingAdapter
private lateinit var apps: ArrayList
private lateinit var filtered: MutableList
private lateinit var appsRouting: MutableSet
private lateinit var menu: Menu
private var appsRoutingMode: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = ""
binding = ActivityAppsRoutingBinding.inflate(layoutInflater)
appsRoutingMode = settings.appsRoutingMode
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.search.focusable = View.NOT_FOCUSABLE
binding.search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextChange(newText: String?): Boolean {
search(newText)
return false
}
override fun onQueryTextSubmit(query: String?): Boolean {
binding.search.clearFocus()
search(query)
return false
}
})
binding.search.findViewById(androidx.appcompat.R.id.search_close_btn)
?.setOnClickListener {
binding.search.setQuery("", false)
binding.search.clearFocus()
}
getApps()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
this.menu = menu
menuInflater.inflate(R.menu.menu_apps_routing, menu)
handleMode()
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.appsRoutingSave -> saveAppsRouting()
R.id.appsRoutingExcludeMode -> setMode(false)
R.id.appsRoutingIncludeMode -> setMode(true)
else -> finish()
}
return true
}
private fun setMode(appsRoutingMode: Boolean) {
this.appsRoutingMode = appsRoutingMode
handleMode().also { message ->
Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show()
}
}
private fun handleMode(): String {
val excludeItem = menu.findItem(R.id.appsRoutingExcludeMode)
val includeItem = menu.findItem(R.id.appsRoutingIncludeMode)
return when (this.appsRoutingMode) {
true -> {
excludeItem.isVisible = true
includeItem.isVisible = false
getString(R.string.appsRoutingExcludeMode)
}
false -> {
excludeItem.isVisible = false
includeItem.isVisible = true
getString(R.string.appsRoutingIncludeMode)
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun search(query: String?) {
val keyword = query?.trim()?.lowercase() ?: ""
if (keyword.isEmpty()) {
if (apps.size > filtered.size) {
filtered.clear()
filtered.addAll(apps.toMutableList())
appsRoutingAdapter.notifyDataSetChanged()
}
return
}
val list = ArrayList()
apps.forEach {
if (it.appName.lowercase().contains(keyword) || it.packageName.contains(keyword)) {
list.add(it)
}
}
filtered.clear()
filtered.addAll(list.toMutableList())
appsRoutingAdapter.notifyDataSetChanged()
}
private fun getApps() {
lifecycleScope.launch {
val selected = ArrayList()
val unselected = ArrayList()
packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS).forEach {
val permissions = it.requestedPermissions
if (
permissions == null || !permissions.contains(Manifest.permission.INTERNET)
) return@forEach
val appIcon = it.applicationInfo!!.loadIcon(packageManager)
val appName = it.applicationInfo!!.loadLabel(packageManager).toString()
val packageName = it.packageName
val app = AppList(appIcon, appName, packageName)
val isSelected = settings.appsRouting.contains(packageName)
if (isSelected) selected.add(app) else unselected.add(app)
}
withContext(Dispatchers.Main) {
apps = ArrayList(selected + unselected)
filtered = apps.toMutableList()
appsRouting = settings.appsRouting.split("\n").toMutableSet()
appsList = binding.appsList
appsRoutingAdapter = AppsRoutingAdapter(
this@AppsRoutingActivity, filtered, appsRouting
)
appsList.adapter = appsRoutingAdapter
appsList.layoutManager = LinearLayoutManager(applicationContext)
}
}
}
private fun saveAppsRouting() {
val appsRoutingMode = this.appsRoutingMode
val appsRouting = this.appsRouting.joinToString("\n")
lifecycleScope.launch {
val tproxySettingsChanged = settings.appsRoutingMode != appsRoutingMode ||
settings.appsRouting != appsRouting
val stopService = tproxySettingsChanged && settings.xrayCorePid().exists()
if (tproxySettingsChanged) transparentProxyHelper.kill()
withContext(Dispatchers.Main) {
binding.search.clearFocus()
settings.appsRoutingMode = appsRoutingMode
settings.appsRouting = appsRouting
if (stopService) TProxyService.stop(this@AppsRoutingActivity)
finish()
}
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/AssetsActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.superuser.Shell
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.databinding.ActivityAssetsBinding
import io.github.saeeddev94.xray.helper.DownloadHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.text.toRegex
class AssetsActivity : AppCompatActivity() {
private lateinit var binding: ActivityAssetsBinding
private var downloading: Boolean = false
private val settings by lazy { Settings(applicationContext) }
private val geoIpLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
writeToFile(it, geoIpFile())
}
private val geoSiteLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
writeToFile(it, geoSiteFile())
}
private val xrayCoreLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
val file = settings.xrayCoreFile()
writeToFile(it, file) { makeExeFile(file) }
}
private fun geoIpFile(): File = File(applicationContext.filesDir, "geoip.dat")
private fun geoSiteFile(): File = File(applicationContext.filesDir, "geosite.dat")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mimeType = "application/octet-stream"
title = getString(R.string.assets)
binding = ActivityAssetsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
setAssetStatus()
// GeoIP
binding.geoIpDownload.setOnClickListener {
download(settings.geoIpAddress, geoIpFile(), binding.geoIpSetup, binding.geoIpProgress)
}
binding.geoIpFile.setOnClickListener { geoIpLauncher.launch(mimeType) }
binding.geoIpDelete.setOnClickListener { delete(geoIpFile()) }
// GeoSite
binding.geoSiteDownload.setOnClickListener {
download(
settings.geoSiteAddress,
geoSiteFile(),
binding.geoSiteSetup,
binding.geoSiteProgress
)
}
binding.geoSiteFile.setOnClickListener { geoSiteLauncher.launch(mimeType) }
binding.geoSiteDelete.setOnClickListener { delete(geoSiteFile()) }
// XTLS/Xray-core
binding.xrayCoreFile.setOnClickListener { runAsRoot { xrayCoreLauncher.launch(mimeType) } }
binding.xrayCoreDelete.setOnClickListener { delete(settings.xrayCoreFile()) }
}
@SuppressLint("SimpleDateFormat")
private fun getFileDate(file: File): String {
return if (file.exists()) {
val date = Date(file.lastModified())
SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(date)
} else {
getString(R.string.noValue)
}
}
private fun getXrayCoreVersion(file: File): String {
return getExeVersion(file, "${file.absolutePath} version")
}
private fun getExeVersion(file: File, cmd: String): String {
val exists = file.exists()
val invalid = {
delete(file)
"Invalid"
}
return if (exists) {
val result = Shell.cmd(cmd).exec()
if (result.isSuccess) {
val txt = result.out.first()
val match = "Xray (.*?) ".toRegex().find(txt)
match?.groups?.get(1)?.value ?: invalid()
} else invalid()
} else getString(R.string.noValue)
}
private fun setAssetStatus() {
val geoIp = geoIpFile()
val geoIpExists = geoIp.exists()
binding.geoIpDate.text = getFileDate(geoIp)
binding.geoIpSetup.visibility = if (geoIpExists) View.GONE else View.VISIBLE
binding.geoIpInstalled.visibility = if (geoIpExists) View.VISIBLE else View.GONE
binding.geoIpProgress.visibility = View.GONE
val geoSite = geoSiteFile()
val geoSiteExists = geoSite.exists()
binding.geoSiteDate.text = getFileDate(geoSite)
binding.geoSiteSetup.visibility = if (geoSiteExists) View.GONE else View.VISIBLE
binding.geoSiteInstalled.visibility = if (geoSiteExists) View.VISIBLE else View.GONE
binding.geoSiteProgress.visibility = View.GONE
val xrayCore = settings.xrayCoreFile()
val xrayCoreExists = xrayCore.exists()
binding.xrayCoreVersion.text = getXrayCoreVersion(xrayCore)
binding.xrayCoreSetup.isVisible = !xrayCoreExists
binding.xrayCoreInstalled.isVisible = xrayCoreExists
}
private fun download(url: String, file: File, setup: LinearLayout, progressBar: ProgressBar) {
if (downloading) {
Toast.makeText(
applicationContext, "Another download is running, please wait", Toast.LENGTH_SHORT
).show()
return
}
setup.visibility = View.GONE
progressBar.visibility = View.VISIBLE
progressBar.progress = 0
downloading = true
DownloadHelper(lifecycleScope, url, file, object : DownloadHelper.DownloadListener {
override fun onProgress(progress: Int) {
progressBar.progress = progress
}
override fun onError(exception: Exception) {
downloading = false
Toast.makeText(applicationContext, exception.message, Toast.LENGTH_SHORT).show()
setAssetStatus()
}
override fun onComplete() {
downloading = false
setAssetStatus()
}
}).start()
}
private fun writeToFile(uri: Uri?, file: File, cb: (() -> Unit)? = null) {
if (uri == null) return
lifecycleScope.launch {
contentResolver.openInputStream(uri).use { input ->
FileOutputStream(file).use { output ->
input?.copyTo(output)
}
}
if (cb != null) cb()
withContext(Dispatchers.Main) {
setAssetStatus()
}
}
}
private fun makeExeFile(file: File) {
Shell.cmd("chown root:root ${file.absolutePath}").exec()
Shell.cmd("chmod +x ${file.absolutePath}").exec()
}
private fun delete(file: File) {
lifecycleScope.launch {
file.delete()
withContext(Dispatchers.Main) {
setAssetStatus()
}
}
}
private fun runAsRoot(cb: () -> Unit) {
val result = Shell.cmd("whoami").exec()
if (result.isSuccess && result.out.first() == "root") {
cb()
return
}
Toast.makeText(this, "Root Required", Toast.LENGTH_SHORT).show()
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/ConfigsActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.blacksquircle.ui.editorkit.plugin.autoindent.autoIndentation
import com.blacksquircle.ui.editorkit.plugin.base.PluginSupplier
import com.blacksquircle.ui.editorkit.plugin.delimiters.highlightDelimiters
import com.blacksquircle.ui.editorkit.plugin.linenumbers.lineNumbers
import com.blacksquircle.ui.editorkit.widget.TextProcessor
import com.blacksquircle.ui.language.json.JsonLanguage
import com.google.android.material.radiobutton.MaterialRadioButton
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.adapter.ConfigAdapter
import io.github.saeeddev94.xray.database.Config
import io.github.saeeddev94.xray.databinding.ActivityConfigsBinding
import io.github.saeeddev94.xray.viewmodel.ConfigViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import kotlin.getValue
import kotlin.reflect.cast
class ConfigsActivity : AppCompatActivity() {
private lateinit var binding: ActivityConfigsBinding
private lateinit var config: Config
private lateinit var adapter: ConfigAdapter
private val configViewModel: ConfigViewModel by viewModels()
private val radioGroup = mutableMapOf()
private val configEditor = mutableMapOf()
private val indentSpaces = 4
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = getString(R.string.configs)
binding = ActivityConfigsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
lifecycleScope.launch {
config = configViewModel.get()
withContext(Dispatchers.Main) {
val context = this@ConfigsActivity
val tabs = listOf("log", "dns", "inbounds", "outbounds", "routing")
adapter = ConfigAdapter(context, tabs) { tab, view -> setup(tab, view) }
binding.viewPager.adapter = adapter
binding.tabLayout.setupWithViewPager(binding.viewPager)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_configs, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.saveConfigs -> saveConfigs()
else -> finish()
}
return true
}
private fun setup(tab: String, view: View) {
val mode = getMode(tab)
val config = getConfig(tab)
val modeRadioGroup = view.findViewById(R.id.modeRadioGroup)
modeRadioGroup.removeAllViews()
Config.Mode.entries.forEach {
val radio = MaterialRadioButton(this)
radio.text = it.name
radio.tag = it
modeRadioGroup.addView(radio)
if (it == mode) modeRadioGroup.check(radio.id)
}
radioGroup.put(tab, modeRadioGroup)
val editor = view.findViewById(R.id.config)
val pluginSupplier = PluginSupplier.create {
lineNumbers {
lineNumbers = true
highlightCurrentLine = true
}
highlightDelimiters()
autoIndentation {
autoIndentLines = true
autoCloseBrackets = true
autoCloseQuotes = true
}
}
editor.language = JsonLanguage()
editor.setTextContent(config)
editor.plugins(pluginSupplier)
configEditor.put(tab, editor)
}
private fun getConfig(tab: String): String {
val editor = configEditor[tab]
val default = ""
return if (editor == null) {
when (tab) {
"log" -> config.log
"dns" -> config.dns
"inbounds" -> config.inbounds
"outbounds" -> config.outbounds
"routing" -> config.routing
else -> default
}
} else getViewConfig(tab, default)
}
private fun getMode(tab: String): Config.Mode {
val group = configEditor[tab]
val default = Config.Mode.Disable
return if (group == null) {
when (tab) {
"log" -> config.logMode
"dns" -> config.dnsMode
"inbounds" -> config.inboundsMode
"outbounds" -> config.outboundsMode
"routing" -> config.routingMode
else -> default
}
} else getViewMode(tab, default)
}
private fun getViewConfig(tab: String, default: String): String {
val editor = configEditor[tab]
if (editor == null) return default
return editor.text.toString()
}
private fun getViewMode(tab: String, default: Config.Mode): Config.Mode {
val group = radioGroup[tab]
if (group == null) return default
val modeRadioButton = group.findViewById(
group.checkedRadioButtonId
)
return Config.Mode::class.cast(modeRadioButton.tag)
}
private fun formatConfig(tab: String, default: String): String {
val json = getViewConfig(tab, default)
if (arrayOf("inbounds", "outbounds").contains(tab)) return JSONArray(json).toString(indentSpaces)
return JSONObject(json).toString(indentSpaces)
}
private fun saveConfigs() {
runCatching {
config.log = formatConfig("log", config.log)
config.dns = formatConfig("dns", config.dns)
config.inbounds = formatConfig("inbounds", config.inbounds)
config.outbounds = formatConfig("outbounds", config.outbounds)
config.routing = formatConfig("routing", config.routing)
config.logMode = getViewMode("log", config.logMode)
config.dnsMode = getViewMode("dns", config.dnsMode)
config.inboundsMode = getViewMode("inbounds", config.inboundsMode)
config.outboundsMode = getViewMode("outbounds", config.outboundsMode)
config.routingMode = getViewMode("routing", config.routingMode)
config
}.onSuccess {
configViewModel.update(it)
finish()
}.onFailure {
Toast.makeText(
this, "Invalid config", Toast.LENGTH_SHORT
).show()
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/LinksActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.adapter.LinkAdapter
import io.github.saeeddev94.xray.database.Link
import io.github.saeeddev94.xray.databinding.ActivityLinksBinding
import io.github.saeeddev94.xray.viewmodel.LinkViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class LinksActivity : AppCompatActivity() {
private val linkViewModel: LinkViewModel by viewModels()
private val adapter by lazy { LinkAdapter() }
private val linksRecyclerView by lazy { findViewById(R.id.linksRecyclerView) }
private var links: MutableList = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = getString(R.string.links)
val binding = ActivityLinksBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
adapter.onEditClick = { link -> openLink(link) }
adapter.onDeleteClick = { link -> deleteLink(link) }
linksRecyclerView.layoutManager = LinearLayoutManager(this)
linksRecyclerView.itemAnimator = DefaultItemAnimator()
linksRecyclerView.adapter = adapter
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
linkViewModel.links.collectLatest {
links = it.toMutableList()
adapter.submitList(it)
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_links, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.refreshLinks -> refreshLinks()
R.id.newLink -> openLink()
else -> finish()
}
return true
}
private fun refreshLinks() {
val intent = LinksManagerActivity.refreshLinks(applicationContext)
startActivity(intent)
}
private fun openLink(link: Link = Link()) {
val intent = LinksManagerActivity.openLink(applicationContext, link)
startActivity(intent)
}
private fun deleteLink(link: Link) {
val intent = LinksManagerActivity.deleteLink(applicationContext, link)
startActivity(intent)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/LinksManagerActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.database.Link
import io.github.saeeddev94.xray.database.Profile
import io.github.saeeddev94.xray.fragment.LinkFormFragment
import io.github.saeeddev94.xray.helper.HttpHelper
import io.github.saeeddev94.xray.helper.IntentHelper
import io.github.saeeddev94.xray.helper.LinkHelper
import io.github.saeeddev94.xray.service.TProxyService
import io.github.saeeddev94.xray.viewmodel.LinkViewModel
import io.github.saeeddev94.xray.viewmodel.ProfileViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import kotlin.reflect.cast
class LinksManagerActivity : AppCompatActivity() {
companion object {
private const val LINK_REF = "ref"
private const val DELETE_ACTION = "delete"
fun refreshLinks(context: Context): Intent {
return Intent(context, LinksManagerActivity::class.java)
}
fun openLink(context: Context, link: Link = Link()): Intent {
return Intent(context, LinksManagerActivity::class.java).apply {
putExtra(LINK_REF, link)
}
}
fun deleteLink(context: Context, link: Link): Intent {
return Intent(context, LinksManagerActivity::class.java).apply {
putExtra(LINK_REF, link)
putExtra(DELETE_ACTION, true)
}
}
}
private val settings by lazy { Settings(applicationContext) }
private val linkViewModel: LinkViewModel by viewModels()
private val profileViewModel: ProfileViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val link: Link? = IntentHelper.getParcelable(intent, LINK_REF, Link::class.java)
val deleteAction = intent.getBooleanExtra(DELETE_ACTION, false)
if (link == null) {
refreshLinks()
return
}
if (deleteAction) {
deleteLink(link)
return
}
LinkFormFragment(link) {
if (link.id == 0L) {
linkViewModel.insert(link)
} else {
linkViewModel.update(link)
}
setResult(RESULT_OK)
finish()
}.show(supportFragmentManager, null)
}
private fun loadingDialog(): Dialog {
val dialogView = LayoutInflater.from(this).inflate(
R.layout.loading_dialog,
LinearLayout(this)
)
return MaterialAlertDialogBuilder(this)
.setView(dialogView)
.setCancelable(false)
.create()
}
private fun refreshLinks() {
val loadingDialog = loadingDialog()
loadingDialog.show()
lifecycleScope.launch {
val links = linkViewModel.activeLinks()
links.forEach { link ->
val profiles = profileViewModel.linkProfiles(link.id)
runCatching {
val content = HttpHelper.get(link.address, link.userAgent).trim()
val newProfiles = if (link.type == Link.Type.Json) {
jsonProfiles(link, content)
} else {
subscriptionProfiles(link, content)
}
if (newProfiles.isNotEmpty()) {
val linkProfiles = profiles.filter { it.linkId == link.id }
manageProfiles(link, linkProfiles, newProfiles)
}
}
}
withContext(Dispatchers.Main) {
settings.lastRefreshLinks = System.currentTimeMillis()
TProxyService.newConfig(applicationContext)
loadingDialog.dismiss()
finish()
}
}
}
private fun jsonProfiles(link: Link, value: String): List {
val list = arrayListOf()
val configs = runCatching { JSONArray(value) }.getOrNull() ?: JSONArray()
for (i in 0 until configs.length()) {
runCatching { JSONObject::class.cast(configs[i]) }.getOrNull()?.let { configuration ->
val label = if (configuration.has("remarks")) {
val remarks = configuration.getString("remarks")
configuration.remove("remarks")
remarks
} else {
LinkHelper.REMARK_DEFAULT
}
val json = configuration.toString(2)
val profile = Profile().apply {
linkId = link.id
name = label
config = json
}
list.add(profile)
}
}
return list.reversed().toList()
}
private fun subscriptionProfiles(link: Link, value: String): List {
val decoded = runCatching { LinkHelper.tryDecodeBase64(value).trim() }.getOrNull() ?: ""
return decoded.split("\n")
.reversed()
.map { LinkHelper(settings, it) }
.filter { it.isValid() }
.map { linkHelper ->
val profile = Profile()
profile.linkId = link.id
profile.config = linkHelper.json()
profile.name = linkHelper.remark()
profile
}
}
private suspend fun manageProfiles(
link: Link, linkProfiles: List, newProfiles: List
) {
if (newProfiles.size >= linkProfiles.size) {
newProfiles.forEachIndexed { index, newProfile ->
if (index >= linkProfiles.size) {
newProfile.linkId = link.id
insertProfile(newProfile)
} else {
val linkProfile = linkProfiles[index]
updateProfile(linkProfile, newProfile)
}
}
return
}
linkProfiles.forEachIndexed { index, linkProfile ->
if (index >= newProfiles.size) {
deleteProfile(linkProfile)
} else {
val newProfile = newProfiles[index]
updateProfile(linkProfile, newProfile)
}
}
}
private suspend fun insertProfile(newProfile: Profile) {
profileViewModel.create(newProfile)
}
private suspend fun updateProfile(linkProfile: Profile, newProfile: Profile) {
linkProfile.name = newProfile.name
linkProfile.config = newProfile.config
profileViewModel.update(linkProfile)
}
private suspend fun deleteProfile(linkProfile: Profile) {
profileViewModel.remove(linkProfile)
withContext(Dispatchers.Main) {
val selectedProfile = settings.selectedProfile
if (selectedProfile == linkProfile.id) {
settings.selectedProfile = 0L
}
}
}
private fun deleteLink(link: Link) {
lifecycleScope.launch {
profileViewModel.linkProfiles(link.id)
.forEach { linkProfile ->
deleteProfile(linkProfile)
}
linkViewModel.delete(link)
withContext(Dispatchers.Main) {
finish()
}
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/LogsActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import io.github.saeeddev94.xray.BuildConfig
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.databinding.ActivityLogsBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
class LogsActivity : AppCompatActivity() {
private lateinit var binding: ActivityLogsBinding
private val settings by lazy { Settings(applicationContext) }
companion object {
private const val MAX_BUFFERED_LINES = (1 shl 14) - 1
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = getString(R.string.logs)
binding = ActivityLogsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
lifecycleScope.launch(Dispatchers.IO) { streamingLog() }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_logs, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.deleteLogs -> flush()
R.id.copyLogs -> copyToClipboard(binding.logsTextView.text.toString())
else -> finish()
}
return true
}
private fun flush() {
lifecycleScope.launch(Dispatchers.IO) {
val command = if (settings.transparentProxy) {
listOf("echo", "''", ">", settings.xrayCoreLogs().absolutePath)
} else {
listOf("logcat", "-c")
}
val process = ProcessBuilder(command).start()
process.waitFor()
withContext(Dispatchers.Main) {
binding.logsTextView.text = ""
}
}
}
private fun copyToClipboard(text: String) {
if (text.isBlank()) return
try {
val clipData = ClipData.newPlainText(null, text)
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager.setPrimaryClip(clipData)
Toast.makeText(applicationContext, "Logs copied", Toast.LENGTH_SHORT).show()
} catch (error: Exception) {
error.printStackTrace()
}
}
@SuppressLint("SetTextI18n")
private suspend fun streamingLog() = withContext(Dispatchers.IO) {
val cmd = if (settings.transparentProxy) {
listOf("tail", "-f", settings.xrayCoreLogs().absolutePath)
} else {
listOf("logcat", "-v", "time", "-s", "GoLog,${BuildConfig.APPLICATION_ID}")
}
val builder = ProcessBuilder(cmd)
builder.environment()["LC_ALL"] = "C"
var process: Process? = null
try {
process = try {
builder.start()
} catch (e: IOException) {
Log.e(packageName, Log.getStackTraceString(e))
return@withContext
}
val stdout = BufferedReader(
InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8)
)
val bufferedLogLines = arrayListOf()
var timeLastNotify = System.nanoTime()
// The timeout is initially small so that the view gets populated immediately.
var timeout = 1000000000L / 2
while (true) {
val line = stdout.readLine() ?: break
bufferedLogLines.add(line)
val timeNow = System.nanoTime()
if (
bufferedLogLines.size < MAX_BUFFERED_LINES &&
(timeNow - timeLastNotify) < timeout && stdout.ready()
) continue
// Increase the timeout after the initial view has something in it.
timeout = 1000000000L * 5 / 2
timeLastNotify = timeNow
withContext(Dispatchers.Main) {
val contentHeight = binding.logsTextView.height
val scrollViewHeight = binding.logsScrollView.height
val isScrolledToBottomAlready =
(binding.logsScrollView.scrollY + scrollViewHeight) >= contentHeight * 0.95
binding.logsTextView.text =
binding.logsTextView.text.toString() + bufferedLogLines.joinToString(
separator = "\n",
postfix = "\n"
)
bufferedLogLines.clear()
if (isScrolledToBottomAlready) {
binding.logsScrollView.post {
binding.logsScrollView.fullScroll(View.FOCUS_DOWN)
}
}
}
}
} finally {
process?.destroy()
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/MainActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import XrayCore.XrayCore
import android.content.BroadcastReceiver
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.ColorStateList
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.view.GravityCompat
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.navigation.NavigationView
import com.google.android.material.tabs.TabLayout
import io.github.saeeddev94.xray.BuildConfig
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.adapter.ProfileAdapter
import io.github.saeeddev94.xray.database.Link
import io.github.saeeddev94.xray.databinding.ActivityMainBinding
import io.github.saeeddev94.xray.dto.ProfileList
import io.github.saeeddev94.xray.helper.HttpHelper
import io.github.saeeddev94.xray.helper.LinkHelper
import io.github.saeeddev94.xray.helper.ProfileTouchHelper
import io.github.saeeddev94.xray.helper.TransparentProxyHelper
import io.github.saeeddev94.xray.service.TProxyService
import io.github.saeeddev94.xray.viewmodel.LinkViewModel
import io.github.saeeddev94.xray.viewmodel.ProfileViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URI
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private val clipboardManager by lazy { getSystemService(ClipboardManager::class.java) }
private val settings by lazy { Settings(applicationContext) }
private val transparentProxyHelper by lazy { TransparentProxyHelper(this, settings) }
private val linkViewModel: LinkViewModel by viewModels()
private val profileViewModel: ProfileViewModel by viewModels()
private var isRunning: Boolean = false
private lateinit var binding: ActivityMainBinding
private lateinit var profileAdapter: ProfileAdapter
private lateinit var tabs: List
private val profilesRecyclerView by lazy { findViewById(R.id.profilesRecyclerView) }
private val profiles = arrayListOf()
private var cameraPermission = registerForActivityResult(RequestPermission()) {
if (!it) return@registerForActivityResult
scannerLauncher.launch(
Intent(applicationContext, ScannerActivity::class.java)
)
}
private val notificationPermission = registerForActivityResult(RequestPermission()) {
onToggleButtonClick()
}
private val linksManager = registerForActivityResult(StartActivityForResult()) {
if (it.resultCode != RESULT_OK) return@registerForActivityResult
refreshLinks()
}
private var scannerLauncher = registerForActivityResult(StartActivityForResult()) {
val link = it.data?.getStringExtra("link")
if (it.resultCode != RESULT_OK || link == null) return@registerForActivityResult
this@MainActivity.processLink(link)
}
private val vpnLauncher = registerForActivityResult(StartActivityForResult()) {
if (it.resultCode != RESULT_OK) return@registerForActivityResult
toggleVpnService()
}
private val vpnServiceEventReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (context == null || intent == null) return
when (intent.action) {
TProxyService.START_VPN_SERVICE_ACTION_NAME -> vpnStartStatus()
TProxyService.STOP_VPN_SERVICE_ACTION_NAME -> vpnStopStatus()
TProxyService.STATUS_VPN_SERVICE_ACTION_NAME -> {
intent.getBooleanExtra("isRunning", false).let { isRunning ->
if (isRunning) vpnStartStatus()
else vpnStopStatus()
}
}
}
}
}
private val linksTabListener = object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
if (tab == null) return
settings.selectedLink = tab.tag.toString().toLong()
profileViewModel.next(settings.selectedLink)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
binding.toggleButton.setOnClickListener { onToggleButtonClick() }
binding.pingBox.setOnClickListener { ping() }
binding.navView.menu.findItem(R.id.appVersion).title = BuildConfig.VERSION_NAME
binding.navView.menu.findItem(R.id.xrayVersion).title = XrayCore.version()
binding.navView.setNavigationItemSelectedListener(this)
ActionBarDrawerToggle(
this, binding.drawerLayout, binding.toolbar,
R.string.drawerOpen, R.string.drawerClose
).also {
binding.drawerLayout.addDrawerListener(it)
it.syncState()
}
profileAdapter = ProfileAdapter(
lifecycleScope,
settings,
profileViewModel,
profiles,
{ index, profile -> profileSelect(index, profile) },
{ profile -> profileEdit(profile) },
{ profile -> profileDelete(profile) },
)
profilesRecyclerView.adapter = profileAdapter
profilesRecyclerView.layoutManager = LinearLayoutManager(applicationContext)
ItemTouchHelper(ProfileTouchHelper(profileAdapter)).also {
it.attachToRecyclerView(profilesRecyclerView)
}
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
linkViewModel.tabs.collectLatest { onNewTabs(it) }
}
}
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
profileViewModel.filtered.collectLatest { onNewProfiles(it) }
}
}
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
profileViewModel.profiles.collectLatest {
val tabs = if (::tabs.isInitialized) tabs else linkViewModel.activeLinks()
val list = tabsList(tabs)
val index = tabsIndex(list)
profileViewModel.next(list[index].id)
}
}
}
intent?.data?.let { deepLink ->
val pathSegments = deepLink.pathSegments
if (pathSegments.isNotEmpty()) processLink(pathSegments[0])
}
}
override fun onResume() {
super.onResume()
lifecycleScope.launch {
if (settings.transparentProxy) transparentProxyHelper.install()
}
}
override fun onStart() {
super.onStart()
IntentFilter().also {
it.addAction(TProxyService.START_VPN_SERVICE_ACTION_NAME)
it.addAction(TProxyService.STOP_VPN_SERVICE_ACTION_NAME)
it.addAction(TProxyService.STATUS_VPN_SERVICE_ACTION_NAME)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(vpnServiceEventReceiver, it, RECEIVER_NOT_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(vpnServiceEventReceiver, it)
}
}
Intent(this, TProxyService::class.java).also {
it.action = TProxyService.STATUS_VPN_SERVICE_ACTION_NAME
startService(it)
}
if (settings.refreshLinksOnOpen) {
val interval = (settings.refreshLinksInterval * 60 * 1000).toLong()
val diff = System.currentTimeMillis() - settings.lastRefreshLinks
if (diff >= interval) refreshLinks()
}
}
override fun onStop() {
super.onStop()
unregisterReceiver(vpnServiceEventReceiver)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.refreshLinks -> refreshLinks()
R.id.newProfile -> startActivity(ProfileActivity.getIntent(applicationContext))
R.id.scanQrCode -> cameraPermission.launch(android.Manifest.permission.CAMERA)
R.id.fromClipboard -> {
runCatching {
clipboardManager.primaryClip!!.getItemAt(0).text.toString().trim()
}.getOrNull()?.let { processLink(it) }
}
}
return true
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.assets -> Intent(applicationContext, AssetsActivity::class.java)
R.id.links -> Intent(applicationContext, LinksActivity::class.java)
R.id.logs -> Intent(applicationContext, LogsActivity::class.java)
R.id.appsRouting -> Intent(applicationContext, AppsRoutingActivity::class.java)
R.id.configs -> Intent(applicationContext, ConfigsActivity::class.java)
R.id.settings -> Intent(applicationContext, SettingsActivity::class.java)
else -> null
}?.let { startActivity(it) }
binding.drawerLayout.closeDrawer(GravityCompat.START)
return true
}
private fun tabsList(list: List): List {
tabs = list
return listOf(Link(name = "All")) + tabs
}
private fun tabsIndex(list: List): Int {
return list.indexOfFirst { it.id == settings.selectedLink }.takeIf { it != -1 } ?: 0
}
private fun onNewTabs(value: List) {
binding.linksTab.removeOnTabSelectedListener(linksTabListener)
binding.linksTab.removeAllTabs()
binding.linksTab.isVisible = !value.isEmpty()
val list = tabsList(value)
val index = tabsIndex(list)
list.forEach {
val tab = binding.linksTab.newTab()
tab.tag = it.id
tab.text = it.name
binding.linksTab.addTab(tab)
}
binding.linksTab.selectTab(binding.linksTab.getTabAt(index))
binding.linksTab.addOnTabSelectedListener(linksTabListener)
}
private fun onNewProfiles(value: List) {
profiles.clear()
profiles.addAll(ArrayList(value))
@Suppress("NotifyDataSetChanged")
profileAdapter.notifyDataSetChanged()
}
private fun vpnStartStatus() {
isRunning = true
binding.toggleButton.text = getString(R.string.vpnStop)
binding.toggleButton.backgroundTintList = ColorStateList.valueOf(
ContextCompat.getColor(this, R.color.primaryColor)
)
binding.pingResult.text = getString(R.string.pingConnected)
}
private fun vpnStopStatus() {
isRunning = false
binding.toggleButton.text = getString(R.string.vpnStart)
binding.toggleButton.backgroundTintList = ColorStateList.valueOf(
ContextCompat.getColor(this, R.color.btnColor)
)
binding.pingResult.text = getString(R.string.pingNotConnected)
}
private fun onToggleButtonClick() {
if (!settings.tun2socks || settings.transparentProxy) {
toggleVpnService()
return
}
if (!hasPostNotification()) return
VpnService.prepare(this).also {
if (it == null) {
toggleVpnService()
return
}
vpnLauncher.launch(it)
}
}
private fun toggleVpnService() {
if (isRunning) {
TProxyService.stop(applicationContext)
return
}
TProxyService.start(applicationContext, false)
}
private fun profileSelect(index: Int, profile: ProfileList) {
val selectedProfile = settings.selectedProfile
lifecycleScope.launch {
val ref = if (selectedProfile > 0L) profileViewModel.find(selectedProfile) else null
withContext(Dispatchers.Main) {
if (selectedProfile == profile.id) return@withContext
settings.selectedProfile = profile.id
profileAdapter.notifyItemChanged(index)
if (isRunning) TProxyService.newConfig(applicationContext)
if (ref == null || ref.id == profile.id) return@withContext
profiles.indexOfFirst { it.id == ref.id }.let {
if (it != -1) profileAdapter.notifyItemChanged(it)
}
}
}
}
private fun profileEdit(profile: ProfileList) {
if (isRunning && settings.selectedProfile == profile.id) return
startActivity(ProfileActivity.getIntent(applicationContext, profile.id))
}
private fun profileDelete(profile: ProfileList) {
if (isRunning && settings.selectedProfile == profile.id) return
MaterialAlertDialogBuilder(this)
.setTitle("Delete Profile#${profile.index + 1} ?")
.setMessage("\"${profile.name}\" will delete forever !!")
.setNegativeButton("No", null)
.setPositiveButton("Yes") { _, _ ->
lifecycleScope.launch {
val ref = profileViewModel.find(profile.id)
val id = ref.id
profileViewModel.remove(ref)
withContext(Dispatchers.Main) {
val selectedProfile = settings.selectedProfile
if (selectedProfile == id) {
settings.selectedProfile = 0L
}
}
}
}.show()
}
private fun processLink(link: String) {
val uri = runCatching { URI(link) }.getOrNull() ?: return
if (uri.scheme == "http") {
Toast.makeText(
applicationContext, getString(R.string.forbiddenHttp), Toast.LENGTH_SHORT
).show()
return
}
if (uri.scheme == "https") {
openLink(uri)
return
}
val linkHelper = LinkHelper(settings, link)
if (!linkHelper.isValid()) {
Toast.makeText(
applicationContext, getString(R.string.invalidLink), Toast.LENGTH_SHORT
).show()
return
}
val json = linkHelper.json()
val name = linkHelper.remark()
startActivity(ProfileActivity.getIntent(applicationContext, name = name, config = json))
}
private fun refreshLinks() {
startActivity(LinksManagerActivity.refreshLinks(applicationContext))
}
private fun openLink(uri: URI) {
val link = Link()
link.name = LinkHelper.remark(uri, LinkHelper.LINK_DEFAULT)
link.address = uri.toString()
val intent = LinksManagerActivity.openLink(applicationContext, link)
linksManager.launch(intent)
}
private fun ping() {
if (!isRunning) return
binding.pingResult.text = getString(R.string.pingTesting)
HttpHelper(lifecycleScope, settings).measureDelay(!settings.transparentProxy) {
binding.pingResult.text = it
}
}
private fun hasPostNotification(): Boolean {
val sharedPref = getSharedPreferences("app", MODE_PRIVATE)
val key = "request_notification_permission"
val askedBefore = sharedPref.getBoolean(key, false)
if (askedBefore) return true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
sharedPref.edit { putBoolean(key, true) }
notificationPermission.launch(android.Manifest.permission.POST_NOTIFICATIONS)
return false
}
return true
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/ProfileActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import XrayCore.XrayCore
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.blacksquircle.ui.editorkit.plugin.autoindent.autoIndentation
import com.blacksquircle.ui.editorkit.plugin.base.PluginSupplier
import com.blacksquircle.ui.editorkit.plugin.delimiters.highlightDelimiters
import com.blacksquircle.ui.editorkit.plugin.linenumbers.lineNumbers
import com.blacksquircle.ui.language.json.JsonLanguage
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.database.Config
import io.github.saeeddev94.xray.database.Profile
import io.github.saeeddev94.xray.databinding.ActivityProfileBinding
import io.github.saeeddev94.xray.helper.ConfigHelper
import io.github.saeeddev94.xray.helper.FileHelper
import io.github.saeeddev94.xray.viewmodel.ConfigViewModel
import io.github.saeeddev94.xray.viewmodel.ProfileViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
class ProfileActivity : AppCompatActivity() {
companion object {
private const val PROFILE_ID = "id"
private const val PROFILE_NAME = "name"
private const val PROFILE_CONFIG = "config"
fun getIntent(
context: Context, id: Long = 0L, name: String = "", config: String = ""
) = Intent(context, ProfileActivity::class.java).also {
it.putExtra(PROFILE_ID, id)
if (name.isNotEmpty()) it.putExtra(PROFILE_NAME, name)
if (config.isNotEmpty()) it.putExtra(
PROFILE_CONFIG,
config.replace("\\/", "/")
)
}
}
private val settings by lazy { Settings(applicationContext) }
private val configViewModel: ConfigViewModel by viewModels()
private val profileViewModel: ProfileViewModel by viewModels()
private lateinit var binding: ActivityProfileBinding
private lateinit var config: Config
private lateinit var profile: Profile
private var id: Long = 0L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
id = intent.getLongExtra(PROFILE_ID, 0L)
title = if (isNew()) getString(R.string.newProfile) else getString(R.string.editProfile)
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
lifecycleScope.launch {
val config = configViewModel.get()
withContext(Dispatchers.Main) {
this@ProfileActivity.config = config
}
}
val jsonUri = intent.data
if (Intent.ACTION_VIEW == intent.action && jsonUri != null) {
val profile = Profile()
profile.config = readJsonFile(jsonUri)
resolved(profile)
} else if (isNew()) {
val profile = Profile()
profile.name = intent.getStringExtra(PROFILE_NAME) ?: ""
profile.config = intent.getStringExtra(PROFILE_CONFIG) ?: ""
resolved(profile)
} else {
lifecycleScope.launch {
val profile = profileViewModel.find(id)
withContext(Dispatchers.Main) {
resolved(profile)
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_profile, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.saveProfile -> save()
else -> finish()
}
return true
}
private fun isNew() = id == 0L
private fun readJsonFile(uri: Uri): String {
val content = StringBuilder()
try {
contentResolver.openInputStream(uri)?.use { input ->
BufferedReader(InputStreamReader(input)).forEachLine { content.append("$it\n") }
}
} catch (error: Exception) {
error.printStackTrace()
}
return content.toString()
}
private fun resolved(value: Profile) {
profile = value
binding.profileName.setText(profile.name)
val editor = binding.profileConfig
val pluginSupplier = PluginSupplier.create {
lineNumbers {
lineNumbers = true
highlightCurrentLine = true
}
highlightDelimiters()
autoIndentation {
autoIndentLines = true
autoCloseBrackets = true
autoCloseQuotes = true
}
}
editor.language = JsonLanguage()
editor.setTextContent(profile.config)
editor.plugins(pluginSupplier)
}
private fun save(check: Boolean = true) {
profile.name = binding.profileName.text.toString()
profile.config = binding.profileConfig.text.toString()
lifecycleScope.launch {
val configHelper = runCatching { ConfigHelper(settings, config, profile.config) }
val error = if (configHelper.isSuccess) {
isValid(configHelper.getOrNull().toString())
} else {
configHelper.exceptionOrNull()?.message ?: getString(R.string.invalidProfile)
}
if (check && error.isNotEmpty()) {
withContext(Dispatchers.Main) {
showError(error)
}
return@launch
}
if (profile.id == 0L) {
profileViewModel.create(profile)
} else {
profileViewModel.update(profile)
}
withContext(Dispatchers.Main) {
finish()
}
}
}
private suspend fun isValid(json: String): String {
return withContext(Dispatchers.IO) {
val pwd = filesDir.absolutePath
val testConfig = settings.testConfig()
FileHelper.createOrUpdate(testConfig, json)
XrayCore.test(pwd, testConfig.absolutePath)
}
}
private fun showError(message: String) {
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.invalidProfile))
.setMessage(message)
.setNegativeButton(getString(R.string.cancel)) { _, _ -> }
.setPositiveButton(getString(R.string.ignore)) { _, _ -> save(false) }
.show()
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/ScannerActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.budiyev.android.codescanner.AutoFocusMode
import com.budiyev.android.codescanner.CodeScanner
import com.budiyev.android.codescanner.CodeScannerView
import com.budiyev.android.codescanner.DecodeCallback
import com.budiyev.android.codescanner.ErrorCallback
import com.budiyev.android.codescanner.ScanMode
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.databinding.ActivityScannerBinding
class ScannerActivity : AppCompatActivity() {
private lateinit var binding: ActivityScannerBinding
private lateinit var codeScanner: CodeScanner
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityScannerBinding.inflate(layoutInflater)
setContentView(binding.root)
val scannerView = findViewById(R.id.scannerView)
codeScanner = CodeScanner(this, scannerView)
codeScanner.camera = CodeScanner.CAMERA_BACK
codeScanner.formats = CodeScanner.ALL_FORMATS
codeScanner.autoFocusMode = AutoFocusMode.SAFE
codeScanner.scanMode = ScanMode.SINGLE
codeScanner.isAutoFocusEnabled = true
codeScanner.isFlashEnabled = false
codeScanner.decodeCallback = DecodeCallback {
runOnUiThread {
val intent = Intent().also { intent -> intent.putExtra("link", it.text) }
setResult(RESULT_OK, intent)
finish()
}
}
codeScanner.errorCallback = ErrorCallback {
runOnUiThread {
Toast.makeText(
this, "Camera initialization error: ${it.message}",
Toast.LENGTH_LONG
).show()
}
}
scannerView.setOnClickListener { codeScanner.startPreview() }
}
override fun onPause() {
codeScanner.releaseResources()
super.onPause()
}
override fun onResume() {
super.onResume()
codeScanner.startPreview()
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/activity/SettingsActivity.kt
================================================
package io.github.saeeddev94.xray.activity
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.adapter.SettingAdapter
import io.github.saeeddev94.xray.databinding.ActivitySettingsBinding
import io.github.saeeddev94.xray.helper.TransparentProxyHelper
import io.github.saeeddev94.xray.service.TProxyService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SettingsActivity : AppCompatActivity() {
private val settings by lazy { Settings(applicationContext) }
private val transparentProxyHelper by lazy { TransparentProxyHelper(this, settings) }
private lateinit var binding: ActivitySettingsBinding
private lateinit var adapter: SettingAdapter
private lateinit var basic: View
private lateinit var advanced: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = getString(R.string.settings)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val tabs = listOf("Basic", "Advanced")
val layouts = listOf(R.layout.tab_basic_settings, R.layout.tab_advanced_settings)
adapter = SettingAdapter(this, tabs, layouts, object : SettingAdapter.ViewsReady {
override fun rendered(views: List) {
basic = views[0]
advanced = views[1]
setupBasic()
setupAdvanced()
}
})
binding.viewPager.adapter = adapter
binding.tabLayout.setupWithViewPager(binding.viewPager)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_settings, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.saveSettings -> applySettings()
else -> finish()
}
return true
}
@SuppressLint("SetTextI18n")
private fun setupBasic() {
basic.findViewById(R.id.socksAddress).setText(settings.socksAddress)
basic.findViewById(R.id.socksPort).setText(settings.socksPort)
basic.findViewById(R.id.socksUsername).setText(settings.socksUsername)
basic.findViewById(R.id.socksPassword).setText(settings.socksPassword)
basic.findViewById(R.id.geoIpAddress).setText(settings.geoIpAddress)
basic.findViewById(R.id.geoSiteAddress).setText(settings.geoSiteAddress)
basic.findViewById(R.id.pingAddress).setText(settings.pingAddress)
basic.findViewById(R.id.pingTimeout).setText(settings.pingTimeout.toString())
basic.findViewById(R.id.refreshLinksInterval)
.setText(settings.refreshLinksInterval.toString())
basic.findViewById(R.id.bypassLan).isChecked = settings.bypassLan
basic.findViewById(R.id.enableIpV6).isChecked = settings.enableIpV6
basic.findViewById(R.id.socksUdp).isChecked = settings.socksUdp
basic.findViewById(R.id.tun2socks).isChecked = settings.tun2socks
basic.findViewById(R.id.bootAutoStart).isChecked = settings.bootAutoStart
basic.findViewById(R.id.refreshLinksOnOpen).isChecked =
settings.refreshLinksOnOpen
}
@SuppressLint("SetTextI18n")
private fun setupAdvanced() {
advanced.findViewById(R.id.primaryDns).setText(settings.primaryDns)
advanced.findViewById(R.id.secondaryDns).setText(settings.secondaryDns)
advanced.findViewById(R.id.primaryDnsV6).setText(settings.primaryDnsV6)
advanced.findViewById(R.id.secondaryDnsV6).setText(settings.secondaryDnsV6)
advanced.findViewById(R.id.tunName).setText(settings.tunName)
advanced.findViewById(R.id.tunMtu).setText(settings.tunMtu.toString())
advanced.findViewById(R.id.tunAddress).setText(settings.tunAddress)
advanced.findViewById(R.id.tunPrefix).setText(settings.tunPrefix.toString())
advanced.findViewById(R.id.tunAddressV6).setText(settings.tunAddressV6)
advanced.findViewById(R.id.tunPrefixV6).setText(settings.tunPrefixV6.toString())
advanced.findViewById(R.id.hotspotInterface).setText(settings.hotspotInterface)
advanced.findViewById(R.id.tetheringInterface).setText(settings.tetheringInterface)
advanced.findViewById(R.id.tproxyAddress).setText(settings.tproxyAddress)
advanced.findViewById(R.id.tproxyPort).setText(settings.tproxyPort.toString())
advanced.findViewById(R.id.tproxyBypassWiFi).setText(settings.tproxyBypassWiFi.joinToString(", "))
advanced.findViewById(R.id.tproxyAutoConnect).isChecked =
settings.tproxyAutoConnect
advanced.findViewById(R.id.tproxyHotspot).isChecked =
settings.tproxyHotspot
advanced.findViewById(R.id.tproxyTethering).isChecked =
settings.tproxyTethering
advanced.findViewById(R.id.transparentProxy).isChecked =
settings.transparentProxy
advanced.findViewById(R.id.tunRoutes).setOnClickListener { tunRoutes() }
}
private fun tunRoutes() {
val tunRoutes = resources.getStringArray(R.array.publicIpAddresses).toSet()
val layout = layoutInflater.inflate(R.layout.layout_tun_routes, null)
val editText = layout.findViewById(R.id.tunRoutesEditText)
val getValue = {
editText.text.toString().lines().map { it.trim() }.filter { it.isNotBlank() }.toSet()
}
editText.setText(settings.tunRoutes.joinToString("\n"))
MaterialAlertDialogBuilder(this)
.setTitle(R.string.tunRoutes)
.setView(layout)
.setPositiveButton("Save") { _, _ -> settings.tunRoutes = getValue() }
.setNeutralButton("Reset") { _, _ -> settings.tunRoutes = tunRoutes }
.setNegativeButton("Close", null)
.show()
}
private fun applySettings() {
val transparentProxy = advanced.findViewById(R.id.transparentProxy).isChecked
if (transparentProxy && !settings.xrayCoreFile().exists()) {
Toast.makeText(
applicationContext, "Install the assets", Toast.LENGTH_SHORT
).show()
return
}
saveSettings()
}
private fun saveSettings() {
/** Basic */
settings.socksAddress = basic.findViewById(R.id.socksAddress).text.toString()
settings.socksPort = basic.findViewById(R.id.socksPort).text.toString()
settings.socksUsername = basic.findViewById(R.id.socksUsername).text.toString()
settings.socksPassword = basic.findViewById(R.id.socksPassword).text.toString()
settings.geoIpAddress = basic.findViewById(R.id.geoIpAddress).text.toString()
settings.geoSiteAddress = basic.findViewById(R.id.geoSiteAddress).text.toString()
settings.pingAddress = basic.findViewById(R.id.pingAddress).text.toString()
settings.pingTimeout = basic.findViewById(R.id.pingTimeout).text.toString().toInt()
settings.refreshLinksInterval =
basic.findViewById(R.id.refreshLinksInterval).text.toString().toInt()
settings.bypassLan = basic.findViewById(R.id.bypassLan).isChecked
val enableIpV6 = basic.findViewById(R.id.enableIpV6).isChecked
settings.socksUdp = basic.findViewById(R.id.socksUdp).isChecked
settings.tun2socks = basic.findViewById(R.id.tun2socks).isChecked
settings.bootAutoStart = basic.findViewById(R.id.bootAutoStart).isChecked
settings.refreshLinksOnOpen =
basic.findViewById(R.id.refreshLinksOnOpen).isChecked
/** Advanced */
settings.primaryDns = advanced.findViewById(R.id.primaryDns).text.toString()
settings.secondaryDns = advanced.findViewById(R.id.secondaryDns).text.toString()
settings.primaryDnsV6 = advanced.findViewById(R.id.primaryDnsV6).text.toString()
settings.secondaryDnsV6 = advanced.findViewById(R.id.secondaryDnsV6).text.toString()
settings.tunName = advanced.findViewById(R.id.tunName).text.toString()
settings.tunMtu = advanced.findViewById(R.id.tunMtu).text.toString().toInt()
settings.tunAddress = advanced.findViewById(R.id.tunAddress).text.toString()
settings.tunPrefix = advanced.findViewById(R.id.tunPrefix).text.toString().toInt()
settings.tunAddressV6 = advanced.findViewById(R.id.tunAddressV6).text.toString()
settings.tunPrefixV6 = advanced.findViewById(R.id.tunPrefixV6).text.toString().toInt()
val hotspotInterface = advanced.findViewById(R.id.hotspotInterface).text.toString()
val tetheringInterface = advanced.findViewById(R.id.tetheringInterface).text.toString()
val tproxyAddress = advanced.findViewById(R.id.tproxyAddress).text.toString()
val tproxyPort = advanced.findViewById(R.id.tproxyPort).text.toString().toInt()
val tproxyBypassWiFi = advanced.findViewById(R.id.tproxyBypassWiFi).text
.toString()
.split(",")
.map { it.trim() }
.filter { it.isNotBlank() }
.toSet()
val tproxyAutoConnect = advanced.findViewById(R.id.tproxyAutoConnect).isChecked
val tproxyHotspot = advanced.findViewById(R.id.tproxyHotspot).isChecked
val tproxyTethering = advanced.findViewById(R.id.tproxyTethering).isChecked
val transparentProxy = advanced.findViewById(R.id.transparentProxy).isChecked
lifecycleScope.launch {
val tproxySettingsChanged = settings.enableIpV6 != enableIpV6 ||
settings.hotspotInterface != hotspotInterface ||
settings.tetheringInterface != tetheringInterface ||
settings.tproxyAddress != tproxyAddress ||
settings.tproxyPort != tproxyPort ||
settings.tproxyBypassWiFi != tproxyBypassWiFi ||
settings.tproxyAutoConnect != tproxyAutoConnect ||
settings.tproxyHotspot != tproxyHotspot ||
settings.tproxyTethering != tproxyTethering ||
settings.transparentProxy != transparentProxy
val stopService = tproxySettingsChanged && settings.xrayCorePid().exists()
if (tproxySettingsChanged) transparentProxyHelper.kill()
withContext(Dispatchers.Main) {
settings.enableIpV6 = enableIpV6
settings.hotspotInterface = hotspotInterface
settings.tetheringInterface = tetheringInterface
settings.tproxyAddress = tproxyAddress
settings.tproxyPort = tproxyPort
settings.tproxyBypassWiFi = tproxyBypassWiFi
settings.tproxyAutoConnect = tproxyAutoConnect
settings.tproxyHotspot = tproxyHotspot
settings.tproxyTethering = tproxyTethering
settings.transparentProxy = transparentProxy
if (stopService) TProxyService.stop(this@SettingsActivity)
finish()
}
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/adapter/AppsRoutingAdapter.kt
================================================
package io.github.saeeddev94.xray.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.checkbox.MaterialCheckBox
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.dto.AppList
class AppsRoutingAdapter(
private var context: Context,
private var apps: MutableList,
private var appsRouting: MutableSet,
) : RecyclerView.Adapter() {
override fun onCreateViewHolder(container: ViewGroup, type: Int): ViewHolder {
val linearLayout = LinearLayout(context)
val item: View = LayoutInflater.from(context).inflate(
R.layout.item_recycler_exclude, linearLayout, false
)
return ViewHolder(item)
}
override fun getItemCount(): Int {
return apps.size
}
override fun onBindViewHolder(holder: ViewHolder, index: Int) {
val app = apps[index]
val isSelected = appsRouting.contains(app.packageName)
holder.appIcon.setImageDrawable(app.appIcon)
holder.appName.text = app.appName
holder.packageName.text = app.packageName
holder.isSelected.isChecked = isSelected
holder.appContainer.setOnClickListener {
if (isSelected) {
appsRouting.remove(app.packageName)
} else {
appsRouting.add(app.packageName)
}
notifyItemChanged(index)
}
}
class ViewHolder(item: View) : RecyclerView.ViewHolder(item) {
var appContainer: LinearLayout = item.findViewById(R.id.appContainer)
var appIcon: ImageView = item.findViewById(R.id.appIcon)
var appName: TextView = item.findViewById(R.id.appName)
var packageName: TextView = item.findViewById(R.id.packageName)
var isSelected: MaterialCheckBox = item.findViewById(R.id.isSelected)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/adapter/ConfigAdapter.kt
================================================
package io.github.saeeddev94.xray.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.viewpager.widget.PagerAdapter
import io.github.saeeddev94.xray.R
class ConfigAdapter(
private var context: Context,
private var tabs: List,
private var setup: (tab: String, view: View) -> Unit,
) : PagerAdapter() {
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = LayoutInflater.from(context).inflate(R.layout.tab_config, container, false)
container.addView(view)
setup(tabs[position], view)
return view
}
override fun getCount(): Int {
return tabs.size
}
override fun getPageTitle(position: Int): CharSequence {
return tabs[position]
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/adapter/LinkAdapter.kt
================================================
package io.github.saeeddev94.xray.adapter
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.database.Link
class LinkAdapter : ListAdapter(diffCallback) {
var onEditClick: (link: Link) -> Unit = {}
var onDeleteClick: (link: Link) -> Unit = {}
override fun onCreateViewHolder(parent: ViewGroup, type: Int) = LinkHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.layout_link_item, parent, false
)
)
override fun onBindViewHolder(holder: LinkHolder, position: Int) {
holder.bind(position)
}
inner class LinkHolder(view: View) : RecyclerView.ViewHolder(view) {
private val card = view.findViewById(R.id.linkCard)
private val name = view.findViewById(R.id.linkName)
private val type = view.findViewById(R.id.linkType)
private val edit = view.findViewById(R.id.linkEdit)
private val delete = view.findViewById(R.id.linkDelete)
fun bind(index: Int) {
val link = getItem(index)
val color = if (link.isActive) R.color.btnColor else R.color.btnColorDisabled
card.backgroundTintList = ColorStateList.valueOf(
ContextCompat.getColor(card.context, color)
)
name.text = link.name
type.text = link.type.name
edit.setOnClickListener { onEditClick(link) }
delete.setOnClickListener { onDeleteClick(link) }
}
}
companion object {
private val diffCallback = object : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: Link, newItem: Link): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Link, newItem: Link): Boolean =
oldItem == newItem
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/adapter/ProfileAdapter.kt
================================================
package io.github.saeeddev94.xray.adapter
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.dto.ProfileList
import io.github.saeeddev94.xray.helper.ProfileTouchHelper
import io.github.saeeddev94.xray.viewmodel.ProfileViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class ProfileAdapter(
private val scope: CoroutineScope,
private val settings: Settings,
private val profileViewModel: ProfileViewModel,
private val profiles: ArrayList,
private val profileSelect: (index: Int, profile: ProfileList) -> Unit,
private val profileEdit: (profile: ProfileList) -> Unit,
private val profileDelete: (profile: ProfileList) -> Unit,
) : RecyclerView.Adapter(), ProfileTouchHelper.ProfileTouchCallback {
override fun onCreateViewHolder(container: ViewGroup, type: Int): ViewHolder {
val linearLayout = LinearLayout(container.context)
val item: View = LayoutInflater.from(container.context).inflate(
R.layout.item_recycler_main, linearLayout, false
)
return ViewHolder(item)
}
override fun getItemCount(): Int {
return profiles.size
}
override fun onBindViewHolder(holder: ViewHolder, index: Int) {
val profile = profiles[index]
val color =
if (settings.selectedProfile == profile.id) R.color.primaryColor else R.color.btnColor
holder.activeIndicator.backgroundTintList = ColorStateList.valueOf(
ContextCompat.getColor(holder.profileCard.context, color)
)
holder.profileName.text = profile.name
holder.profileCard.setOnClickListener {
profileSelect(index, profile)
}
holder.profileEdit.setOnClickListener {
profileEdit(profile)
}
holder.profileDelete.setOnClickListener {
profileDelete(profile)
}
}
override fun onItemMoved(fromPosition: Int, toPosition: Int): Boolean {
profiles.add(toPosition, profiles.removeAt(fromPosition))
notifyItemMoved(fromPosition, toPosition)
if (toPosition > fromPosition) {
notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1)
} else {
notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1)
}
return true
}
override fun onItemMoveCompleted(startPosition: Int, endPosition: Int) {
val isMoveUp = startPosition > endPosition
val start = if (isMoveUp) profiles[endPosition + 1] else profiles[endPosition - 1]
val end = profiles[endPosition]
scope.launch {
if (isMoveUp) profileViewModel.moveUp(start.index, end.index, end.id)
else profileViewModel.moveDown(start.index, end.index, end.id)
}
}
class ViewHolder(item: View) : RecyclerView.ViewHolder(item) {
var activeIndicator: LinearLayout = item.findViewById(R.id.activeIndicator)
var profileCard: CardView = item.findViewById(R.id.profileCard)
var profileName: TextView = item.findViewById(R.id.profileName)
var profileEdit: LinearLayout = item.findViewById(R.id.profileEdit)
var profileDelete: LinearLayout = item.findViewById(R.id.profileDelete)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/adapter/SettingAdapter.kt
================================================
package io.github.saeeddev94.xray.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.viewpager.widget.PagerAdapter
class SettingAdapter(
private var context: Context,
private var tabs: List,
private var layouts: List,
private var callback: ViewsReady,
) : PagerAdapter() {
private val views: MutableList = mutableListOf()
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val layoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = layoutInflater.inflate(layouts[position], container, false)
container.addView(view)
views.add(view)
if (views.size == tabs.size) {
callback.rendered(views)
}
return view
}
override fun getCount(): Int {
return tabs.size
}
override fun getPageTitle(position: Int): CharSequence {
return tabs[position]
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
interface ViewsReady {
fun rendered(views: List)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/component/EmptySubmitSearchView.kt
================================================
package io.github.saeeddev94.xray.component
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.SearchView
class EmptySubmitSearchView : SearchView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
@SuppressLint("RestrictedApi")
override fun setOnQueryTextListener(listener: OnQueryTextListener?) {
super.setOnQueryTextListener(listener)
val searchAutoComplete =
this.findViewById(androidx.appcompat.R.id.search_src_text)
searchAutoComplete.setOnEditorActionListener { _, _, _ ->
listener?.onQueryTextSubmit(query.toString())
return@setOnEditorActionListener true
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/database/Config.kt
================================================
package io.github.saeeddev94.xray.database
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "configs")
data class Config(
@PrimaryKey
@ColumnInfo(name = "id")
var id: Int = 1,
@ColumnInfo(name = "log")
var log: String = "{}",
@ColumnInfo(name = "dns")
var dns: String = "{}",
@ColumnInfo(name = "inbounds")
var inbounds: String = "[]",
@ColumnInfo(name = "outbounds")
var outbounds: String = "[]",
@ColumnInfo(name = "routing")
var routing: String = "{}",
@ColumnInfo(name = "log_mode")
var logMode: Mode = Mode.Disable,
@ColumnInfo(name = "dns_mode")
var dnsMode: Mode = Mode.Disable,
@ColumnInfo(name = "inbounds_mode")
var inboundsMode: Mode = Mode.Disable,
@ColumnInfo(name = "outbounds_mode")
var outboundsMode: Mode = Mode.Disable,
@ColumnInfo(name = "routing_mode")
var routingMode: Mode = Mode.Disable,
) : Parcelable {
enum class Mode(val value: Int) {
Disable(0),
Replace(1),
Merge(2);
class Convertor {
@TypeConverter
fun fromMode(mode: Mode): Int = mode.value
@TypeConverter
fun toMode(value: Int): Mode = entries[value]
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/database/ConfigDao.kt
================================================
package io.github.saeeddev94.xray.database
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
@Dao
interface ConfigDao {
@Query("SELECT * FROM configs WHERE id = 1")
suspend fun get(): Config?
@Insert
suspend fun insert(config: Config)
@Update
suspend fun update(config: Config)
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/database/Link.kt
================================================
package io.github.saeeddev94.xray.database
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "links")
data class Link(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id: Long = 0L,
@ColumnInfo(name = "name")
var name: String = "",
@ColumnInfo(name = "address")
var address: String = "",
@ColumnInfo(name = "type")
var type: Type = Type.Json,
@ColumnInfo(name = "is_active")
var isActive: Boolean = false,
@ColumnInfo(name = "user_agent")
var userAgent: String? = null,
) : Parcelable {
enum class Type(val value: Int) {
Json(0),
Subscription(1);
class Convertor {
@TypeConverter
fun fromType(type: Type): Int = type.value
@TypeConverter
fun toType(value: Int): Type = entries[value]
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/database/LinkDao.kt
================================================
package io.github.saeeddev94.xray.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface LinkDao {
@Query("SELECT * FROM links ORDER BY id ASC")
fun all(): Flow>
@Query("SELECT * FROM links WHERE is_active = 1 ORDER BY id ASC")
fun tabs(): Flow>
@Query("SELECT * FROM links WHERE is_active = 1 ORDER BY id ASC")
suspend fun activeLinks(): List
@Insert
suspend fun insert(link: Link): Long
@Update
suspend fun update(link: Link)
@Delete
suspend fun delete(link: Link)
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/database/Profile.kt
================================================
package io.github.saeeddev94.xray.database
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(
tableName = "profiles",
foreignKeys = [
ForeignKey(
entity = Link::class,
parentColumns = ["id"],
childColumns = ["link_id"],
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE,
),
],
indices = [
Index(
name = "profiles_link_id_foreign",
value = ["link_id"]
),
],
)
data class Profile(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id: Long = 0L,
@ColumnInfo(name = "link_id")
var linkId: Long? = null,
@ColumnInfo(name = "index")
var index: Int = -1,
@ColumnInfo(name = "name")
var name: String = "",
@ColumnInfo(name = "config")
var config: String = "",
) : Parcelable
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/database/ProfileDao.kt
================================================
package io.github.saeeddev94.xray.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.github.saeeddev94.xray.dto.ProfileList
import kotlinx.coroutines.flow.Flow
@Dao
interface ProfileDao {
@Query(
"SELECT `profiles`.`id`, `profiles`.`index`, `profiles`.`name`, `profiles`.`link_id` AS `link`" +
" FROM `profiles`" +
" LEFT JOIN `links` ON `profiles`.`link_id` = `links`.`id`" +
" WHERE `links`.`is_active` IS NULL OR `links`.`is_active` = 1" +
" ORDER BY `profiles`.`index` ASC"
)
fun all(): Flow>
@Query("SELECT * FROM profiles WHERE link_id = :linkId ORDER BY `index` DESC")
suspend fun linkProfiles(linkId: Long): List
@Query("SELECT * FROM profiles WHERE `id` = :id")
suspend fun find(id: Long): Profile
@Insert
suspend fun insert(profile: Profile): Long
@Update
suspend fun update(profile: Profile)
@Delete
suspend fun delete(profile: Profile)
@Query("UPDATE profiles SET `index` = :index WHERE `id` = :id")
suspend fun updateIndex(index: Int, id: Long)
@Query("UPDATE profiles SET `index` = `index` + 1")
suspend fun fixInsertIndex()
@Query("UPDATE profiles SET `index` = `index` - 1 WHERE `index` > :index")
suspend fun fixDeleteIndex(index: Int)
@Query(
"UPDATE profiles" +
" SET `index` = `index` + 1" +
" WHERE `index` >= :start" +
" AND `index` < :end" +
" AND `id` NOT IN (:exclude)"
)
suspend fun fixMoveUpIndex(start: Int, end: Int, exclude: Long)
@Query(
"UPDATE profiles" +
" SET `index` = `index` - 1" +
" WHERE `index` > :start" +
" AND `index` <= :end" +
" AND `id` NOT IN (:exclude)"
)
suspend fun fixMoveDownIndex(start: Int, end: Int, exclude: Long)
@Transaction
suspend fun create(profile: Profile) {
insert(profile)
fixInsertIndex()
}
@Transaction
suspend fun remove(profile: Profile) {
delete(profile)
fixDeleteIndex(profile.index)
}
@Transaction
suspend fun moveUp(start: Int, end: Int, exclude: Long) {
updateIndex(start, exclude)
fixMoveUpIndex(start, end, exclude)
}
@Transaction
suspend fun moveDown(start: Int, end: Int, exclude: Long) {
updateIndex(start, exclude)
fixMoveDownIndex(end, start, exclude)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/database/XrayDatabase.kt
================================================
package io.github.saeeddev94.xray.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@Database(
entities = [
Config::class,
Link::class,
Profile::class,
],
version = 4,
exportSchema = false,
)
@TypeConverters(
Link.Type.Convertor::class,
Config.Mode.Convertor::class,
)
abstract class XrayDatabase : RoomDatabase() {
abstract fun configDao(): ConfigDao
abstract fun linkDao(): LinkDao
abstract fun profileDao(): ProfileDao
companion object {
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// create links table
db.execSQL("""
CREATE TABLE links (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
address TEXT NOT NULL,
type INTEGER NOT NULL,
is_active INTEGER NOT NULL
)
""")
// add link_id to profiles table
db.execSQL("ALTER TABLE profiles ADD COLUMN link_id INTEGER")
// create profiles_new table similar to profiles but with new column (link_id)
db.execSQL("""
CREATE TABLE profiles_new (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
link_id INTEGER,
"index" INTEGER NOT NULL,
name TEXT NOT NULL,
config TEXT NOT NULL,
FOREIGN KEY (link_id) REFERENCES links(id) ON UPDATE CASCADE ON DELETE CASCADE
)
""")
// create index for link_id
db.execSQL("CREATE INDEX profiles_link_id_foreign ON profiles_new(link_id)")
// importing data from profile to profiles_new
db.execSQL("""
INSERT INTO profiles_new (id, link_id, "index", name, config)
SELECT id, link_id, "index", name, config FROM profiles
""")
// drop profiles table
db.execSQL("DROP TABLE profiles")
// rename profiles_new to profiles
db.execSQL("ALTER TABLE profiles_new RENAME TO profiles")
}
}
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE links ADD COLUMN user_agent TEXT")
}
}
private val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
// create config table
db.execSQL("""
CREATE TABLE configs (
id INTEGER PRIMARY KEY NOT NULL,
log TEXT NOT NULL,
dns TEXT NOT NULL,
inbounds TEXT NOT NULL,
outbounds TEXT NOT NULL,
routing TEXT NOT NULL,
log_mode INTEGER NOT NULL,
dns_mode INTEGER NOT NULL,
inbounds_mode INTEGER NOT NULL,
outbounds_mode INTEGER NOT NULL,
routing_mode INTEGER NOT NULL
)
""")
}
}
@Volatile
private var db: XrayDatabase? = null
fun ref(context: Context): XrayDatabase {
if (db == null) {
synchronized(this) {
if (db == null) {
val migrations = arrayOf(
MIGRATION_1_2,
MIGRATION_2_3,
MIGRATION_3_4,
)
db = Room.databaseBuilder(
context.applicationContext,
XrayDatabase::class.java,
"xray"
).addMigrations(*migrations).build()
}
}
}
return db!!
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/dto/AppList.kt
================================================
package io.github.saeeddev94.xray.dto
import android.graphics.drawable.Drawable
data class AppList(
var appIcon: Drawable,
var appName: String,
var packageName: String,
)
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/dto/ProfileList.kt
================================================
package io.github.saeeddev94.xray.dto
data class ProfileList(
var id: Long,
var index: Int,
var name: String,
var link: Long?,
)
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/dto/XrayConfig.kt
================================================
package io.github.saeeddev94.xray.dto
data class XrayConfig(
val dir: String,
val file: String,
)
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/fragment/LinkFormFragment.kt
================================================
package io.github.saeeddev94.xray.fragment
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.radiobutton.MaterialRadioButton
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.database.Link
import java.net.URI
import kotlin.reflect.cast
class LinkFormFragment(
private val link: Link,
private val onConfirm: () -> Unit,
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return openLink(requireActivity())
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
requireActivity().finish()
}
private fun openLink(context: FragmentActivity): Dialog =
MaterialAlertDialogBuilder(context).apply {
val layout = context.layoutInflater.inflate(
R.layout.layout_link_form,
LinearLayout(context)
)
val typeRadioGroup = layout.findViewById(R.id.typeRadioGroup)
val nameEditText = layout.findViewById(R.id.nameEditText)
val addressEditText = layout.findViewById(R.id.addressEditText)
val userAgentEditText = layout.findViewById(R.id.userAgentEditText)
val isActiveSwitch = layout.findViewById(R.id.isActiveSwitch)
Link.Type.entries.forEach {
val radio = MaterialRadioButton(context)
radio.text = it.name
radio.tag = it
typeRadioGroup.addView(radio)
if (it == link.type) typeRadioGroup.check(radio.id)
}
nameEditText.setText(link.name)
addressEditText.setText(link.address)
userAgentEditText.setText(link.userAgent)
isActiveSwitch.isChecked = if (link.id == 0L) {
true
} else {
link.isActive
}
setView(layout)
setTitle(
if (link.id == 0L) context.getString(R.string.newLink)
else context.getString(R.string.editLink)
)
setPositiveButton(
if (link.id == 0L) context.getString(R.string.createLink)
else context.getString(R.string.updateLink)
) { _, _ ->
val address = addressEditText.text.toString()
val typeRadioButton = typeRadioGroup.findViewById(
typeRadioGroup.checkedRadioButtonId
)
val uri = runCatching { URI(address) }.getOrNull()
val invalidLink = context.getString(R.string.invalidLink)
val onlyHttps = context.getString(R.string.onlyHttps)
if (uri == null) {
Toast.makeText(context, invalidLink, Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
if (uri.scheme != "https") {
Toast.makeText(context, onlyHttps, Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
link.type = Link.Type::class.cast(typeRadioButton.tag)
link.name = nameEditText.text.toString()
link.address = address
link.userAgent = userAgentEditText.text.toString().ifBlank { null }
link.isActive = isActiveSwitch.isChecked
onConfirm()
}
setNegativeButton(context.getString(R.string.closeLink)) { _, _ -> }
}.create()
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/ConfigHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.database.Config
import org.json.JSONObject
class ConfigHelper(
settings: Settings,
config: Config,
base: String,
) {
private val base: JSONObject = JsonHelper.makeObject(base)
init {
process("log", config.log, config.logMode)
process("dns", config.dns, config.dnsMode)
process("inbounds", config.inbounds, config.inboundsMode)
process("outbounds", config.outbounds, config.outboundsMode)
process("routing", config.routing, config.routingMode)
if (settings.tproxyHotspot || settings.tproxyTethering) sharedInbounds()
}
override fun toString(): String {
return base.toString(4)
}
private fun process(key: String, config: String, mode: Config.Mode) {
if (mode == Config.Mode.Disable) return
if (arrayOf("inbounds", "outbounds").contains(key)) {
processArray(key, config, mode)
return
}
processObject(key, config, mode)
}
private fun processObject(key: String, config: String, mode: Config.Mode) {
val oldValue = JsonHelper.getObject(base, key)
val newValue = JsonHelper.makeObject(config)
val final = if (mode == Config.Mode.Replace) newValue
else JsonHelper.mergeObjects(oldValue, newValue)
base.put(key, final)
}
private fun processArray(key: String, config: String, mode: Config.Mode) {
val oldValue = JsonHelper.getArray(base, key)
val newValue = JsonHelper.makeArray(config)
val final = if (mode == Config.Mode.Replace) newValue
else JsonHelper.mergeArrays(oldValue, newValue, "protocol")
base.put(key, final)
}
private fun sharedInbounds() {
val key = "inbounds"
val inbounds = JsonHelper.getArray(base, key)
for (i in 0 until inbounds.length()) {
val inbound = inbounds[i]
if (inbound is JSONObject && inbound.has("listen")) {
inbound.remove("listen")
inbounds.put(i, inbound)
}
}
base.put(key, inbounds)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/DownloadHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.HttpURLConnection
import java.net.URL
class DownloadHelper(
private val scope: CoroutineScope,
private val url: String,
private val file: File,
private val callback: DownloadListener,
) {
fun start() {
scope.launch(Dispatchers.IO) {
var input: InputStream? = null
var output: OutputStream? = null
var connection: HttpURLConnection? = null
try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Expected HTTP ${HttpURLConnection.HTTP_OK} but received HTTP ${connection.responseCode}")
}
input = connection.inputStream
output = FileOutputStream(file)
val fileLength = connection.contentLength
val data = ByteArray(4096)
var total: Long = 0
var count: Int
while (input.read(data).also { count = it } != -1) {
total += count.toLong()
if (fileLength > 0) {
val progress = (total * 100 / fileLength).toInt()
withContext(Dispatchers.Main) {
callback.onProgress(progress)
}
}
output.write(data, 0, count)
}
withContext(Dispatchers.Main) {
callback.onComplete()
}
} catch (exception: Exception) {
withContext(Dispatchers.Main) {
callback.onError(exception)
}
} finally {
try {
output?.close()
input?.close()
} catch (_: IOException) {
}
connection?.disconnect()
}
}
}
interface DownloadListener {
fun onProgress(progress: Int)
fun onError(exception: Exception)
fun onComplete()
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/FileHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import java.io.File
class FileHelper {
companion object {
fun createOrUpdate(file: File, content: String) {
val fileContent = if (file.exists()) file.bufferedReader().use { it.readText() } else ""
if (content != fileContent) file.writeText(content)
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/HttpHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import io.github.saeeddev94.xray.BuildConfig
import io.github.saeeddev94.xray.Settings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.Authenticator
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.PasswordAuthentication
import java.net.Proxy
import java.net.URL
class HttpHelper(
val scope: CoroutineScope,
val settings: Settings,
) {
companion object {
private fun getConnection(
link: String,
method: String = "GET",
proxy: Proxy? = null,
timeout: Int = 5000,
userAgent: String? = null,
): HttpURLConnection {
val url = URL(link)
val connection = if (proxy == null) {
url.openConnection() as HttpURLConnection
} else {
url.openConnection(proxy) as HttpURLConnection
}
connection.requestMethod = method
connection.connectTimeout = timeout
connection.readTimeout = timeout
userAgent?.let { connection.setRequestProperty("User-Agent", it) }
connection.setRequestProperty("Connection", "close")
return connection
}
suspend fun get(link: String, userAgent: String? = null): String {
return withContext(Dispatchers.IO) {
val defaultUserAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME}"
val connection = getConnection(link, userAgent = userAgent ?: defaultUserAgent)
var responseCode = 0
val responseBody = try {
connection.connect()
responseCode = connection.responseCode
connection.inputStream.bufferedReader().use { it.readText() }
} catch (_: Exception) {
null
} finally {
connection.disconnect()
}
if (responseCode != HttpURLConnection.HTTP_OK || responseBody == null) {
throw Exception("HTTP Error: $responseCode")
}
responseBody
}
}
}
fun measureDelay(proxy: Boolean, callback: (result: String) -> Unit) {
scope.launch(Dispatchers.IO) {
val start = System.currentTimeMillis()
val connection = getConnection(proxy)
var result = "HTTP {status}, {delay} ms"
result = try {
setSocksAuth(getSocksAuth())
val responseCode = connection.responseCode
result.replace("{status}", "$responseCode")
} catch (error: Exception) {
error.message ?: "Http delay measure failed"
} finally {
connection.disconnect()
setSocksAuth(null)
}
val delay = System.currentTimeMillis() - start
withContext(Dispatchers.Main) {
callback(result.replace("{delay}", "$delay"))
}
}
}
private suspend fun getConnection(withProxy: Boolean): HttpURLConnection {
return withContext(Dispatchers.IO) {
val link = settings.pingAddress
val method = "HEAD"
val address = InetSocketAddress(settings.socksAddress, settings.socksPort.toInt())
val proxy = if (withProxy) Proxy(Proxy.Type.SOCKS, address) else null
val timeout = settings.pingTimeout * 1000
getConnection(link, method, proxy, timeout)
}
}
private fun getSocksAuth(): Authenticator? {
if (
settings.socksUsername.trim().isEmpty() || settings.socksPassword.trim().isEmpty()
) return null
return object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
return PasswordAuthentication(
settings.socksUsername,
settings.socksPassword.toCharArray()
)
}
}
}
private fun setSocksAuth(auth: Authenticator?) {
Authenticator.setDefault(auth)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/IntentHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import android.content.Intent
import android.os.Build
class IntentHelper {
companion object {
fun getParcelable(intent: Intent, name: String, clazz: Class): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(name, clazz)
} else {
@Suppress("deprecation")
intent.getParcelableExtra(name)
}
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/JsonHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import org.json.JSONArray
import org.json.JSONObject
class JsonHelper {
companion object {
fun makeObject(value: String) = JSONObject(value)
fun makeArray(value: String) = JSONArray(value)
fun getObject(value: JSONObject, key: String) =
value.optJSONObject(key) ?: JSONObject()
fun getArray(value: JSONObject, key: String) =
value.optJSONArray(key) ?: JSONArray()
fun mergeObjects(obj1: JSONObject, obj2: JSONObject): JSONObject {
val result = JSONObject(obj1.toString())
for (key in obj2.keys()) {
val value2 = obj2[key]
if (result.has(key)) {
val value1 = result[key]
when {
value1 is JSONObject && value2 is JSONObject -> {
result.put(key, mergeObjects(value1, value2))
}
value1 is JSONArray && value2 is JSONArray -> {
result.put(key, mergeArrays(value1, value2))
}
else -> result.put(key, value2)
}
} else result.put(key, value2)
}
return result
}
fun mergeArrays(arr1: JSONArray, arr2: JSONArray, mergeKey: String = ""): JSONArray {
val result = JSONArray()
for (i in 0 until arr1.length()) result.put(arr1[i])
for (i in 0 until arr2.length()) {
val value2 = arr2[i]
if (value2 is JSONObject && value2.has(mergeKey)) {
val keyValue = value2[mergeKey]
var merged = false
for (j in 0 until result.length()) {
val value1 = result[j]
if (value1 is JSONObject && value1.has(mergeKey) && value1[mergeKey] == keyValue) {
result.put(j, mergeObjects(value1, value2))
merged = true
break
}
}
if (!merged) result.put(value2)
} else result.put(value2)
}
return result
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/LinkHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import XrayCore.XrayCore
import android.util.Base64
import io.github.saeeddev94.xray.Settings
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.net.URI
class LinkHelper(
private val settings: Settings,
link: String
) {
private val success: Boolean
private val outbound: JSONObject?
private var remark: String = REMARK_DEFAULT
init {
val base64: String = XrayCore.json(link)
val decoded = tryDecodeBase64(base64)
val response = try {
JSONObject(decoded)
} catch (_: JSONException) {
JSONObject()
}
val data = response.optJSONObject("data") ?: JSONObject()
val outbounds = data.optJSONArray("outbounds") ?: JSONArray()
success = response.optBoolean("success", false)
outbound = if (outbounds.length() > 0) outbounds[0] as JSONObject else null
}
companion object {
const val REMARK_DEFAULT = "New Profile"
const val LINK_DEFAULT = "New Link"
fun remark(uri: URI, default: String = ""): String {
val name = uri.fragment ?: ""
return name.ifEmpty { default }
}
fun tryDecodeBase64(value: String): String {
return runCatching {
val byteArray = Base64.decode(value, Base64.DEFAULT)
String(byteArray)
}.getOrNull() ?: value
}
}
fun isValid(): Boolean {
return success && outbound != null
}
fun json(): String {
return config().toString(2) + "\n"
}
fun remark(): String {
return remark
}
private fun log(): JSONObject {
val log = JSONObject()
log.put("loglevel", "warning")
return log
}
private fun dns(): JSONObject {
val dns = JSONObject()
val servers = JSONArray()
servers.put(settings.primaryDns)
servers.put(settings.secondaryDns)
dns.put("servers", servers)
return dns
}
private fun inbounds(): JSONArray {
val inbounds = JSONArray()
val sniffing = JSONObject()
sniffing.put("enabled", true)
val sniffingDestOverride = JSONArray()
sniffingDestOverride.put("http")
sniffingDestOverride.put("tls")
sniffingDestOverride.put("quic")
sniffing.put("destOverride", sniffingDestOverride)
val tproxy = JSONObject()
tproxy.put("listen", settings.tproxyAddress)
tproxy.put("port", settings.tproxyPort)
tproxy.put("protocol", "dokodemo-door")
val tproxySettings = JSONObject()
tproxySettings.put("network", "tcp,udp")
tproxySettings.put("followRedirect", true)
val tproxySockopt = JSONObject()
tproxySockopt.put("tproxy", "tproxy")
val tproxyStreamSettings = JSONObject()
tproxyStreamSettings.put("sockopt", tproxySockopt)
tproxy.put("settings", tproxySettings)
tproxy.put("sniffing", sniffing)
tproxy.put("streamSettings", tproxyStreamSettings)
tproxy.put("tag", "all-in")
val socks = JSONObject()
socks.put("listen", settings.socksAddress)
socks.put("port", settings.socksPort.toInt())
socks.put("protocol", "socks")
val socksSettings = JSONObject()
socksSettings.put("udp", true)
if (
settings.socksUsername.trim().isNotEmpty() &&
settings.socksPassword.trim().isNotEmpty()
) {
val account = JSONObject()
account.put("user", settings.socksUsername)
account.put("pass", settings.socksPassword)
val accounts = JSONArray()
accounts.put(account)
socksSettings.put("auth", "password")
socksSettings.put("accounts", accounts)
}
socks.put("settings", socksSettings)
socks.put("sniffing", sniffing)
socks.put("tag", "socks")
if (settings.transparentProxy) inbounds.put(tproxy)
else inbounds.put(socks)
return inbounds
}
private fun outbounds(): JSONArray {
val outbounds = JSONArray()
val proxy = JSONObject(outbound!!.toString())
if (proxy.has("sendThrough")) {
remark = proxy.optString("sendThrough", REMARK_DEFAULT)
proxy.remove("sendThrough")
}
proxy.put("tag", "proxy")
val direct = JSONObject()
direct.put("protocol", "freedom")
direct.put("tag", "direct")
val block = JSONObject()
block.put("protocol", "blackhole")
block.put("tag", "block")
val dns = JSONObject()
dns.put("protocol", "dns")
dns.put("tag", "dns-out")
outbounds.put(proxy)
outbounds.put(direct)
outbounds.put(block)
if (settings.transparentProxy) outbounds.put(dns)
return outbounds
}
private fun routing(): JSONObject {
val routing = JSONObject()
routing.put("domainStrategy", "IPIfNonMatch")
val rules = JSONArray()
val proxyDns = JSONObject()
if (settings.transparentProxy) {
val inboundTag = JSONArray()
inboundTag.put("all-in")
proxyDns.put("network", "udp")
proxyDns.put("port", 53)
proxyDns.put("inboundTag", inboundTag)
proxyDns.put("outboundTag", "dns-out")
} else {
val proxyDnsIp = JSONArray()
proxyDnsIp.put(settings.primaryDns)
proxyDnsIp.put(settings.secondaryDns)
proxyDns.put("ip", proxyDnsIp)
proxyDns.put("port", 53)
proxyDns.put("outboundTag", "proxy")
}
val directPrivate = JSONObject()
val directPrivateIp = JSONArray()
directPrivateIp.put("geoip:private")
directPrivate.put("ip", directPrivateIp)
directPrivate.put("outboundTag", "direct")
rules.put(proxyDns)
rules.put(directPrivate)
routing.put("rules", rules)
return routing
}
private fun config(): JSONObject {
val config = JSONObject()
config.put("log", log())
config.put("dns", dns())
config.put("inbounds", inbounds())
config.put("outbounds", outbounds())
config.put("routing", routing())
return config
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/NetworkStateHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import com.topjohnwu.superuser.Shell
import io.github.saeeddev94.xray.service.TProxyService
import java.io.File
class NetworkStateHelper() {
fun monitor(script: File, pid: File) {
if (!script.exists()) makeScript(script)
val file = "/data/misc/net/rt_tables"
val cmd = "nohup inotifyd ${script.absolutePath} $file:w" +
" > /dev/null 2>&1 & echo $! > ${pid.absolutePath}"
Shell.cmd(cmd).exec()
}
fun getState(): NetworkState {
return NetworkState(getWifi(), getData())
}
fun isOnline(state: NetworkState): Boolean {
return state.wifi != null || state.data
}
private fun makeScript(file: File) {
val pkg = TProxyService.PKG_NAME
val action = TProxyService.NETWORK_UPDATE_SERVICE_ACTION_NAME
val content = arrayListOf(
"#!/bin/sh",
"",
"am start-foreground-service -n $pkg/.service.TProxyService -a $action",
"",
).joinToString("\n")
FileHelper.createOrUpdate(file, content)
Shell.cmd("chown root:root ${file.absolutePath}").exec()
Shell.cmd("chmod +x ${file.absolutePath}").exec()
}
private fun getWifi(): String? {
val cmd = "dumpsys wifi" +
" | grep 'mWifiInfo SSID'" +
" | awk -F 'SSID: ' '{print $2}'" +
" | awk -F ',' '{print $1}'" +
" | head -n 1"
val result = Shell.cmd(cmd).exec()
if (!result.isSuccess || result.out.isEmpty()) return null
val ssid = result.out.first()
if (ssid == "") return null
return ssid.trim('"')
}
private fun getData(): Boolean {
val cmd = "settings get global mobile_data" +
" && settings get global mobile_data1" +
" && settings get global mobile_data2"
val result = Shell.cmd(cmd).exec()
if (!result.isSuccess || result.out.isEmpty()) return false
return result.out.contains("1")
}
data class NetworkState(
val wifi: String?,
val data: Boolean,
)
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/ProfileTouchHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
class ProfileTouchHelper(private var adapter: ProfileTouchCallback) : ItemTouchHelper.Callback() {
private var startPosition: Int = -1
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
override fun onMove(
recyclerView: RecyclerView,
source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = adapter.onItemMoved(source.absoluteAdapterPosition, target.absoluteAdapterPosition)
override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
direction: Int
) {
}
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?,
actionState: Int
) {
if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) return
startPosition = viewHolder!!.absoluteAdapterPosition
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
val endPosition = viewHolder.absoluteAdapterPosition
adapter.onItemMoveCompleted(startPosition, endPosition)
}
interface ProfileTouchCallback {
fun onItemMoved(fromPosition: Int, toPosition: Int): Boolean
fun onItemMoveCompleted(startPosition: Int, endPosition: Int)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/helper/TransparentProxyHelper.kt
================================================
package io.github.saeeddev94.xray.helper
import android.content.Context
import com.topjohnwu.superuser.Shell
import io.github.saeeddev94.xray.BuildConfig
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.service.TProxyService
import java.io.FileOutputStream
class TransparentProxyHelper(
private val context: Context,
private val settings: Settings,
) {
private val networkStateHelper by lazy { NetworkStateHelper() }
fun isRunning(): Boolean = settings.xrayCorePid().exists()
fun startService() {
makeConfig()
Shell.cmd("${cmd()} service start").exec()
}
fun stopService() {
Shell.cmd("${cmd()} service stop").exec()
}
fun enableProxy() {
Shell.cmd("${cmd()} proxy enable").exec()
}
fun disableProxy() {
Shell.cmd("${cmd()} proxy disable").exec()
}
fun refreshProxy() {
Shell.cmd("${cmd()} proxy refresh").exec()
}
fun kill() {
if (settings.networkMonitorPid().exists()) {
val path = settings.networkMonitorPid().absolutePath
Shell.cmd("kill $(cat $path) && rm $path").exec()
}
if (settings.xrayCorePid().exists()) {
disableProxy()
stopService()
}
}
fun monitorNetwork() {
val script = settings.networkMonitorScript()
val pid = settings.networkMonitorPid()
if (!settings.tproxyAutoConnect || !settings.transparentProxy || pid.exists()) return
networkStateHelper.monitor(script, pid)
}
fun networkState(): NetworkStateHelper.NetworkState {
return networkStateHelper.getState()
}
fun bypassWiFi(state: NetworkStateHelper.NetworkState): Boolean {
val tproxyBypassWiFi = settings.tproxyBypassWiFi
val ssid = state.wifi ?: ""
return tproxyBypassWiFi.isNotEmpty() && tproxyBypassWiFi.contains(ssid)
}
fun networkUpdate(value: NetworkStateHelper.NetworkState? = null) {
if (!settings.tproxyAutoConnect) return
val state = value ?: networkState()
val isOnline = networkStateHelper.isOnline(state)
val isRunning = settings.xrayCorePid().exists()
if (!isOnline || bypassWiFi(state)) {
TProxyService.stop(context)
return
}
if (!isRunning) {
TProxyService.start(context, false)
return
}
refreshProxy()
}
fun install() {
val xrayHelper = settings.xrayHelperFile()
val appVersionCode = BuildConfig.VERSION_CODE
val xrayHelperVersionCode = settings.xrayHelperVersionCode
if (xrayHelper.exists() && xrayHelperVersionCode == appVersionCode) return
if (xrayHelper.exists()) xrayHelper.delete()
context.assets.open(xrayHelper.name).use { input ->
FileOutputStream(xrayHelper).use { output ->
input.copyTo(output)
}
}
Shell.cmd("chown root:root ${xrayHelper.absolutePath}").exec()
Shell.cmd("chmod +x ${xrayHelper.absolutePath}").exec()
settings.xrayHelperVersionCode = appVersionCode
}
private fun cmd(): String {
return arrayListOf(
settings.xrayHelperFile().absolutePath,
"-c",
settings.xrayHelperConfig().absolutePath,
).joinToString(" ")
}
private fun makeConfig() {
val yml = arrayListOf(
"xrayHelper:",
" coreType: xray",
" corePath: ${settings.xrayCoreFile().absolutePath}",
" coreConfig: ${settings.xrayConfig().absolutePath}",
" dataDir: ${settings.baseDir().absolutePath}",
" runDir: ${settings.baseDir().absolutePath}",
"proxy:",
" method: tproxy",
" tproxyPort: ${settings.tproxyPort}",
" enableIPv6: ${settings.enableIpV6}",
" mode: ${if (settings.appsRoutingMode) "blacklist" else "whitelist"}",
)
val appsList = settings.appsRouting.split("\n")
.map { it.trim() }
.filter { it.trim().isNotBlank() }
if (appsList.isNotEmpty()) {
yml.add(" pkgList:")
appsList.forEach { yml.add(" - $it") }
}
val includedInterfaces = arrayListOf()
val excludedInterfaces = arrayListOf()
if (settings.tproxyHotspot) includedInterfaces.add(settings.hotspotInterface)
else excludedInterfaces.add(settings.hotspotInterface)
if (settings.tproxyTethering) includedInterfaces.add(settings.tetheringInterface)
else excludedInterfaces.add(settings.tetheringInterface)
if (includedInterfaces.isNotEmpty()) {
yml.add(" apList:")
includedInterfaces.forEach { yml.add(" - $it") }
}
if (excludedInterfaces.isNotEmpty()) {
yml.add(" ignoreList:")
excludedInterfaces.forEach { yml.add(" - $it") }
}
yml.add("")
FileHelper.createOrUpdate(
settings.xrayHelperConfig(),
yml.joinToString("\n")
)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/receiver/BootReceiver.kt
================================================
package io.github.saeeddev94.xray.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.helper.TransparentProxyHelper
import io.github.saeeddev94.xray.service.TProxyService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val systemUpTime = SystemClock.elapsedRealtime()
val twoMinutes = 2L * 60L * 1000L
val isAppLaunch = systemUpTime > twoMinutes
if (
context == null ||
intent == null ||
intent.action != Intent.ACTION_BOOT_COMPLETED ||
isAppLaunch
) return
val settings = Settings(context)
val xrayCorePid = settings.xrayCorePid()
val networkMonitorPid = settings.networkMonitorPid()
if (xrayCorePid.exists()) xrayCorePid.delete()
if (networkMonitorPid.exists()) networkMonitorPid.delete()
if (!settings.bootAutoStart) {
TProxyService.stop(context)
return
}
if (settings.transparentProxy) {
val pendingResult = goAsync()
val transparentProxyHelper = TransparentProxyHelper(context, settings)
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
val state = transparentProxyHelper.networkState()
val bypassWiFi = transparentProxyHelper.bypassWiFi(state)
transparentProxyHelper.monitorNetwork()
withContext(Dispatchers.Main) {
if (bypassWiFi) TProxyService.stop(context)
else TProxyService.start(context, false)
pendingResult.finish()
}
}
return
}
TProxyService.start(context, settings.tun2socks)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/receiver/VpnActionReceiver.kt
================================================
package io.github.saeeddev94.xray.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.service.TProxyService
import io.github.saeeddev94.xray.service.VpnTileService
class VpnActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (context == null || intent == null) return
val allowed = listOf(
TProxyService.START_VPN_SERVICE_ACTION_NAME,
TProxyService.STOP_VPN_SERVICE_ACTION_NAME,
TProxyService.NEW_CONFIG_SERVICE_ACTION_NAME,
)
val action = intent.action ?: ""
val label = intent.getStringExtra("profile") ?: context.getString(R.string.appName)
if (!allowed.contains(action)) return
Intent(context, VpnTileService::class.java).also {
it.putExtra("action", action)
it.putExtra("label", label)
context.startService(it)
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/repository/ConfigRepository.kt
================================================
package io.github.saeeddev94.xray.repository
import io.github.saeeddev94.xray.database.Config
import io.github.saeeddev94.xray.database.ConfigDao
class ConfigRepository(private val configDao: ConfigDao) {
suspend fun get(): Config {
val config = configDao.get()
if (config != null) return config
return Config().also { configDao.insert(it) }
}
suspend fun update(config: Config) {
configDao.update(config)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/repository/LinkRepository.kt
================================================
package io.github.saeeddev94.xray.repository
import io.github.saeeddev94.xray.database.Link
import io.github.saeeddev94.xray.database.LinkDao
class LinkRepository(private val linkDao: LinkDao) {
val all = linkDao.all()
val tabs = linkDao.tabs()
suspend fun activeLinks(): List {
return linkDao.activeLinks()
}
suspend fun insert(link: Link) {
linkDao.insert(link)
}
suspend fun update(link: Link) {
linkDao.update(link)
}
suspend fun delete(link: Link) {
linkDao.delete(link)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/repository/ProfileRepository.kt
================================================
package io.github.saeeddev94.xray.repository
import io.github.saeeddev94.xray.database.Profile
import io.github.saeeddev94.xray.database.ProfileDao
class ProfileRepository(private val profileDao: ProfileDao) {
val all = profileDao.all()
suspend fun linkProfiles(linkId: Long): List {
return profileDao.linkProfiles(linkId)
}
suspend fun find(id: Long): Profile {
return profileDao.find(id)
}
suspend fun update(profile: Profile) {
profileDao.update(profile)
}
suspend fun create(profile: Profile) {
profileDao.create(profile)
}
suspend fun remove(profile: Profile) {
profileDao.remove(profile)
}
suspend fun updateIndex(index: Int, id: Long) {
profileDao.updateIndex(index, id)
}
suspend fun moveUp(start: Int, end: Int, exclude: Long) {
profileDao.moveUp(start, end, exclude)
}
suspend fun moveDown(start: Int, end: Int, exclude: Long) {
profileDao.moveDown(start, end, exclude)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/service/TProxyService.kt
================================================
package io.github.saeeddev94.xray.service
import XrayCore.XrayCore
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.VpnService
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.ParcelFileDescriptor
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import io.github.saeeddev94.xray.BuildConfig
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
import io.github.saeeddev94.xray.Xray
import io.github.saeeddev94.xray.activity.MainActivity
import io.github.saeeddev94.xray.database.Config
import io.github.saeeddev94.xray.database.Profile
import io.github.saeeddev94.xray.dto.XrayConfig
import io.github.saeeddev94.xray.helper.ConfigHelper
import io.github.saeeddev94.xray.helper.FileHelper
import io.github.saeeddev94.xray.helper.TransparentProxyHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.io.File
import kotlin.reflect.cast
@SuppressLint("VpnServicePolicy")
class TProxyService : VpnService() {
companion object {
init {
System.loadLibrary("hev-socks5-tunnel")
}
const val PKG_NAME = BuildConfig.APPLICATION_ID
const val STATUS_VPN_SERVICE_ACTION_NAME = "$PKG_NAME.VpnStatus"
const val STOP_VPN_SERVICE_ACTION_NAME = "$PKG_NAME.VpnStop"
const val START_VPN_SERVICE_ACTION_NAME = "$PKG_NAME.VpnStart"
const val NEW_CONFIG_SERVICE_ACTION_NAME = "$PKG_NAME.NewConfig"
const val NETWORK_UPDATE_SERVICE_ACTION_NAME = "$PKG_NAME.NetworkUpdate"
private const val VPN_SERVICE_NOTIFICATION_ID = 1
private const val OPEN_MAIN_ACTIVITY_ACTION_ID = 2
private const val STOP_VPN_SERVICE_ACTION_ID = 3
fun status(context: Context) = startCommand(context, STATUS_VPN_SERVICE_ACTION_NAME)
fun stop(context: Context) = startCommand(context, STOP_VPN_SERVICE_ACTION_NAME)
fun newConfig(context: Context) = startCommand(context, NEW_CONFIG_SERVICE_ACTION_NAME)
fun start(context: Context, check: Boolean) {
if (check && prepare(context) != null) {
Log.e(
"TProxyService",
"Can't start: VpnService#prepare(): needs user permission"
)
return
}
startCommand(context, START_VPN_SERVICE_ACTION_NAME, true)
}
private fun startCommand(context: Context, name: String, foreground: Boolean = false) {
Intent(context, TProxyService::class.java).also {
it.action = name
if (foreground) {
context.startForegroundService(it)
} else {
context.startService(it)
}
}
}
}
private val notificationManager by lazy { getSystemService(NotificationManager::class.java) }
private val connectivityManager by lazy { getSystemService(ConnectivityManager::class.java) }
private val settings by lazy { Settings(applicationContext) }
private val transparentProxyHelper by lazy { TransparentProxyHelper(this, settings) }
private val configRepository by lazy { Xray::class.cast(application).configRepository }
private val profileRepository by lazy { Xray::class.cast(application).profileRepository }
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var isRunning: Boolean = false
private var tunDevice: ParcelFileDescriptor? = null
private var cellularCallback: ConnectivityManager.NetworkCallback? = null
private var toast: Toast? = null
private external fun TProxyStartService(configPath: String, fd: Int)
private external fun TProxyStopService()
private external fun TProxyGetStats(): LongArray
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
scope.launch {
when (intent?.action) {
START_VPN_SERVICE_ACTION_NAME -> start(getProfile(), globalConfigs())
NEW_CONFIG_SERVICE_ACTION_NAME -> newConfig(getProfile(), globalConfigs())
STOP_VPN_SERVICE_ACTION_NAME -> stopVPN()
STATUS_VPN_SERVICE_ACTION_NAME -> broadcastStatus()
NETWORK_UPDATE_SERVICE_ACTION_NAME -> transparentProxyHelper.networkUpdate()
}
}
return START_STICKY
}
override fun onRevoke() {
stopVPN()
}
override fun onDestroy() {
scope.cancel()
cellularCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
cellularCallback = null
toast = null
super.onDestroy()
}
private fun configName(profile: Profile?): String = profile?.name ?: settings.tunName
private fun getIsRunning(): Boolean {
return if (settings.transparentProxy) {
transparentProxyHelper.isRunning()
} else {
isRunning
}
}
private suspend fun getProfile(): Profile? {
return if (settings.selectedProfile == 0L) {
null
} else {
profileRepository.find(settings.selectedProfile)
}
}
private suspend fun globalConfigs(): Config {
return configRepository.get()
}
private fun getConfig(profile: Profile, globalConfigs: Config): XrayConfig? {
val dir: File = applicationContext.filesDir
val config: File = settings.xrayConfig()
val configHelper = runCatching { ConfigHelper(settings, globalConfigs, profile.config) }
val error: String = if (configHelper.isSuccess) {
FileHelper.createOrUpdate(config, configHelper.getOrNull().toString())
XrayCore.test(dir.absolutePath, config.absolutePath)
} else {
configHelper.exceptionOrNull()?.message ?: getString(R.string.invalidProfile)
}
if (error.isNotEmpty()) {
showToast(error)
return null
}
return XrayConfig(dir.absolutePath, config.absolutePath)
}
private fun start(profile: Profile?, globalConfigs: Config) {
if (profile == null) return
getConfig(profile, globalConfigs)?.let {
startXray(it)
startVPN(profile)
}
}
private fun newConfig(profile: Profile?, globalConfigs: Config) {
if (!getIsRunning() || profile == null) return
stopXray()
getConfig(profile, globalConfigs).also {
if (it == null) stopVPN() else startXray(it)
}?.let {
val name = configName(profile)
val notification = createNotification(name)
showToast(name)
broadcastStart(NEW_CONFIG_SERVICE_ACTION_NAME, name)
notificationManager.notify(VPN_SERVICE_NOTIFICATION_ID, notification)
}
}
private fun startXray(config: XrayConfig) {
if (settings.transparentProxy) transparentProxyHelper.startService()
else XrayCore.start(config.dir, config.file)
}
private fun stopXray() {
if (settings.transparentProxy) transparentProxyHelper.stopService()
else XrayCore.stop()
}
private fun startVPN(profile: Profile?) {
if (settings.transparentProxy) {
transparentProxyHelper.enableProxy()
transparentProxyHelper.monitorNetwork()
} else if (settings.tun2socks) {
/** Create Tun */
val tun = Builder()
val tunName = getString(R.string.appName)
/** Basic tun config */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) tun.setMetered(false)
tun.setMtu(settings.tunMtu)
tun.setSession(tunName)
/** IPv4 */
tun.addAddress(settings.tunAddress, settings.tunPrefix)
tun.addDnsServer(settings.primaryDns)
tun.addDnsServer(settings.secondaryDns)
/** IPv6 */
if (settings.enableIpV6) {
tun.addAddress(settings.tunAddressV6, settings.tunPrefixV6)
tun.addDnsServer(settings.primaryDnsV6)
tun.addDnsServer(settings.secondaryDnsV6)
tun.addRoute("::", 0)
}
/** Bypass LAN (IPv4) */
if (settings.bypassLan) {
settings.tunRoutes.forEach {
val address = it.split('/')
tun.addRoute(address[0], address[1].toInt())
}
} else {
tun.addRoute("0.0.0.0", 0)
}
/** Apps Routing */
if (settings.appsRoutingMode) tun.addDisallowedApplication(applicationContext.packageName)
settings.appsRouting.split("\n").forEach {
val packageName = it.trim()
if (packageName.isBlank()) return@forEach
if (settings.appsRoutingMode) tun.addDisallowedApplication(packageName)
else tun.addAllowedApplication(packageName)
}
/** Build tun device */
tunDevice = tun.establish()
/** Check tun device */
if (tunDevice == null) {
Log.e("TProxyService", "tun#establish failed")
return
}
/** Create, Update tun2socks config */
val tun2socksConfig = arrayListOf(
"tunnel:",
" name: $tunName",
" mtu: ${settings.tunMtu}",
"socks5:",
" address: ${settings.socksAddress}",
" port: ${settings.socksPort}",
)
if (
settings.socksUsername.trim().isNotEmpty() &&
settings.socksPassword.trim().isNotEmpty()
) {
tun2socksConfig.add(" username: ${settings.socksUsername}")
tun2socksConfig.add(" password: ${settings.socksPassword}")
}
tun2socksConfig.add(if (settings.socksUdp) " udp: udp" else " udp: tcp")
tun2socksConfig.add("")
FileHelper.createOrUpdate(
settings.tun2socksConfig(),
tun2socksConfig.joinToString("\n")
)
/** Start tun2socks */
TProxyStartService(settings.tun2socksConfig().absolutePath, tunDevice!!.fd)
}
/** Service Notification */
val name = configName(profile)
startForeground(VPN_SERVICE_NOTIFICATION_ID, createNotification(name))
/** Listen for cellular changes */
if (cellularCallback == null) {
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
cellularCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
this@TProxyService.transparentProxyHelper.networkUpdate()
}
}
connectivityManager.registerNetworkCallback(request, cellularCallback!!)
}
/** Broadcast start event */
showToast("Start VPN")
isRunning = true
broadcastStart(START_VPN_SERVICE_ACTION_NAME, name)
}
private fun stopVPN() {
if (settings.transparentProxy) {
transparentProxyHelper.disableProxy()
} else {
TProxyStopService()
runCatching { tunDevice?.close() }
tunDevice = null
isRunning = false
}
stopXray()
stopForeground(STOP_FOREGROUND_REMOVE)
showToast("Stop VPN")
broadcastStop()
stopSelf()
}
private fun broadcastStart(action: String, configName: String) {
Intent(action).also {
it.`package` = BuildConfig.APPLICATION_ID
it.putExtra("profile", configName)
sendBroadcast(it)
}
}
private fun broadcastStop() {
Intent(STOP_VPN_SERVICE_ACTION_NAME).also {
it.`package` = BuildConfig.APPLICATION_ID
sendBroadcast(it)
}
}
private fun broadcastStatus() {
Intent(STATUS_VPN_SERVICE_ACTION_NAME).also {
it.`package` = BuildConfig.APPLICATION_ID
it.putExtra("isRunning", getIsRunning())
sendBroadcast(it)
}
}
private fun createNotification(name: String): Notification {
val pendingActivity = PendingIntent.getActivity(
applicationContext,
OPEN_MAIN_ACTIVITY_ACTION_ID,
Intent(applicationContext, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
val pendingStop = PendingIntent.getService(
applicationContext,
STOP_VPN_SERVICE_ACTION_ID,
Intent(applicationContext, TProxyService::class.java).also {
it.action = STOP_VPN_SERVICE_ACTION_NAME
},
PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat
.Builder(applicationContext, createNotificationChannel())
.setSmallIcon(R.drawable.baseline_vpn_lock)
.setContentTitle(name)
.setContentIntent(pendingActivity)
.addAction(0, getString(R.string.vpnStop), pendingStop)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setOngoing(true)
.build()
}
private fun createNotificationChannel(): String {
val id = "XrayVpnServiceNotification"
val name = "Xray VPN Service"
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(channel)
return id
}
private fun showToast(message: String) {
Handler(Looper.getMainLooper()).post {
toast?.cancel()
toast = Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).also {
it.show()
}
}
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/service/VpnTileService.kt
================================================
package io.github.saeeddev94.xray.service
import android.content.ComponentName
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.drawable.Icon
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.core.content.edit
import io.github.saeeddev94.xray.R
import io.github.saeeddev94.xray.Settings
class VpnTileService : TileService() {
private val settings by lazy { Settings(applicationContext) }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
requestListeningState(this, ComponentName(this, VpnTileService::class.java))
val action = intent?.getStringExtra("action") ?: ""
val label = intent?.getStringExtra("label") ?: ""
val sharedPref = sharedPref()
sharedPref.edit {
putString("action", action)
putString("label", label)
}
handleUpdate(action, label)
return START_STICKY
}
override fun onStartListening() {
super.onStartListening()
handleUpdate()
}
override fun onClick() {
super.onClick()
val proxy = !settings.tun2socks || settings.transparentProxy
when (qsTile?.state) {
Tile.STATE_INACTIVE -> {
TProxyService.start(applicationContext, !proxy)
}
Tile.STATE_ACTIVE -> TProxyService.stop(applicationContext)
}
}
private fun handleUpdate(newAction: String? = null, newLabel: String? = null) {
val sharedPref = sharedPref()
val action = newAction ?: sharedPref.getString("action", "")!!
val label = newLabel ?: sharedPref.getString("label", "")!!
if (action.isNotEmpty() && label.isNotEmpty()) {
when (action) {
TProxyService.START_VPN_SERVICE_ACTION_NAME,
TProxyService.NEW_CONFIG_SERVICE_ACTION_NAME -> updateTile(Tile.STATE_ACTIVE, label)
TProxyService.STOP_VPN_SERVICE_ACTION_NAME -> updateTile(Tile.STATE_INACTIVE, label)
}
}
}
private fun updateTile(newState: Int, newLabel: String) {
val tile = qsTile ?: return
tile.apply {
state = newState
label = newLabel
icon = Icon.createWithResource(applicationContext, R.drawable.vpn_key)
updateTile()
}
}
private fun sharedPref(): SharedPreferences {
return getSharedPreferences("vpn_tile", MODE_PRIVATE)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/viewmodel/ConfigViewModel.kt
================================================
package io.github.saeeddev94.xray.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import io.github.saeeddev94.xray.Xray
import io.github.saeeddev94.xray.database.Config
import kotlinx.coroutines.launch
class ConfigViewModel(application: Application) : AndroidViewModel(application) {
private val configRepository by lazy { getApplication().configRepository }
suspend fun get() = configRepository.get()
fun update(config: Config) = viewModelScope.launch {
configRepository.update(config)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/viewmodel/LinkViewModel.kt
================================================
package io.github.saeeddev94.xray.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import io.github.saeeddev94.xray.Xray
import io.github.saeeddev94.xray.database.Link
import kotlinx.coroutines.launch
class LinkViewModel(application: Application) : AndroidViewModel(application) {
private val linkRepository by lazy { getApplication().linkRepository }
val tabs = linkRepository.tabs
val links = linkRepository.all
suspend fun activeLinks(): List {
return linkRepository.activeLinks()
}
fun insert(link: Link) = viewModelScope.launch {
linkRepository.insert(link)
}
fun update(link: Link) = viewModelScope.launch {
linkRepository.update(link)
}
fun delete(link: Link) = viewModelScope.launch {
linkRepository.delete(link)
}
}
================================================
FILE: app/src/main/java/io/github/saeeddev94/xray/viewmodel/ProfileViewModel.kt
================================================
package io.github.saeeddev94.xray.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import io.github.saeeddev94.xray.Xray
import io.github.saeeddev94.xray.database.Profile
import io.github.saeeddev94.xray.dto.ProfileList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class ProfileViewModel(application: Application) : AndroidViewModel(application) {
private val profileRepository by lazy { getApplication().profileRepository }
val profiles = profileRepository.all.flowOn(Dispatchers.IO).stateIn(
viewModelScope,
SharingStarted.Eagerly,
listOf(),
)
val filtered = MutableSharedFlow>()
fun next(link: Long) = viewModelScope.launch {
val all = profiles.value
fixIndex(all)
val list = all.filter { link == 0L || link == it.link }
filtered.emit(list)
}
suspend fun linkProfiles(linkId: Long): List {
return profileRepository.linkProfiles(linkId)
}
suspend fun find(id: Long): Profile {
return profileRepository.find(id)
}
suspend fun create(profile: Profile) {
return profileRepository.create(profile)
}
suspend fun update(profile: Profile) {
profileRepository.update(profile)
}
suspend fun remove(profile: Profile) {
profileRepository.remove(profile)
}
suspend fun moveUp(start: Int, end: Int, exclude: Long) {
profileRepository.moveUp(start, end, exclude)
}
suspend fun moveDown(start: Int, end: Int, exclude: Long) {
profileRepository.moveDown(start, end, exclude)
}
private fun fixIndex(list: List) = viewModelScope.launch {
list.forEachIndexed { index, profile ->
if (profile.index == index) return@forEachIndexed
profileRepository.updateIndex(index, profile.id)
}
}
}
================================================
FILE: app/src/main/jni/Android.mk
================================================
# Copyright (C) 2023 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.
#
include $(call all-subdir-makefiles)
================================================
FILE: app/src/main/jni/Application.mk
================================================
# Copyright (C) 2023 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.
#
APP_OPTIM := release
APP_PLATFORM := android-26
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
APP_CFLAGS := -O3 -DPKGNAME=io/github/saeeddev94/xray/service
APP_CPPFLAGS := -O3 -std=c++11
NDK_TOOLCHAIN_VERSION := clang
LOCAL_LDFLAGS += -Wl,--build-id=none -Wl,--hash-style=gnu
================================================
FILE: app/src/main/res/drawable/baseline_adb.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_add.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_alt_route.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_config.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_content_copy.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_delete.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_done.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_download.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_edit.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_file_open.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_folder_open.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_link.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_refresh.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_settings.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_vpn_key.xml
================================================
================================================
FILE: app/src/main/res/drawable/baseline_vpn_lock.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_xray.xml
================================================
================================================
FILE: app/src/main/res/drawable-v26/ic_launcher.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_apps_routing.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_assets.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_configs.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_links.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_logs.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_profile.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_scanner.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_settings.xml
================================================
================================================
FILE: app/src/main/res/layout/item_recycler_exclude.xml
================================================
================================================
FILE: app/src/main/res/layout/item_recycler_main.xml
================================================
================================================
FILE: app/src/main/res/layout/layout_link_form.xml
================================================
================================================
FILE: app/src/main/res/layout/layout_link_item.xml
================================================
================================================
FILE: app/src/main/res/layout/layout_tun_routes.xml
================================================
================================================
FILE: app/src/main/res/layout/loading_dialog.xml
================================================
================================================
FILE: app/src/main/res/layout/tab_advanced_settings.xml
================================================
================================================
FILE: app/src/main/res/layout/tab_basic_settings.xml
================================================
================================================
FILE: app/src/main/res/layout/tab_config.xml
================================================
================================================
FILE: app/src/main/res/menu/menu_apps_routing.xml
================================================
================================================
FILE: app/src/main/res/menu/menu_configs.xml
================================================
================================================
FILE: app/src/main/res/menu/menu_drawer.xml
================================================
================================================
FILE: app/src/main/res/menu/menu_links.xml
================================================
================================================
FILE: app/src/main/res/menu/menu_logs.xml
================================================
================================================
FILE: app/src/main/res/menu/menu_main.xml
================================================
================================================
FILE: app/src/main/res/menu/menu_profile.xml
================================================
================================================
FILE: app/src/main/res/menu/menu_settings.xml
================================================
================================================
FILE: app/src/main/res/values/array.xml
================================================
- 0.0.0.0/5
- 8.0.0.0/7
- 11.0.0.0/8
- 12.0.0.0/6
- 16.0.0.0/4
- 32.0.0.0/3
- 64.0.0.0/2
- 128.0.0.0/3
- 160.0.0.0/5
- 168.0.0.0/6
- 172.0.0.0/12
- 172.32.0.0/11
- 172.64.0.0/10
- 172.128.0.0/9
- 173.0.0.0/8
- 174.0.0.0/7
- 176.0.0.0/4
- 192.0.0.0/9
- 192.128.0.0/11
- 192.160.0.0/13
- 192.169.0.0/16
- 192.170.0.0/15
- 192.172.0.0/14
- 192.176.0.0/12
- 192.192.0.0/10
- 193.0.0.0/8
- 194.0.0.0/7
- 196.0.0.0/6
- 200.0.0.0/5
- 208.0.0.0/4
- 240.0.0.0/4
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#009966
#636363
#313131
================================================
FILE: app/src/main/res/values/strings.xml
================================================
Xray
-
Open drawer
Close drawer
Scan QrCode
From Clipboard
Loading…
START
STOP
Not Connected
Connected, tap to check connection
Testing…
SaeedDev94/Xray
XTLS/Xray-core
heiher/hev-socks5-tunnel
2.14.4
Assets
Links
Logs
Apps Routing
Configs
Settings
GeoIP
GeoSite
Refresh Links
New Link
Edit Link
Name
Address
User-Agent
Is Active?
Create
Update
Close
New Profile
Edit Profile
Name
Config
Edit Icon
Delete Icon
Download Icon
File Open Icon
Copy Logs
Delete Logs
App Icon
Open Json
Save
Save Configs
Mode: Exclude
Mode: Include
Save Settings
Save Profile
Invalid Profile
Invalid Link
HTTP is forbidden
Only HTTPS is acceptable
Cancel
Ignore
OK
Socks Address
Socks Port
Socks Username
Socks Password
GeoIP Address
GeoSite Address
Ping Address
Ping Timeout
Refresh Links Interval
Bypass LAN (IPv4)
Enable IPv6
Socks UDP
Tun2socks
Boot Auto Start
Refresh Links On Open
Primary DNS (v4)
Secondary DNS (v4)
Primary DNS (v6)
Secondary DNS (v6)
Tun Name
Tun Mtu
Tun Address (IPv4)
Prefix
Tun Address (IPv6)
Prefix
Hotspot Interface
Tethering Interface
TPROXY Address
TPROXY Port
TPROXY Bypass WiFi
TPROXY Auto Connect
TPROXY Hotspot
TPROXY Tethering
Transparent Proxy
Tun Routes
================================================
FILE: app/src/main/res/values/themes.xml
================================================
================================================
FILE: app/src/main/res/xml/network_security_config.xml
================================================
================================================
FILE: app/versionCode.txt
================================================
1060
================================================
FILE: build-xray.sh
================================================
#!/bin/bash
# Update repo
apt-get update
apt-get install -y ca-certificates
echo "deb https://deb.debian.org/debian forky main" > /etc/apt/sources.list.d/forky.list
apt-get update || apt-get update
apt-get dist-upgrade -y
# Tools version
ANDROID_PLATFORM_VERSION="android-36"
ANDROID_SDK_VERSION="36.0.0"
JAVA_VERSION="21"
# Install Tools
apt-get install -y git openjdk-$JAVA_VERSION-jdk-headless sdkmanager wget unzip gcc libc-dev golang-go
# Define dirs
HOME_DIR="/home/vagrant"
BUILD_DIR="$HOME_DIR/build"
REPO_DIR="$BUILD_DIR/io.github.saeeddev94.xray"
# Set vars
export JAVA_HOME="/usr/lib/jvm/java-$JAVA_VERSION-openjdk-amd64"
export ANDROID_HOME="/opt/android-sdk"
# Set path
export PATH="$JAVA_HOME/bin:$PATH"
export PATH="$ANDROID_HOME/platform-tools:$PATH"
export PATH="$ANDROID_HOME/build-tools/$ANDROID_SDK_VERSION:$PATH"
# Clone repo
git clone https://github.com/SaeedDev94/Xray.git $REPO_DIR
cd $REPO_DIR
git checkout "$RELEASE_TAG"
git submodule update --init --recursive
# Setup SDK & NDK
ANDROID_NDK_VERSION=$(awk -F '"' '/ndkVersion/ {print $2}' app/build.gradle.kts)
sdkmanager "platform-tools" "platforms;$ANDROID_PLATFORM_VERSION" "build-tools;$ANDROID_SDK_VERSION"
sdkmanager --install "ndk;$ANDROID_NDK_VERSION" --channel=3
# Setup gradle
GRADLE_DIR="$BUILD_DIR/gradle"
GRADLE_URL=$(grep distributionUrl gradle/wrapper/gradle-wrapper.properties | \
cut -d '=' -f 2 | \
sed 's#\\##g')
GRADLE_ARCHIVE=$(basename $GRADLE_URL)
GRADLE_VERSION=$(echo "$GRADLE_ARCHIVE" | sed -E 's/gradle-([0-9.]+)-bin\.zip/\1/')
mkdir -p $GRADLE_DIR
pushd $GRADLE_DIR
wget "$GRADLE_URL"
unzip "$GRADLE_ARCHIVE"
rm "$GRADLE_ARCHIVE"
mv * "$GRADLE_VERSION"
popd
export PATH="$GRADLE_DIR/$GRADLE_VERSION/bin:$PATH"
# Clean task
rm gradle/wrapper/gradle-wrapper.jar
gradle clean
# Build dependencies
./buildGo.sh $NATIVE_ARCH
# Build app
echo "$KS_FILE" > /tmp/xray_base64.txt
base64 -d /tmp/xray_base64.txt > /tmp/xray.jks
gradle -PabiId=$ABI_ID -PabiTarget=$ABI_TARGET assembleRelease
rm /tmp/xray_base64.txt /tmp/xray.jks
# Build name
VERSION_CODE=$(cat app/versionCode.txt)
((VERSION_CODE += ABI_ID))
BUILD_NAME="Xray-$RELEASE_TAG-$VERSION_CODE.apk"
mv "app/build/outputs/apk/release/app-$ABI_TARGET-release.apk" "$DIST_DIR/$BUILD_NAME"
================================================
FILE: build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.google.ksp) apply false
}
================================================
FILE: buildGo.sh
================================================
#!/bin/bash
# Set vars
export GOROOT="$(realpath go-root)"
export GOPATH="$(realpath go-path)"
# Set path
export PATH="$GOROOT/bin:$PATH"
export PATH="$GOPATH/bin:$PATH"
git clone https://github.com/golang/go.git $GOROOT
pushd $GOROOT
git checkout "go$(sed -n -E 's/^go (.*)/\1/p' ../XrayCore/go.mod)"
cd src
./make.bash
popd
./buildXrayCore.sh $1
./buildXrayHelper.sh $1
================================================
FILE: buildXrayCore.sh
================================================
#!/bin/bash
TARGET="$1"
REFRESH="$2"
SETUP="$3"
ARCHS=(arm arm64 386 amd64)
DEST="../app/libs"
is_in_array() {
local value="$1"
local array=("${@:2}")
for item in "${array[@]}"; do
if [[ "$item" == "$value" ]]; then
return 0
fi
done
return 1
}
check_target() {
if ! is_in_array "$TARGET" "${ARCHS[@]}"; then
echo "Not supported"
exit 1
fi
}
prepare_go() {
echo "Install dependencies"
if [[ -n "$SETUP" ]]; then
rm go*
go mod init XrayCore
go mod edit -replace github.com/xtls/xray-core=./Xray-core
go mod edit -replace github.com/xtls/libxray=./libXray
go mod tidy
go get golang.org/x/mobile
go get google.golang.org/genproto
fi
local VERSION=$(awk -F ' ' '/golang.org\/x\/mobile/ {print $2}' go.mod)
go install golang.org/x/mobile/cmd/gomobile@$VERSION
go mod download
}
build_android() {
echo "Building XrayCore for $TARGET"
rm -f "$DEST/XrayCore*"
gomobile init
gomobile bind -o "$DEST/XrayCore.aar" -androidapi 26 -target "android/$TARGET" -ldflags="-buildid=" -trimpath
}
refresh_dependencies() {
echo "Gradle: refresh dependencies"
./gradlew --refresh-dependencies clean
}
check_target
pushd XrayCore
prepare_go
build_android
popd
if [[ -n "$REFRESH" ]]; then
refresh_dependencies
fi
================================================
FILE: buildXrayHelper.sh
================================================
#!/bin/bash
TARGET="$1"
ARCHS=(arm arm64 386 amd64)
DEST="../app/src/main/assets"
is_in_array() {
local value="$1"
local array=("${@:2}")
for item in "${array[@]}"; do
if [[ "$item" == "$value" ]]; then
return 0
fi
done
return 1
}
check_target() {
if ! is_in_array "$TARGET" "${ARCHS[@]}"; then
echo "Not supported"
exit 1
fi
}
prepare_go() {
echo "Install dependencies"
go mod download
}
build_android() {
echo "Building XrayHelper for $TARGET"
local OUTPUT="$DEST/xrayhelper"
rm -f $OUTPUT
CGO_ENABLED=0 GOOS=linux GOARCH=$TARGET \
go build -v -o $OUTPUT \
-ldflags "-s -w -buildid=" -buildvcs=false -trimpath ./main
}
check_target
pushd XrayHelper
prepare_go
build_android
popd
================================================
FILE: gradle/libs.versions.toml
================================================
[versions]
agp = "9.1.1"
parcelize="2.3.20"
ksp = "2.3.6"
androidxActivityKtx = "1.13.0"
androidxCoreKtx = "1.18.0"
androidxAppCompat = "1.7.1"
androidxLifecycleViewmodelKtx = "2.10.0"
androidxRoom = "2.8.4"
blacksquircleUi = "2.9.0"
googleMaterial = "1.13.0"
libsuCore = "5.2.2"
codeScanner="2.3.2"
[libraries]
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidxActivityKtx" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCoreKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycleViewmodelKtx" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidxRoom" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" }
blacksquircle-ui-editorkit = { group = "com.blacksquircle.ui", name = "editorkit", version.ref = "blacksquircleUi" }
blacksquircle-ui-language-json = { group = "com.blacksquircle.ui", name = "language-json", version.ref = "blacksquircleUi" }
google-material = { group = "com.google.android.material", name = "material", version.ref = "googleMaterial" }
topjohnwu-libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsuCore" }
yuriy-budiyev-code-scanner = { group = "com.github.yuriy-budiyev", name = "code-scanner", version.ref = "codeScanner" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "parcelize" }
google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
# abiId to add versionCode (all: 0) (armeabi-v7a: 1) (arm64-v8a: 2) (x86: 3) (x86_64: 4)
abiId=0
# abiTarget on build time
abiTarget=armeabi-v7a,arm64-v8a,x86,x86_64
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# 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
#
# https://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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: metadata/en-US/changelogs/1004.txt
================================================
- New setting: Tun2socks
- TPROXY: Bypass NTP
================================================
FILE: metadata/en-US/changelogs/1014.txt
================================================
- Skip vpn permission check when tun2socks is disabled
================================================
FILE: metadata/en-US/changelogs/1024.txt
================================================
- Xray-core: v25.10.15
================================================
FILE: metadata/en-US/changelogs/1034.txt
================================================
- Xray-core: v25.12.2
- hev-socks5-tunnel: v2.14.1
================================================
FILE: metadata/en-US/changelogs/104.txt
================================================
- Fresh new design
- Profile CRUD
- Ping active connection
================================================
FILE: metadata/en-US/changelogs/1044.txt
================================================
- Xray-core: v26.2.6
- hev-socks5-tunnel: v2.14.4
================================================
FILE: metadata/en-US/changelogs/1054.txt
================================================
- Fix F-Droid Build Issue
================================================
FILE: metadata/en-US/changelogs/1064.txt
================================================
- Xray-core: v26.4.17
================================================
FILE: metadata/en-US/changelogs/114.txt
================================================
- minSdk 26 (Android 8)
- Vpn service notification
- New dark Material3 theme
- New settings layout
- New settings: Tun Name, Tun Mtu, Ping Address
- New output format for ping result
================================================
FILE: metadata/en-US/changelogs/124.txt
================================================
- Increase profile CardView corner radius
- Close drawer on app info items click
- Profile delete confirm dialog
- Allow selected profile to delete
- Assets manager (download, select, delete) dat files (GeoIP, GeoSite)
- Improve ping, thanks to @maskedeken
================================================
FILE: metadata/en-US/changelogs/134.txt
================================================
- List drag and drop
- SmartTV app launcher support
================================================
FILE: metadata/en-US/changelogs/144.txt
================================================
- libXray: fix coreServer is always nil
================================================
FILE: metadata/en-US/changelogs/154.txt
================================================
- New Settings:
- ++ GeoIP Address
- ++ GeoSite Address
================================================
FILE: metadata/en-US/changelogs/164.txt
================================================
- Settings: [Basic, Advanced] Tabs
- New Settings:
- ++ Ping Timeout
- ++ Enable IPv6
- ++ IPv6 DNS
- ++ Tun IPv4, IPv6 address
================================================
FILE: metadata/en-US/changelogs/174.txt
================================================
- Allow CRUD while VpnService is running except selected profile
- Xray QS Tile
================================================
FILE: metadata/en-US/changelogs/184.txt
================================================
- Fix: selected profile save issue
================================================
FILE: metadata/en-US/changelogs/194.txt
================================================
- Fix ping: socks auth error
- heiher/hev-socks5-tunnel @ 2.6.7
- LogsActivity: Xray-core live logs!
================================================
FILE: metadata/en-US/changelogs/204.txt
================================================
- New Activity: Excluded Apps
================================================
FILE: metadata/en-US/changelogs/214.txt
================================================
- XTLS/Xray-core@v1.8.8
================================================
FILE: metadata/en-US/changelogs/224.txt
================================================
- Json config editor
================================================
FILE: metadata/en-US/changelogs/234.txt
================================================
- Profile: Full width Config Editor
- Profile: Remove save button and add save to menu
================================================
FILE: metadata/en-US/changelogs/244.txt
================================================
- Open Json File With Xray
================================================
FILE: metadata/en-US/changelogs/254.txt
================================================
- Xray-core @ v1.8.9
================================================
FILE: metadata/en-US/changelogs/264.txt
================================================
- Fix qsTile problem on some devices
- Optional notification permission
================================================
FILE: metadata/en-US/changelogs/274.txt
================================================
- QS Tile: Fix app crash on some scenarios
- Fix UI update issue on VPN disconnect by system settings
================================================
FILE: metadata/en-US/changelogs/284.txt
================================================
- Xray-core @ 1.8.10
- hev-socks5-tunnel @ 2.6.8
- Fix: Excluded Apps: deselect issue (thanks to @maskedeken)
================================================
FILE: metadata/en-US/changelogs/294.txt
================================================
- Xray-core @ 1.8.11
================================================
FILE: metadata/en-US/changelogs/304.txt
================================================
- hev-socks5-tunnel @ v2.6.9
================================================
FILE: metadata/en-US/changelogs/314.txt
================================================
- Xray-core @ v1.8.13
================================================
FILE: metadata/en-US/changelogs/324.txt
================================================
- hev-socks5-tunnel @ v2.7.0
================================================
FILE: metadata/en-US/changelogs/334.txt
================================================
- Xray-core @ v1.8.16
================================================
FILE: metadata/en-US/changelogs/344.txt
================================================
- Xray-core @ v1.8.18
- hev-socks5-tunnel @ v2.7.1
================================================
FILE: metadata/en-US/changelogs/354.txt
================================================
- Xray-core @ v1.8.19
================================================
FILE: metadata/en-US/changelogs/364.txt
================================================
- Xray-core @ v1.8.21
================================================
FILE: metadata/en-US/changelogs/374.txt
================================================
- Xray-core @ v1.8.23
================================================
FILE: metadata/en-US/changelogs/384.txt
================================================
- Xray-core @ v1.8.24
- New: add config via url
================================================
FILE: metadata/en-US/changelogs/394.txt
================================================
- hev-socks5-tunnel @ v2.7.4
- New Profile > From Clipboard: now supports import json config from http(s)
- Added deep link: "xray://import-profile/{LINK}"
================================================
FILE: metadata/en-US/changelogs/404.txt
================================================
- Prevent slash character escaping on importing profile via link (Fix #31)
================================================
FILE: metadata/en-US/changelogs/414.txt
================================================
- Take uri fragment as profile name (#32)
================================================
FILE: metadata/en-US/changelogs/424.txt
================================================
- Logs optimizations by @maskedeken (#27)
================================================
FILE: metadata/en-US/changelogs/504.txt
================================================
- Xray-core @ v24.10.31
================================================
FILE: metadata/en-US/changelogs/514.txt
================================================
- Xray-core @ v24.11.5
================================================
FILE: metadata/en-US/changelogs/524.txt
================================================
- Xray-core @ v24.12.15
================================================
FILE: metadata/en-US/changelogs/534.txt
================================================
- Xray-core @ v24.12.18
================================================
FILE: metadata/en-US/changelogs/54.txt
================================================
- New package name `io.github.saeeddev94.xray`
================================================
FILE: metadata/en-US/changelogs/544.txt
================================================
- Xray-core @ v25.1.1
- Improve importing config links
- Android 15: Fix layouts overlap with status bar
================================================
FILE: metadata/en-US/changelogs/604.txt
================================================
- Migrate to kotlin CoroutineScope
- New Activity: LinksActivity
- Ability to add and manage "Json and Subscription" Links
- Manual update profiles via links
================================================
FILE: metadata/en-US/changelogs/614.txt
================================================
- Some optimizations for lower android versions (8-12)
================================================
FILE: metadata/en-US/changelogs/624.txt
================================================
- Some optimizations for lower android versions (8-12)
================================================
FILE: metadata/en-US/changelogs/634.txt
================================================
- Some optimizations for lower android versions (8-12)
================================================
FILE: metadata/en-US/changelogs/64.txt
================================================
- New settings:
- ++ Socks auth
- ++ Bypass LAN
- Set Xray-core version on activity start
================================================
FILE: metadata/en-US/changelogs/644.txt
================================================
- Remove redundant permissions
- New Settings: Boot Auto Start
- Fix notification stop action
================================================
FILE: metadata/en-US/changelogs/654.txt
================================================
- Breaking Change:
- **The app expects a list of configs for a Json link**
- The app doesn't accept HTTP link anymore
- Details: https://github.com/XTLS/Xray-core/discussions/3765
- Only prevent invalid config on start
- Show Invalid Profile error message on save inside a dialog
================================================
FILE: metadata/en-US/changelogs/664.txt
================================================
- Rename Excluded Apps to Apps Routing
- Apps Routing has 2 modes: Exclude, Include
- Default mode is Exclude
================================================
FILE: metadata/en-US/changelogs/674.txt
================================================
- Xray-core @ v25.2.21
- hev-socks5-tunnel @ v2.8.0
================================================
FILE: metadata/en-US/changelogs/684.txt
================================================
- Xray-core @ v25.3.31
- hev-socks5-tunnel @ v2.10.0
- open app on qs tile long press
================================================
FILE: metadata/en-US/changelogs/694.txt
================================================
- Allow user certificates
- Custom "User-Agent" header for Links
================================================
FILE: metadata/en-US/changelogs/704.txt
================================================
- App default User-Agent header
- Change default ping address to `https://www.google.com`
- Xray-core @ v25.4.30
================================================
FILE: metadata/en-US/changelogs/714.txt
================================================
- Drop support for importing json url via clipboard (convert it to json sub instead)
- Ability to add new link (Json, Subscription) via clipboard directly from MainActivity
- Ability to refresh links directly from MainActivity
- Show loading dialog while refreshing links instead of toast
================================================
FILE: metadata/en-US/changelogs/724.txt
================================================
- Shows active links as tabs
- Shows only profiles with active links
================================================
FILE: metadata/en-US/changelogs/734.txt
================================================
- Shows active links as tabs
- Shows only profiles with active links
- New links sort: oldest first
================================================
FILE: metadata/en-US/changelogs/74.txt
================================================
- Split settings: Basic and Advanced
- Advanced settings are hidden by default
================================================
FILE: metadata/en-US/changelogs/744.txt
================================================
- Xray-core @ v25.5.16
================================================
FILE: metadata/en-US/changelogs/754.txt
================================================
- Allow profile change while the app is in start mode
================================================
FILE: metadata/en-US/changelogs/764.txt
================================================
- Update qs tile and notification on profile select
- Save last selected link tab
================================================
FILE: metadata/en-US/changelogs/774.txt
================================================
- Target SDK 36 (Android 16)
- Migrate to Gradle kts and version catalogs
================================================
FILE: metadata/en-US/changelogs/784.txt
================================================
- Reload xray after links refresh
- Show active profile name on new config
================================================
FILE: metadata/en-US/changelogs/794.txt
================================================
- Xray-core: v25.6.8
- New setting: Refresh Links On Open
================================================
FILE: metadata/en-US/changelogs/804.txt
================================================
- New setting: Refresh Links Interval
================================================
FILE: metadata/en-US/changelogs/814.txt
================================================
- Xray-core: v25.8.3
- hev-socks5-tunnel: v2.13.0
================================================
FILE: metadata/en-US/changelogs/824.txt
================================================
- Downgrade AGP to v8.11.1 to fix F-Droid build issue (#74)
================================================
FILE: metadata/en-US/changelogs/834.txt
================================================
- New experimental feature for root users: Transparent Proxy Support!
- TPROXY Auto Connect
- TPROXY Bypass WiFi
================================================
FILE: metadata/en-US/changelogs/844.txt
================================================
- Fix TPROXY Boot Auto Start
- Stop network monitor when TPROXY is OFF
- Stop when TPROXY Auto Connect is disabled
================================================
FILE: metadata/en-US/changelogs/854.txt
================================================
- New menu item: Scan QrCode
================================================
FILE: metadata/en-US/changelogs/864.txt
================================================
- New experimental feature: (Global) Configs (#36)
================================================
FILE: metadata/en-US/changelogs/874.txt
================================================
- Fix Configs crash on fresh installs
================================================
FILE: metadata/en-US/changelogs/884.txt
================================================
- Show Xray-core logs on "Transparent Proxy" mode
================================================
FILE: metadata/en-US/changelogs/894.txt
================================================
- Selected Profile is no longer optional! (For socks proxy just create a simple Xray-core config)
- Share TPROXY for Hotspot and (USB) Tethering
================================================
FILE: metadata/en-US/changelogs/904.txt
================================================
- Selected Profile is no longer optional! (For socks proxy just create a simple Xray-core config)
- Share TPROXY for Hotspot and (USB) Tethering
- Call TProxyService#stop() on TPROXY Settings Change
================================================
FILE: metadata/en-US/changelogs/914.txt
================================================
- Xray-core: v25.8.31
- Fix empty list if selected link become inactive
- Profile: consider (global) "Configs" on validating
- Call TProxyService#stop() on TPROXY Apps Routing Change
================================================
FILE: metadata/en-US/changelogs/924.txt
================================================
- Fix restore selected link tab on open
================================================
FILE: metadata/en-US/changelogs/934.txt
================================================
- Xray-core: v25.9.5
- Customize tun routes (#71)
================================================
FILE: metadata/en-US/changelogs/944.txt
================================================
- Auto install xrayhelper asset
================================================
FILE: metadata/en-US/changelogs/954.txt
================================================
- Xray-core: v25.9.10
================================================
FILE: metadata/en-US/changelogs/964.txt
================================================
- Xray-core: v25.9.11
================================================
FILE: metadata/en-US/changelogs/974.txt
================================================
- Process non-base64 subs
================================================
FILE: metadata/en-US/changelogs/984.txt
================================================
- Fix profiles filter issue on app open
================================================
FILE: metadata/en-US/changelogs/994.txt
================================================
- Optimize build process
================================================
FILE: metadata/en-US/full_description.txt
================================================
This is a simple GUI client for XTLS/Xray-core
================================================
FILE: metadata/en-US/short_description.txt
================================================
Xray GUI Client For Android
================================================
FILE: settings.gradle.kts
================================================
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { setUrl("https://jitpack.io") }
}
}
rootProject.name = "Xray"
include(":app")