Full Code of SaeedDev94/Xray for AI

master 69bf223e8c24 cached
221 files
320.1 KB
85.8k tokens
12 symbols
1 requests
Download .txt
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
[![Release CI](https://github.com/SaeedDev94/Xray/actions/workflows/release.yml/badge.svg)](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("")
            
Download .txt
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
Download .txt
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.

Copied to clipboard!