Showing preview only (372K chars total). Download the full file or copy to clipboard to get everything.
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
<img src="metadata/en-US/images/featureGraphic.png" alt="App Cover" height="150" />
This is a simple GUI client for <a href="https://github.com/XTLS/Xray-core">XTLS/Xray-core</a>
# Screenshots
<img src="metadata/en-US/images/phoneScreenshots/screenshot-01-home.png" alt="MainActivity" height="666" /><img src="metadata/en-US/images/phoneScreenshots/screenshot-02-assets.png" alt="AssetsActivity" height="666" /><img src="metadata/en-US/images/phoneScreenshots/screenshot-03-settings-basic.png" alt="SettingsActivity: Basic Tab" height="666" /><img src="metadata/en-US/images/phoneScreenshots/screenshot-04-settings-advanced.png" alt="SettingsActivity: Advanced Tab" height="666" />
# APK variants guide
- arm32 => versionCode + 1
- arm64 => versionCode + 2
- x86 => versionCode + 3
- amd64 => versionCode + 4
# Download
[](https://github.com/SaeedDev94/Xray/actions)
<a href="https://github.com/SaeedDev94/Xray/releases"><img src="get-it-on-github.png" alt="Get it on GitHub" height="100" /></a>
<a href="https://f-droid.org/packages/io.github.saeeddev94.xray"><img src="get-it-on-fdroid.png" alt="Get it on F-Droid" height="100" /></a>
================================================
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
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
android:minSdkVersion="34" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".Xray"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:banner="@mipmap/banner"
android:networkSecurityConfig="@xml/network_security_config"
android:label="@string/appName"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity"
android:exported="true"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter android:label="@string/appName">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="xray"
android:host="import-profile" />
</intent-filter>
</activity>
<activity
android:name=".activity.ProfileActivity"
android:parentActivityName=".activity.MainActivity"
android:label="@string/openJson"
android:exported="true">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/json" />
</intent-filter>
</activity>
<activity
android:name=".activity.LinksManagerActivity"
android:theme="@style/TransparentTheme" />
<activity
android:name=".activity.ScannerActivity"
android:parentActivityName=".activity.MainActivity" />
<activity
android:name=".activity.AssetsActivity"
android:parentActivityName=".activity.MainActivity" />
<activity
android:name=".activity.LinksActivity"
android:parentActivityName=".activity.MainActivity" />
<activity
android:name=".activity.LogsActivity"
android:parentActivityName=".activity.MainActivity" />
<activity
android:name=".activity.AppsRoutingActivity"
android:parentActivityName=".activity.MainActivity" />
<activity
android:name=".activity.ConfigsActivity"
android:parentActivityName=".activity.MainActivity" />
<activity
android:name=".activity.SettingsActivity"
android:parentActivityName=".activity.MainActivity" />
<service
android:name=".service.TProxyService"
android:exported="true"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Xray VPN Service" />
</service>
<service
android:name=".service.VpnTileService"
android:exported="true"
android:label="@string/appName"
android:icon="@drawable/vpn_key"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
<receiver
android:name=".receiver.VpnActionReceiver"
android:exported="false">
<intent-filter>
<action android:name="io.github.saeeddev94.xray.VpnStart" />
<action android:name="io.github.saeeddev94.xray.VpnStop" />
<action android:name="io.github.saeeddev94.xray.NewConfig" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
================================================
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<String>
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<String>
get() = sharedPreferences.getStringSet("tproxyBypassWiFi", mutableSetOf<String>())!!
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<AppList>
private lateinit var filtered: MutableList<AppList>
private lateinit var appsRouting: MutableSet<String>
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<ImageView>(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<AppList>()
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<AppList>()
val unselected = ArrayList<AppList>()
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<String, RadioGroup>()
private val configEditor = mutableMapOf<String, TextProcessor>()
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<RadioGroup>(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<TextProcessor>(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<RadioButton>(
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<RecyclerView>(R.id.linksRecyclerView) }
private var links: MutableList<Link> = 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<Profile> {
val list = arrayListOf<Profile>()
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<Profile> {
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<Profile>, newProfiles: List<Profile>
) {
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<String>()
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<Link>
private val profilesRecyclerView by lazy { findViewById<RecyclerView>(R.id.profilesRecyclerView) }
private val profiles = arrayListOf<ProfileList>()
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<Link>): List<Link> {
tabs = list
return listOf(Link(name = "All")) + tabs
}
private fun tabsIndex(list: List<Link>): Int {
return list.indexOfFirst { it.id == settings.selectedLink }.takeIf { it != -1 } ?: 0
}
private fun onNewTabs(value: List<Link>) {
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<ProfileList>) {
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<CodeScannerView>(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<View>) {
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<EditText>(R.id.socksAddress).setText(settings.socksAddress)
basic.findViewById<EditText>(R.id.socksPort).setText(settings.socksPort)
basic.findViewById<EditText>(R.id.socksUsername).setText(settings.socksUsername)
basic.findViewById<EditText>(R.id.socksPassword).setText(settings.socksPassword)
basic.findViewById<EditText>(R.id.geoIpAddress).setText(settings.geoIpAddress)
basic.findViewById<EditText>(R.id.geoSiteAddress).setText(settings.geoSiteAddress)
basic.findViewById<EditText>(R.id.pingAddress).setText(settings.pingAddress)
basic.findViewById<EditText>(R.id.pingTimeout).setText(settings.pingTimeout.toString())
basic.findViewById<EditText>(R.id.refreshLinksInterval)
.setText(settings.refreshLinksInterval.toString())
basic.findViewById<MaterialSwitch>(R.id.bypassLan).isChecked = settings.bypassLan
basic.findViewById<MaterialSwitch>(R.id.enableIpV6).isChecked = settings.enableIpV6
basic.findViewById<MaterialSwitch>(R.id.socksUdp).isChecked = settings.socksUdp
basic.findViewById<MaterialSwitch>(R.id.tun2socks).isChecked = settings.tun2socks
basic.findViewById<MaterialSwitch>(R.id.bootAutoStart).isChecked = settings.bootAutoStart
basic.findViewById<MaterialSwitch>(R.id.refreshLinksOnOpen).isChecked =
settings.refreshLinksOnOpen
}
@SuppressLint("SetTextI18n")
private fun setupAdvanced() {
advanced.findViewById<EditText>(R.id.primaryDns).setText(settings.primaryDns)
advanced.findViewById<EditText>(R.id.secondaryDns).setText(settings.secondaryDns)
advanced.findViewById<EditText>(R.id.primaryDnsV6).setText(settings.primaryDnsV6)
advanced.findViewById<EditText>(R.id.secondaryDnsV6).setText(settings.secondaryDnsV6)
advanced.findViewById<EditText>(R.id.tunName).setText(settings.tunName)
advanced.findViewById<EditText>(R.id.tunMtu).setText(settings.tunMtu.toString())
advanced.findViewById<EditText>(R.id.tunAddress).setText(settings.tunAddress)
advanced.findViewById<EditText>(R.id.tunPrefix).setText(settings.tunPrefix.toString())
advanced.findViewById<EditText>(R.id.tunAddressV6).setText(settings.tunAddressV6)
advanced.findViewById<EditText>(R.id.tunPrefixV6).setText(settings.tunPrefixV6.toString())
advanced.findViewById<EditText>(R.id.hotspotInterface).setText(settings.hotspotInterface)
advanced.findViewById<EditText>(R.id.tetheringInterface).setText(settings.tetheringInterface)
advanced.findViewById<EditText>(R.id.tproxyAddress).setText(settings.tproxyAddress)
advanced.findViewById<EditText>(R.id.tproxyPort).setText(settings.tproxyPort.toString())
advanced.findViewById<EditText>(R.id.tproxyBypassWiFi).setText(settings.tproxyBypassWiFi.joinToString(", "))
advanced.findViewById<MaterialSwitch>(R.id.tproxyAutoConnect).isChecked =
settings.tproxyAutoConnect
advanced.findViewById<MaterialSwitch>(R.id.tproxyHotspot).isChecked =
settings.tproxyHotspot
advanced.findViewById<MaterialSwitch>(R.id.tproxyTethering).isChecked =
settings.tproxyTethering
advanced.findViewById<MaterialSwitch>(R.id.transparentProxy).isChecked =
settings.transparentProxy
advanced.findViewById<LinearLayout>(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<EditText>(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<MaterialSwitch>(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<EditText>(R.id.socksAddress).text.toString()
settings.socksPort = basic.findViewById<EditText>(R.id.socksPort).text.toString()
settings.socksUsername = basic.findViewById<EditText>(R.id.socksUsername).text.toString()
settings.socksPassword = basic.findViewById<EditText>(R.id.socksPassword).text.toString()
settings.geoIpAddress = basic.findViewById<EditText>(R.id.geoIpAddress).text.toString()
settings.geoSiteAddress = basic.findViewById<EditText>(R.id.geoSiteAddress).text.toString()
settings.pingAddress = basic.findViewById<EditText>(R.id.pingAddress).text.toString()
settings.pingTimeout = basic.findViewById<EditText>(R.id.pingTimeout).text.toString().toInt()
settings.refreshLinksInterval =
basic.findViewById<EditText>(R.id.refreshLinksInterval).text.toString().toInt()
settings.bypassLan = basic.findViewById<MaterialSwitch>(R.id.bypassLan).isChecked
val enableIpV6 = basic.findViewById<MaterialSwitch>(R.id.enableIpV6).isChecked
settings.socksUdp = basic.findViewById<MaterialSwitch>(R.id.socksUdp).isChecked
settings.tun2socks = basic.findViewById<MaterialSwitch>(R.id.tun2socks).isChecked
settings.bootAutoStart = basic.findViewById<MaterialSwitch>(R.id.bootAutoStart).isChecked
settings.refreshLinksOnOpen =
basic.findViewById<MaterialSwitch>(R.id.refreshLinksOnOpen).isChecked
/** Advanced */
settings.primaryDns = advanced.findViewById<EditText>(R.id.primaryDns).text.toString()
settings.secondaryDns = advanced.findViewById<EditText>(R.id.secondaryDns).text.toString()
settings.primaryDnsV6 = advanced.findViewById<EditText>(R.id.primaryDnsV6).text.toString()
settings.secondaryDnsV6 = advanced.findViewById<EditText>(R.id.secondaryDnsV6).text.toString()
settings.tunName = advanced.findViewById<EditText>(R.id.tunName).text.toString()
settings.tunMtu = advanced.findViewById<EditText>(R.id.tunMtu).text.toString().toInt()
settings.tunAddress = advanced.findViewById<EditText>(R.id.tunAddress).text.toString()
settings.tunPrefix = advanced.findViewById<EditText>(R.id.tunPrefix).text.toString().toInt()
settings.tunAddressV6 = advanced.findViewById<EditText>(R.id.tunAddressV6).text.toString()
settings.tunPrefixV6 = advanced.findViewById<EditText>(R.id.tunPrefixV6).text.toString().toInt()
val hotspotInterface = advanced.findViewById<EditText>(R.id.hotspotInterface).text.toString()
val tetheringInterface = advanced.findViewById<EditText>(R.id.tetheringInterface).text.toString()
val tproxyAddress = advanced.findViewById<EditText>(R.id.tproxyAddress).text.toString()
val tproxyPort = advanced.findViewById<EditText>(R.id.tproxyPort).text.toString().toInt()
val tproxyBypassWiFi = advanced.findViewById<EditText>(R.id.tproxyBypassWiFi).text
.toString()
.split(",")
.map { it.trim() }
.filter { it.isNotBlank() }
.toSet()
val tproxyAutoConnect = advanced.findViewById<MaterialSwitch>(R.id.tproxyAutoConnect).isChecked
val tproxyHotspot = advanced.findViewById<MaterialSwitch>(R.id.tproxyHotspot).isChecked
val tproxyTethering = advanced.findViewById<MaterialSwitch>(R.id.tproxyTethering).isChecked
val transparentProxy = advanced.findViewById<MaterialSwitch>(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<AppList>,
private var appsRouting: MutableSet<String>,
) : RecyclerView.Adapter<AppsRoutingAdapter.ViewHolder>() {
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<String>,
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<Link, LinkAdapter.LinkHolder>(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<CardView>(R.id.linkCard)
private val name = view.findViewById<TextView>(R.id.linkName)
private val type = view.findViewById<TextView>(R.id.linkType)
private val edit = view.findViewById<LinearLayout>(R.id.linkEdit)
private val delete = view.findViewById<LinearLayout>(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<Link>() {
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<ProfileList>,
private val profileSelect: (index: Int, profile: ProfileList) -> Unit,
private val profileEdit: (profile: ProfileList) -> Unit,
private val profileDelete: (profile: ProfileList) -> Unit,
) : RecyclerView.Adapter<ProfileAdapter.ViewHolder>(), 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<String>,
private var layouts: List<Int>,
private var callback: ViewsReady,
) : PagerAdapter() {
private val views: MutableList<View> = 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<View>)
}
}
================================================
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<SearchAutoComplete>(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<List<Link>>
@Query("SELECT * FROM links WHERE is_active = 1 ORDER BY id ASC")
fun tabs(): Flow<List<Link>>
@Query("SELECT * FROM links WHERE is_active = 1 ORDER BY id ASC")
suspend fun activeLinks(): List<Link>
@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<List<ProfileList>>
@Query("SELECT * FROM profiles WHERE link_id = :linkId ORDER BY `index` DESC")
suspend fun linkProfiles(linkId: Long): List<Profile>
@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<RadioGroup>(R.id.typeRadioGroup)
val nameEditText = layout.findViewById<EditText>(R.id.nameEditText)
val addressEditText = layout.findViewById<EditText>(R.id.addressEditText)
val userAgentEditText = layout.findViewById<EditText>(R.id.userAgentEditText)
val isActiveSwitch = layout.findViewById<MaterialSwitch>(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<RadioButton>(
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 <T> getParcelable(intent: Intent, name: String, clazz: Class<T>): 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 == "<unknown 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<String>()
val excludedInterfaces = arrayListOf<String>()
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<Link> {
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<Profile> {
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("")
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
SYMBOL INDEX (12 symbols across 5 files)
FILE: XrayCore/lib/core.go
function Server (line 11) | func Server(config string) (*core.Instance, error) {
function Start (line 24) | func Start(dir string, config string) (err error) {
function Stop (line 36) | func Stop() error {
function Version (line 47) | func Version() string {
FILE: XrayCore/lib/env.go
function SetEnv (line 5) | func SetEnv(dir string) {
FILE: XrayCore/lib/error.go
function WrapError (line 3) | func WrapError(err error) string {
FILE: XrayCore/lib/test.go
function Test (line 3) | func Test(dir string, config string) error {
FILE: XrayCore/main.go
function Test (line 10) | func Test(dir string, config string) string {
function Start (line 15) | func Start(dir string, config string) string {
function Stop (line 20) | func Stop() string {
function Version (line 25) | func Version() string {
function Json (line 29) | func Json(link string) string {
Condensed preview — 221 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (357K chars).
[
{
"path": ".github/workflows/release.yml",
"chars": 4991,
"preview": "name: Release CI\n\non:\n push:\n tags:\n - '*'\n\njobs:\n build-arm32:\n runs-on: ubuntu-latest\n steps:\n - "
},
{
"path": ".gitignore",
"chars": 239,
"preview": "*.iml\n.idea\n.kotlin\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.i"
},
{
"path": ".gitmodules",
"chars": 447,
"preview": "[submodule \"app/src/main/jni/hev-socks5-tunnel\"]\n\tpath = app/src/main/jni/hev-socks5-tunnel\n\turl = https://github.com/he"
},
{
"path": "Dockerfile",
"chars": 140,
"preview": "FROM debian:trixie\n\nENV LANG=C.UTF-8 \\\n DEBIAN_FRONTEND=noninteractive\n\nCOPY build-xray.sh /build-xray.sh\n\nENTRYPOINT"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2024 SaeedDev94\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 1240,
"preview": "# Xray\n<img src=\"metadata/en-US/images/featureGraphic.png\" alt=\"App Cover\" height=\"150\" /> \nThis is a simple GUI client"
},
{
"path": "XrayCore/go.mod",
"chars": 2439,
"preview": "module XrayCore\n\ngo 1.26.2\n\nreplace github.com/xtls/xray-core => ./Xray-core\n\nreplace github.com/xtls/libxray => ./libXr"
},
{
"path": "XrayCore/go.sum",
"chars": 12244,
"preview": "github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=\ngithub.com/andybalholm/brotli v1.0."
},
{
"path": "XrayCore/lib/core.go",
"chars": 827,
"preview": "package lib\n\nimport (\n\t\"github.com/xtls/xray-core/common/cmdarg\"\n\t\"github.com/xtls/xray-core/core\"\n\t_ \"github.com/xtls/x"
},
{
"path": "XrayCore/lib/env.go",
"chars": 93,
"preview": "package lib\n\nimport \"os\"\n\nfunc SetEnv(dir string) {\n\tos.Setenv(\"xray.location.asset\", dir)\n}\n"
},
{
"path": "XrayCore/lib/error.go",
"chars": 102,
"preview": "package lib\n\nfunc WrapError(err error) string {\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\treturn \"\"\n}\n"
},
{
"path": "XrayCore/lib/test.go",
"chars": 144,
"preview": "package lib\n\nfunc Test(dir string, config string) error {\n\tSetEnv(dir)\n\t_, err := Server(config)\n\tif err != nil {\n\t\tretu"
},
{
"path": "XrayCore/main.go",
"chars": 671,
"preview": "package XrayCore\n\nimport (\n\t\"github.com/xtls/xray-core/infra/conf\"\n\t\"github.com/xtls/libxray/nodep\"\n\t\"github.com/xtls/li"
},
{
"path": "app/.gitignore",
"chars": 16,
"preview": "/build\n/release\n"
},
{
"path": "app/build.gradle.kts",
"chars": 2596,
"preview": "plugins {\n alias(libs.plugins.android.application)\n alias(libs.plugins.kotlin.parcelize)\n alias(libs.plugins.go"
},
{
"path": "app/libs/.gitignore",
"chars": 12,
"preview": "*.aar\n*.jar\n"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 6030,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmln"
},
{
"path": "app/src/main/assets/.gitignore",
"chars": 11,
"preview": "xrayhelper\n"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/Settings.kt",
"chars": 8943,
"preview": "package io.github.saeeddev94.xray\n\nimport android.content.Context\nimport androidx.core.content.edit\nimport java.io.File\n"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/Xray.kt",
"chars": 637,
"preview": "package io.github.saeeddev94.xray\n\nimport android.app.Application\nimport io.github.saeeddev94.xray.database.XrayDatabase"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/AppsRoutingActivity.kt",
"chars": 7198,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport androi"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/AssetsActivity.kt",
"chars": 7409,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport android.annotation.SuppressLint\nimport android.net.Uri\nimport android"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/ConfigsActivity.kt",
"chars": 6894,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.MenuIt"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/LinksActivity.kt",
"chars": 2925,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.MenuIt"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/LinksManagerActivity.kt",
"chars": 7862,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport android.app.Dialog\nimport android.content.Context\nimport android.cont"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/LogsActivity.kt",
"chars": 5526,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport android.annotation.SuppressLint\nimport android.content.ClipData\nimpor"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/MainActivity.kt",
"chars": 16997,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport XrayCore.XrayCore\nimport android.content.BroadcastReceiver\nimport and"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/ProfileActivity.kt",
"chars": 6915,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport XrayCore.XrayCore\nimport android.content.Context\nimport android.conte"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/ScannerActivity.kt",
"chars": 2190,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.widget"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/activity/SettingsActivity.kt",
"chars": 12504,
"preview": "package io.github.saeeddev94.xray.activity\n\nimport android.annotation.SuppressLint\nimport android.os.Bundle\nimport andro"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/adapter/AppsRoutingAdapter.kt",
"chars": 2102,
"preview": "package io.github.saeeddev94.xray.adapter\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport andr"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/adapter/ConfigAdapter.kt",
"chars": 1100,
"preview": "package io.github.saeeddev94.xray.adapter\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport andr"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/adapter/LinkAdapter.kt",
"chars": 2339,
"preview": "package io.github.saeeddev94.xray.adapter\n\nimport android.content.res.ColorStateList\nimport android.view.LayoutInflater\n"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/adapter/ProfileAdapter.kt",
"chars": 3671,
"preview": "package io.github.saeeddev94.xray.adapter\n\nimport android.content.res.ColorStateList\nimport android.view.LayoutInflater\n"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/adapter/SettingAdapter.kt",
"chars": 1252,
"preview": "package io.github.saeeddev94.xray.adapter\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport andr"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/component/EmptySubmitSearchView.kt",
"chars": 983,
"preview": "package io.github.saeeddev94.xray.component\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimpor"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/database/Config.kt",
"chars": 1404,
"preview": "package io.github.saeeddev94.xray.database\n\nimport android.os.Parcelable\nimport androidx.room.ColumnInfo\nimport androidx"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/database/ConfigDao.kt",
"chars": 366,
"preview": "package io.github.saeeddev94.xray.database\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.Qu"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/database/Link.kt",
"chars": 1020,
"preview": "package io.github.saeeddev94.xray.database\n\nimport android.os.Parcelable\nimport androidx.room.ColumnInfo\nimport androidx"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/database/LinkDao.kt",
"chars": 693,
"preview": "package io.github.saeeddev94.xray.database\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.In"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/database/Profile.kt",
"chars": 1057,
"preview": "package io.github.saeeddev94.xray.database\n\nimport android.os.Parcelable\nimport androidx.room.ColumnInfo\nimport androidx"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/database/ProfileDao.kt",
"chars": 2562,
"preview": "package io.github.saeeddev94.xray.database\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.In"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/database/XrayDatabase.kt",
"chars": 4468,
"preview": "package io.github.saeeddev94.xray.database\n\nimport android.content.Context\nimport androidx.room.Database\nimport androidx"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/dto/AppList.kt",
"chars": 185,
"preview": "package io.github.saeeddev94.xray.dto\n\nimport android.graphics.drawable.Drawable\n\ndata class AppList(\n var appIcon: D"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/dto/ProfileList.kt",
"chars": 146,
"preview": "package io.github.saeeddev94.xray.dto\n\ndata class ProfileList(\n var id: Long,\n var index: Int,\n var name: Strin"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/dto/XrayConfig.kt",
"chars": 107,
"preview": "package io.github.saeeddev94.xray.dto\n\ndata class XrayConfig(\n val dir: String,\n val file: String,\n)\n"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/fragment/LinkFormFragment.kt",
"chars": 4000,
"preview": "package io.github.saeeddev94.xray.fragment\n\nimport android.app.Dialog\nimport android.content.DialogInterface\nimport andr"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/ConfigHelper.kt",
"chars": 2201,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport io.github.saeeddev94.xray.Settings\nimport io.github.saeeddev94.xray.dat"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/DownloadHelper.kt",
"chars": 2488,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/FileHelper.kt",
"chars": 348,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport java.io.File\n\nclass FileHelper {\n\n companion object {\n fun cr"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/HttpHelper.kt",
"chars": 4302,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport io.github.saeeddev94.xray.BuildConfig\nimport io.github.saeeddev94.xray."
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/IntentHelper.kt",
"chars": 501,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport android.content.Intent\nimport android.os.Build\n\nclass IntentHelper {\n "
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/JsonHelper.kt",
"chars": 2313,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport org.json.JSONArray\nimport org.json.JSONObject\n\nclass JsonHelper {\n c"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/LinkHelper.kt",
"chars": 6411,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport XrayCore.XrayCore\nimport android.util.Base64\nimport io.github.saeeddev9"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/NetworkStateHelper.kt",
"chars": 2163,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport com.topjohnwu.superuser.Shell\nimport io.github.saeeddev94.xray.service."
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/ProfileTouchHelper.kt",
"chars": 1517,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclervi"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/helper/TransparentProxyHelper.kt",
"chars": 5140,
"preview": "package io.github.saeeddev94.xray.helper\n\nimport android.content.Context\nimport com.topjohnwu.superuser.Shell\nimport io."
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/receiver/BootReceiver.kt",
"chars": 2118,
"preview": "package io.github.saeeddev94.xray.receiver\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimpo"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/receiver/VpnActionReceiver.kt",
"chars": 1048,
"preview": "package io.github.saeeddev94.xray.receiver\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimpo"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/repository/ConfigRepository.kt",
"chars": 462,
"preview": "package io.github.saeeddev94.xray.repository\n\nimport io.github.saeeddev94.xray.database.Config\nimport io.github.saeeddev"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/repository/LinkRepository.kt",
"chars": 565,
"preview": "package io.github.saeeddev94.xray.repository\n\nimport io.github.saeeddev94.xray.database.Link\nimport io.github.saeeddev94"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/repository/ProfileRepository.kt",
"chars": 1036,
"preview": "package io.github.saeeddev94.xray.repository\n\nimport io.github.saeeddev94.xray.database.Profile\nimport io.github.saeedde"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/service/TProxyService.kt",
"chars": 14440,
"preview": "package io.github.saeeddev94.xray.service\n\nimport XrayCore.XrayCore\nimport android.annotation.SuppressLint\nimport androi"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/service/VpnTileService.kt",
"chars": 2529,
"preview": "package io.github.saeeddev94.xray.service\n\nimport android.content.ComponentName\nimport android.content.Intent\nimport and"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/viewmodel/ConfigViewModel.kt",
"chars": 603,
"preview": "package io.github.saeeddev94.xray.viewmodel\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidViewModel\ni"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/viewmodel/LinkViewModel.kt",
"chars": 897,
"preview": "package io.github.saeeddev94.xray.viewmodel\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidViewModel\ni"
},
{
"path": "app/src/main/java/io/github/saeeddev94/xray/viewmodel/ProfileViewModel.kt",
"chars": 2140,
"preview": "package io.github.saeeddev94.xray.viewmodel\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidViewModel\ni"
},
{
"path": "app/src/main/jni/Android.mk",
"chars": 640,
"preview": "# Copyright (C) 2023 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": "app/src/main/jni/Application.mk",
"chars": 877,
"preview": "# Copyright (C) 2023 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": "app/src/main/res/drawable/baseline_adb.xml",
"chars": 959,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_add.xml",
"chars": 360,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_alt_route.xml",
"chars": 872,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_config.xml",
"chars": 1148,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_content_copy.xml",
"chars": 1003,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_delete.xml",
"chars": 408,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_done.xml",
"chars": 384,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_download.xml",
"chars": 617,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_edit.xml",
"chars": 491,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_file_open.xml",
"chars": 783,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_folder_open.xml",
"chars": 791,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_link.xml",
"chars": 751,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_refresh.xml",
"chars": 652,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_settings.xml",
"chars": 1229,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_vpn_key.xml",
"chars": 1060,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/baseline_vpn_lock.xml",
"chars": 1410,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_xray.xml",
"chars": 1284,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"108dp\"\n android:height=\"108dp\"\n"
},
{
"path": "app/src/main/res/drawable-v26/ic_launcher.xml",
"chars": 517,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <bac"
},
{
"path": "app/src/main/res/layout/activity_apps_routing.xml",
"chars": 1427,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmln"
},
{
"path": "app/src/main/res/layout/activity_assets.xml",
"chars": 19365,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/layout/activity_configs.xml",
"chars": 1375,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/layout/activity_links.xml",
"chars": 1004,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/layout/activity_logs.xml",
"chars": 1216,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/layout/activity_main.xml",
"chars": 3480,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.drawerlayout.widget.DrawerLayout xmlns:android=\"http://schemas.android."
},
{
"path": "app/src/main/res/layout/activity_profile.xml",
"chars": 2509,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/layout/activity_scanner.xml",
"chars": 1101,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n x"
},
{
"path": "app/src/main/res/layout/activity_settings.xml",
"chars": 1401,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/layout/item_recycler_exclude.xml",
"chars": 1816,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/layout/item_recycler_main.xml",
"chars": 3527,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.cardview.widget.CardView\n xmlns:android=\"http://schemas.android.com/"
},
{
"path": "app/src/main/res/layout/layout_link_form.xml",
"chars": 2325,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/layout/layout_link_item.xml",
"chars": 4166,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmln"
},
{
"path": "app/src/main/res/layout/layout_tun_routes.xml",
"chars": 590,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmln"
},
{
"path": "app/src/main/res/layout/loading_dialog.xml",
"chars": 610,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n andr"
},
{
"path": "app/src/main/res/layout/tab_advanced_settings.xml",
"chars": 15496,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.core.widget.NestedScrollView\n xmlns:android=\"http://schemas.android."
},
{
"path": "app/src/main/res/layout/tab_basic_settings.xml",
"chars": 9632,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.core.widget.NestedScrollView\n xmlns:android=\"http://schemas.android."
},
{
"path": "app/src/main/res/layout/tab_config.xml",
"chars": 912,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/menu/menu_apps_routing.xml",
"chars": 850,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/menu/menu_configs.xml",
"chars": 345,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/menu/menu_drawer.xml",
"chars": 1783,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <group andr"
},
{
"path": "app/src/main/res/menu/menu_links.xml",
"chars": 517,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/menu/menu_logs.xml",
"chars": 523,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/menu/menu_main.xml",
"chars": 884,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/menu/menu_profile.xml",
"chars": 345,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/menu/menu_settings.xml",
"chars": 347,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/values/array.xml",
"chars": 1170,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <string-array name=\"publicIpAddresses\">\n <item>0.0.0.0/5</"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 205,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"primaryColor\">#009966</color>\n <color name=\"btnCo"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 5006,
"preview": "<resources>\n <string name=\"appName\">Xray</string>\n <string name=\"noValue\">-</string>\n <string name=\"drawerOpen\""
},
{
"path": "app/src/main/res/values/themes.xml",
"chars": 620,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <style name=\"AppTheme\" parent=\"Theme.Material3.Dark.NoActionBar\">"
},
{
"path": "app/src/main/res/xml/network_security_config.xml",
"chars": 398,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config\n xmlns:tools=\"http://schemas.android.com/tools\">\n "
},
{
"path": "app/versionCode.txt",
"chars": 5,
"preview": "1060\n"
},
{
"path": "build-xray.sh",
"chars": 2255,
"preview": "#!/bin/bash\n\n# Update repo\napt-get update\napt-get install -y ca-certificates\necho \"deb https://deb.debian.org/debian for"
},
{
"path": "build.gradle.kts",
"chars": 268,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nplugins {\n alias("
},
{
"path": "buildGo.sh",
"chars": 376,
"preview": "#!/bin/bash\n\n# Set vars\nexport GOROOT=\"$(realpath go-root)\"\nexport GOPATH=\"$(realpath go-path)\"\n\n# Set path\nexport PATH="
},
{
"path": "buildXrayCore.sh",
"chars": 1291,
"preview": "#!/bin/bash\n\nTARGET=\"$1\"\nREFRESH=\"$2\"\nSETUP=\"$3\"\nARCHS=(arm arm64 386 amd64)\nDEST=\"../app/libs\"\n\nis_in_array() {\n local"
},
{
"path": "buildXrayHelper.sh",
"chars": 746,
"preview": "#!/bin/bash\n\nTARGET=\"$1\"\nARCHS=(arm arm64 386 amd64)\nDEST=\"../app/src/main/assets\"\n\nis_in_array() {\n local value=\"$1\"\n "
},
{
"path": "gradle/libs.versions.toml",
"chars": 1919,
"preview": "[versions]\nagp = \"9.1.1\"\nparcelize=\"2.3.20\"\nksp = \"2.3.6\"\nandroidxActivityKtx = \"1.13.0\"\nandroidxCoreKtx = \"1.18.0\"\nandr"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 252,
"preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
},
{
"path": "gradle.properties",
"chars": 1525,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "gradlew",
"chars": 8595,
"preview": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\")"
},
{
"path": "gradlew.bat",
"chars": 2896,
"preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
},
{
"path": "metadata/en-US/changelogs/1004.txt",
"chars": 46,
"preview": "- New setting: Tun2socks\n- TPROXY: Bypass NTP\n"
},
{
"path": "metadata/en-US/changelogs/1014.txt",
"chars": 55,
"preview": "- Skip vpn permission check when tun2socks is disabled\n"
},
{
"path": "metadata/en-US/changelogs/1024.txt",
"chars": 23,
"preview": "- Xray-core: v25.10.15\n"
},
{
"path": "metadata/en-US/changelogs/1034.txt",
"chars": 51,
"preview": "- Xray-core: v25.12.2\n- hev-socks5-tunnel: v2.14.1\n"
},
{
"path": "metadata/en-US/changelogs/104.txt",
"chars": 59,
"preview": "- Fresh new design\n- Profile CRUD\n- Ping active connection\n"
},
{
"path": "metadata/en-US/changelogs/1044.txt",
"chars": 50,
"preview": "- Xray-core: v26.2.6\n- hev-socks5-tunnel: v2.14.4\n"
},
{
"path": "metadata/en-US/changelogs/1054.txt",
"chars": 26,
"preview": "- Fix F-Droid Build Issue\n"
},
{
"path": "metadata/en-US/changelogs/1064.txt",
"chars": 22,
"preview": "- Xray-core: v26.4.17\n"
},
{
"path": "metadata/en-US/changelogs/114.txt",
"chars": 184,
"preview": "- minSdk 26 (Android 8)\n- Vpn service notification\n- New dark Material3 theme\n- New settings layout\n- New settings: Tun "
},
{
"path": "metadata/en-US/changelogs/124.txt",
"chars": 257,
"preview": "- Increase profile CardView corner radius\n- Close drawer on app info items click\n- Profile delete confirm dialog\n- Allow"
},
{
"path": "metadata/en-US/changelogs/134.txt",
"chars": 52,
"preview": "- List drag and drop\n- SmartTV app launcher support\n"
},
{
"path": "metadata/en-US/changelogs/144.txt",
"chars": 40,
"preview": "- libXray: fix coreServer is always nil\n"
},
{
"path": "metadata/en-US/changelogs/154.txt",
"chars": 56,
"preview": "- New Settings:\n- ++ GeoIP Address\n- ++ GeoSite Address\n"
},
{
"path": "metadata/en-US/changelogs/164.txt",
"chars": 128,
"preview": "- Settings: [Basic, Advanced] Tabs\n- New Settings:\n- ++ Ping Timeout\n- ++ Enable IPv6\n- ++ IPv6 DNS\n- ++ Tun IPv4, IPv6 "
},
{
"path": "metadata/en-US/changelogs/174.txt",
"chars": 80,
"preview": "- Allow CRUD while VpnService is running except selected profile\n- Xray QS Tile\n"
},
{
"path": "metadata/en-US/changelogs/184.txt",
"chars": 35,
"preview": "- Fix: selected profile save issue\n"
},
{
"path": "metadata/en-US/changelogs/194.txt",
"chars": 101,
"preview": "- Fix ping: socks auth error\n- heiher/hev-socks5-tunnel @ 2.6.7\n- LogsActivity: Xray-core live logs!\n"
},
{
"path": "metadata/en-US/changelogs/204.txt",
"chars": 30,
"preview": "- New Activity: Excluded Apps\n"
},
{
"path": "metadata/en-US/changelogs/214.txt",
"chars": 24,
"preview": "- XTLS/Xray-core@v1.8.8\n"
},
{
"path": "metadata/en-US/changelogs/224.txt",
"chars": 21,
"preview": "- Json config editor\n"
},
{
"path": "metadata/en-US/changelogs/234.txt",
"chars": 87,
"preview": "- Profile: Full width Config Editor\n- Profile: Remove save button and add save to menu\n"
},
{
"path": "metadata/en-US/changelogs/244.txt",
"chars": 27,
"preview": "- Open Json File With Xray\n"
},
{
"path": "metadata/en-US/changelogs/254.txt",
"chars": 21,
"preview": "- Xray-core @ v1.8.9\n"
},
{
"path": "metadata/en-US/changelogs/264.txt",
"chars": 72,
"preview": "- Fix qsTile problem on some devices\n- Optional notification permission\n"
},
{
"path": "metadata/en-US/changelogs/274.txt",
"chars": 102,
"preview": "- QS Tile: Fix app crash on some scenarios\n- Fix UI update issue on VPN disconnect by system settings\n"
},
{
"path": "metadata/en-US/changelogs/284.txt",
"chars": 110,
"preview": "- Xray-core @ 1.8.10\n- hev-socks5-tunnel @ 2.6.8\n- Fix: Excluded Apps: deselect issue (thanks to @maskedeken)\n"
},
{
"path": "metadata/en-US/changelogs/294.txt",
"chars": 21,
"preview": "- Xray-core @ 1.8.11\n"
},
{
"path": "metadata/en-US/changelogs/304.txt",
"chars": 29,
"preview": "- hev-socks5-tunnel @ v2.6.9\n"
},
{
"path": "metadata/en-US/changelogs/314.txt",
"chars": 22,
"preview": "- Xray-core @ v1.8.13\n"
},
{
"path": "metadata/en-US/changelogs/324.txt",
"chars": 29,
"preview": "- hev-socks5-tunnel @ v2.7.0\n"
},
{
"path": "metadata/en-US/changelogs/334.txt",
"chars": 22,
"preview": "- Xray-core @ v1.8.16\n"
},
{
"path": "metadata/en-US/changelogs/344.txt",
"chars": 51,
"preview": "- Xray-core @ v1.8.18\n- hev-socks5-tunnel @ v2.7.1\n"
},
{
"path": "metadata/en-US/changelogs/354.txt",
"chars": 22,
"preview": "- Xray-core @ v1.8.19\n"
},
{
"path": "metadata/en-US/changelogs/364.txt",
"chars": 22,
"preview": "- Xray-core @ v1.8.21\n"
},
{
"path": "metadata/en-US/changelogs/374.txt",
"chars": 22,
"preview": "- Xray-core @ v1.8.23\n"
},
{
"path": "metadata/en-US/changelogs/384.txt",
"chars": 48,
"preview": "- Xray-core @ v1.8.24\n- New: add config via url\n"
},
{
"path": "metadata/en-US/changelogs/394.txt",
"chars": 156,
"preview": "- hev-socks5-tunnel @ v2.7.4\n- New Profile > From Clipboard: now supports import json config from http(s)\n- Added deep l"
},
{
"path": "metadata/en-US/changelogs/404.txt",
"chars": 75,
"preview": "- Prevent slash character escaping on importing profile via link (Fix #31)\n"
},
{
"path": "metadata/en-US/changelogs/414.txt",
"chars": 42,
"preview": "- Take uri fragment as profile name (#32)\n"
},
{
"path": "metadata/en-US/changelogs/424.txt",
"chars": 42,
"preview": "- Logs optimizations by @maskedeken (#27)\n"
},
{
"path": "metadata/en-US/changelogs/504.txt",
"chars": 24,
"preview": "- Xray-core @ v24.10.31\n"
},
{
"path": "metadata/en-US/changelogs/514.txt",
"chars": 23,
"preview": "- Xray-core @ v24.11.5\n"
},
{
"path": "metadata/en-US/changelogs/524.txt",
"chars": 24,
"preview": "- Xray-core @ v24.12.15\n"
},
{
"path": "metadata/en-US/changelogs/534.txt",
"chars": 24,
"preview": "- Xray-core @ v24.12.18\n"
},
{
"path": "metadata/en-US/changelogs/54.txt",
"chars": 47,
"preview": "- New package name `io.github.saeeddev94.xray`\n"
},
{
"path": "metadata/en-US/changelogs/544.txt",
"chars": 105,
"preview": "- Xray-core @ v25.1.1\n- Improve importing config links\n- Android 15: Fix layouts overlap with status bar\n"
},
{
"path": "metadata/en-US/changelogs/604.txt",
"chars": 158,
"preview": "- Migrate to kotlin CoroutineScope\n- New Activity: LinksActivity\n- Ability to add and manage \"Json and Subscription\" Lin"
},
{
"path": "metadata/en-US/changelogs/614.txt",
"chars": 55,
"preview": "- Some optimizations for lower android versions (8-12)\n"
},
{
"path": "metadata/en-US/changelogs/624.txt",
"chars": 55,
"preview": "- Some optimizations for lower android versions (8-12)\n"
},
{
"path": "metadata/en-US/changelogs/634.txt",
"chars": 55,
"preview": "- Some optimizations for lower android versions (8-12)\n"
},
{
"path": "metadata/en-US/changelogs/64.txt",
"chars": 90,
"preview": "- New settings:\n- ++ Socks auth\n- ++ Bypass LAN\n- Set Xray-core version on activity start\n"
},
{
"path": "metadata/en-US/changelogs/644.txt",
"chars": 94,
"preview": "- Remove redundant permissions\n- New Settings: Boot Auto Start\n- Fix notification stop action\n"
},
{
"path": "metadata/en-US/changelogs/654.txt",
"chars": 280,
"preview": "- Breaking Change:\n- **The app expects a list of configs for a Json link**\n- The app doesn't accept HTTP link anymore\n- "
},
{
"path": "metadata/en-US/changelogs/664.txt",
"chars": 110,
"preview": "- Rename Excluded Apps to Apps Routing\n- Apps Routing has 2 modes: Exclude, Include\n- Default mode is Exclude\n"
},
{
"path": "metadata/en-US/changelogs/674.txt",
"chars": 52,
"preview": "- Xray-core @ v25.2.21\n- hev-socks5-tunnel @ v2.8.0\n"
},
{
"path": "metadata/en-US/changelogs/684.txt",
"chars": 86,
"preview": "- Xray-core @ v25.3.31\n- hev-socks5-tunnel @ v2.10.0\n- open app on qs tile long press\n"
},
{
"path": "metadata/en-US/changelogs/694.txt",
"chars": 65,
"preview": "- Allow user certificates\n- Custom \"User-Agent\" header for Links\n"
},
{
"path": "metadata/en-US/changelogs/704.txt",
"chars": 113,
"preview": "- App default User-Agent header\n- Change default ping address to `https://www.google.com`\n- Xray-core @ v25.4.30\n"
},
{
"path": "metadata/en-US/changelogs/714.txt",
"chars": 289,
"preview": "- Drop support for importing json url via clipboard (convert it to json sub instead)\n- Ability to add new link (Json, Su"
},
{
"path": "metadata/en-US/changelogs/724.txt",
"chars": 69,
"preview": "- Shows active links as tabs\n- Shows only profiles with active links\n"
},
{
"path": "metadata/en-US/changelogs/734.txt",
"chars": 100,
"preview": "- Shows active links as tabs\n- Shows only profiles with active links\n- New links sort: oldest first\n"
},
{
"path": "metadata/en-US/changelogs/74.txt",
"chars": 79,
"preview": "- Split settings: Basic and Advanced\n- Advanced settings are hidden by default\n"
},
{
"path": "metadata/en-US/changelogs/744.txt",
"chars": 23,
"preview": "- Xray-core @ v25.5.16\n"
},
{
"path": "metadata/en-US/changelogs/754.txt",
"chars": 54,
"preview": "- Allow profile change while the app is in start mode\n"
},
{
"path": "metadata/en-US/changelogs/764.txt",
"chars": 82,
"preview": "- Update qs tile and notification on profile select\n- Save last selected link tab\n"
},
{
"path": "metadata/en-US/changelogs/774.txt",
"chars": 74,
"preview": "- Target SDK 36 (Android 16)\n- Migrate to Gradle kts and version catalogs\n"
},
{
"path": "metadata/en-US/changelogs/784.txt",
"chars": 75,
"preview": "- Reload xray after links refresh\n- Show active profile name on new config\n"
},
{
"path": "metadata/en-US/changelogs/794.txt",
"chars": 58,
"preview": "- Xray-core: v25.6.8\n- New setting: Refresh Links On Open\n"
},
{
"path": "metadata/en-US/changelogs/804.txt",
"chars": 38,
"preview": "- New setting: Refresh Links Interval\n"
},
{
"path": "metadata/en-US/changelogs/814.txt",
"chars": 50,
"preview": "- Xray-core: v25.8.3\n- hev-socks5-tunnel: v2.13.0\n"
},
{
"path": "metadata/en-US/changelogs/824.txt",
"chars": 60,
"preview": "- Downgrade AGP to v8.11.1 to fix F-Droid build issue (#74)\n"
}
]
// ... and 21 more files (download for full content)
About this extraction
This page contains the full source code of the SaeedDev94/Xray GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 221 files (320.1 KB), approximately 85.8k tokens, and a symbol index with 12 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.