├── .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 ├── proguard-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── saeeddev94 │ │ │ └── xray │ │ │ ├── Settings.kt │ │ │ ├── Xray.kt │ │ │ ├── activity │ │ │ ├── AppsRoutingActivity.kt │ │ │ ├── AssetsActivity.kt │ │ │ ├── LinksActivity.kt │ │ │ ├── LinksManagerActivity.kt │ │ │ ├── LogsActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── ProfileActivity.kt │ │ │ └── SettingsActivity.kt │ │ │ ├── adapter │ │ │ ├── AppsRoutingAdapter.kt │ │ │ ├── LinkAdapter.kt │ │ │ ├── ProfileAdapter.kt │ │ │ └── SettingAdapter.kt │ │ │ ├── component │ │ │ └── EmptySubmitSearchView.kt │ │ │ ├── database │ │ │ ├── Link.kt │ │ │ ├── LinkDao.kt │ │ │ ├── Profile.kt │ │ │ ├── ProfileDao.kt │ │ │ └── XrayDatabase.kt │ │ │ ├── dto │ │ │ ├── AppList.kt │ │ │ ├── ProfileList.kt │ │ │ └── XrayConfig.kt │ │ │ ├── fragment │ │ │ └── LinkFormFragment.kt │ │ │ ├── helper │ │ │ ├── DownloadHelper.kt │ │ │ ├── FileHelper.kt │ │ │ ├── HttpHelper.kt │ │ │ ├── IntentHelper.kt │ │ │ ├── LinkHelper.kt │ │ │ └── ProfileTouchHelper.kt │ │ │ ├── receiver │ │ │ ├── BootReceiver.kt │ │ │ └── VpnActionReceiver.kt │ │ │ ├── repository │ │ │ ├── LinkRepository.kt │ │ │ └── ProfileRepository.kt │ │ │ ├── service │ │ │ ├── TProxyService.kt │ │ │ └── VpnTileService.kt │ │ │ └── viewmodel │ │ │ ├── LinkViewModel.kt │ │ │ └── ProfileViewModel.kt │ │ ├── jni │ │ ├── Android.mk │ │ └── Application.mk │ │ └── res │ │ ├── drawable-v26 │ │ └── ic_launcher.xml │ │ ├── drawable │ │ ├── baseline_adb.xml │ │ ├── baseline_add.xml │ │ ├── baseline_alt_route.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 │ │ └── vpn_key.png │ │ ├── layout │ │ ├── activity_apps_routing.xml │ │ ├── activity_assets.xml │ │ ├── activity_links.xml │ │ ├── activity_logs.xml │ │ ├── activity_main.xml │ │ ├── activity_profile.xml │ │ ├── activity_settings.xml │ │ ├── item_recycler_exclude.xml │ │ ├── item_recycler_main.xml │ │ ├── layout_link_form.xml │ │ ├── layout_link_item.xml │ │ ├── loading_dialog.xml │ │ ├── tab_advanced_settings.xml │ │ └── tab_basic_settings.xml │ │ ├── menu │ │ ├── menu_apps_routing.xml │ │ ├── menu_drawer.xml │ │ ├── menu_links.xml │ │ ├── menu_logs.xml │ │ ├── menu_main.xml │ │ ├── menu_profile.xml │ │ └── menu_settings.xml │ │ ├── mipmap │ │ └── banner.png │ │ ├── values │ │ ├── array.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ └── network_security_config.xml └── versionCode.txt ├── build-xray.sh ├── build.gradle.kts ├── buildXrayCore.sh ├── get-it-on-fdroid.png ├── get-it-on-github.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── metadata └── en-US │ ├── changelogs │ ├── 104.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 │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── screenshot-01-home.png │ │ ├── screenshot-02-assets.png │ │ ├── screenshot-03-settings-basic.png │ │ └── screenshot-04-settings-advanced.png │ └── short_description.txt └── settings.gradle.kts /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build-arm32: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Build arm32 docker 15 | run: docker build -t arm32 -f Dockerfile . 16 | - name: Compile arm32 build 17 | run: | 18 | docker run --rm \ 19 | -v /opt/dist:/opt/dist \ 20 | -e DIST_DIR='/opt/dist' \ 21 | -e RELEASE_TAG="$GITHUB_REF_NAME" \ 22 | -e NATIVE_ARCH="arm" \ 23 | -e ABI_ID=1 \ 24 | -e ABI_TARGET="armeabi-v7a" \ 25 | -e KS_FILE="${{ secrets.KS_FILE }}" \ 26 | -e KS_PASSWORD="${{ secrets.KS_PASSWORD }}" \ 27 | -e KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \ 28 | -e KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \ 29 | arm32 30 | - name: Upload arm32 artifact 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: arm32-build 34 | path: /opt/dist 35 | 36 | build-arm64: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v4 41 | - name: Build arm64 docker 42 | run: docker build -t arm64 -f Dockerfile . 43 | - name: Compile arm64 build 44 | run: | 45 | docker run --rm \ 46 | -v /opt/dist:/opt/dist \ 47 | -e DIST_DIR='/opt/dist' \ 48 | -e RELEASE_TAG="$GITHUB_REF_NAME" \ 49 | -e NATIVE_ARCH="arm64" \ 50 | -e ABI_ID=2 \ 51 | -e ABI_TARGET="arm64-v8a" \ 52 | -e KS_FILE="${{ secrets.KS_FILE }}" \ 53 | -e KS_PASSWORD="${{ secrets.KS_PASSWORD }}" \ 54 | -e KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \ 55 | -e KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \ 56 | arm64 57 | - name: Upload arm64 artifact 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: arm64-build 61 | path: /opt/dist 62 | 63 | build-x86: 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Checkout code 67 | uses: actions/checkout@v4 68 | - name: Build x86 docker 69 | run: docker build -t x86 -f Dockerfile . 70 | - name: Compile x86 build 71 | run: | 72 | docker run --rm \ 73 | -v /opt/dist:/opt/dist \ 74 | -e DIST_DIR='/opt/dist' \ 75 | -e RELEASE_TAG="$GITHUB_REF_NAME" \ 76 | -e NATIVE_ARCH="386" \ 77 | -e ABI_ID=3 \ 78 | -e ABI_TARGET="x86" \ 79 | -e KS_FILE="${{ secrets.KS_FILE }}" \ 80 | -e KS_PASSWORD="${{ secrets.KS_PASSWORD }}" \ 81 | -e KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \ 82 | -e KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \ 83 | x86 84 | - name: Upload x86 artifact 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: x86-build 88 | path: /opt/dist 89 | 90 | build-amd64: 91 | runs-on: ubuntu-latest 92 | steps: 93 | - name: Checkout code 94 | uses: actions/checkout@v4 95 | - name: Build amd64 docker 96 | run: docker build -t amd64 -f Dockerfile . 97 | - name: Compile amd64 build 98 | run: | 99 | docker run --rm \ 100 | -v /opt/dist:/opt/dist \ 101 | -e DIST_DIR='/opt/dist' \ 102 | -e RELEASE_TAG="$GITHUB_REF_NAME" \ 103 | -e NATIVE_ARCH="amd64" \ 104 | -e ABI_ID=4 \ 105 | -e ABI_TARGET="x86_64" \ 106 | -e KS_FILE="${{ secrets.KS_FILE }}" \ 107 | -e KS_PASSWORD="${{ secrets.KS_PASSWORD }}" \ 108 | -e KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \ 109 | -e KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \ 110 | amd64 111 | - name: Upload amd64 artifact 112 | uses: actions/upload-artifact@v4 113 | with: 114 | name: amd64-build 115 | path: /opt/dist 116 | 117 | publish: 118 | runs-on: ubuntu-latest 119 | needs: 120 | - build-arm32 121 | - build-arm64 122 | - build-x86 123 | - build-amd64 124 | permissions: 125 | contents: write 126 | steps: 127 | - name: Checkout code 128 | uses: actions/checkout@v4 129 | - name: Download arm32 artifact 130 | uses: actions/download-artifact@v4 131 | with: 132 | name: arm32-build 133 | path: dist 134 | - name: Download arm64 artifact 135 | uses: actions/download-artifact@v4 136 | with: 137 | name: arm64-build 138 | path: dist 139 | - name: Download x86 artifact 140 | uses: actions/download-artifact@v4 141 | with: 142 | name: x86-build 143 | path: dist 144 | - name: Download amd64 artifact 145 | uses: actions/download-artifact@v4 146 | with: 147 | name: amd64-build 148 | path: dist 149 | - name: Set VERSION_CODE 150 | run: | 151 | ALL_VARIANTS=4 152 | VERSION_CODE=$(cat "$GITHUB_WORKSPACE/app/versionCode.txt") 153 | ((VERSION_CODE += ALL_VARIANTS)) 154 | echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV 155 | - name: Publish release 156 | uses: softprops/action-gh-release@v1 157 | with: 158 | tag_name: ${{ github.ref_name }} 159 | name: ${{ github.ref_name }} 160 | prerelease: false 161 | draft: false 162 | files: "dist/*.apk" 163 | body_path: ${{ github.workspace }}/metadata/en-US/changelogs/${{ env.VERSION_CODE }}.txt 164 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | .kotlin 4 | .gradle 5 | /local.properties 6 | /.idea/caches 7 | /.idea/libraries 8 | /.idea/modules.xml 9 | /.idea/workspace.xml 10 | /.idea/navEditor.xml 11 | /.idea/assetWizardSettings.xml 12 | .DS_Store 13 | /build 14 | /captures 15 | .externalNativeBuild 16 | .cxx 17 | local.properties 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "app/src/main/jni/hev-socks5-tunnel"] 2 | path = app/src/main/jni/hev-socks5-tunnel 3 | url = https://github.com/heiher/hev-socks5-tunnel.git 4 | [submodule "XrayCore/libXray"] 5 | path = XrayCore/libXray 6 | url = https://github.com/XTLS/libXray.git 7 | [submodule "XrayCore/Xray-core"] 8 | path = XrayCore/Xray-core 9 | url = https://github.com/XTLS/Xray-core.git 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm 2 | 3 | ENV LANG=C.UTF-8 \ 4 | DEBIAN_FRONTEND=noninteractive 5 | 6 | COPY build-xray.sh /build-xray.sh 7 | 8 | ENTRYPOINT ["/build-xray.sh"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 SaeedDev94 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xray 2 | App Cover 3 | This is a simple GUI client for XTLS/Xray-core
4 | Also you can use it independent of Xray-core just for your socks proxy (Like SocksDroid) 5 | 6 | # Screenshots 7 | MainActivityAssetsActivitySettingsActivity: Basic TabSettingsActivity: Advanced Tab 8 | 9 | # APK variants guide 10 | - arm32 => versionCode + 1 11 | - arm64 => versionCode + 2 12 | - x86 => versionCode + 3 13 | - amd64 => versionCode + 4 14 | 15 | # Download 16 | [![Release CI](https://github.com/SaeedDev94/Xray/actions/workflows/release.yml/badge.svg)](https://github.com/SaeedDev94/Xray/actions) 17 | Get it on GitHub 18 | Get it on F-Droid 19 | -------------------------------------------------------------------------------- /XrayCore/go.mod: -------------------------------------------------------------------------------- 1 | module XrayCore 2 | 3 | go 1.24.3 4 | 5 | replace github.com/xtls/xray-core => ./Xray-core 6 | 7 | replace github.com/xtls/libxray => ./libXray 8 | 9 | require ( 10 | github.com/xtls/libxray v0.0.0-00010101000000-000000000000 11 | github.com/xtls/xray-core v0.0.0-00010101000000-000000000000 12 | ) 13 | 14 | require ( 15 | github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 // indirect 16 | github.com/andybalholm/brotli v1.1.0 // indirect 17 | github.com/cloudflare/circl v1.6.1 // indirect 18 | github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect 19 | github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect 20 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 21 | github.com/google/btree v1.1.2 // indirect 22 | github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect 23 | github.com/gorilla/websocket v1.5.3 // indirect 24 | github.com/klauspost/compress v1.17.8 // indirect 25 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 26 | github.com/onsi/ginkgo/v2 v2.19.0 // indirect 27 | github.com/pelletier/go-toml v1.9.5 // indirect 28 | github.com/pires/go-proxyproto v0.8.1 // indirect 29 | github.com/quic-go/qpack v0.5.1 // indirect 30 | github.com/quic-go/quic-go v0.51.0 // indirect 31 | github.com/refraction-networking/utls v1.7.3 // indirect 32 | github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect 33 | github.com/sagernet/sing v0.5.1 // indirect 34 | github.com/sagernet/sing-shadowsocks v0.2.7 // indirect 35 | github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect 36 | github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect 37 | github.com/vishvananda/netlink v1.3.1 // indirect 38 | github.com/vishvananda/netns v0.0.5 // indirect 39 | github.com/xtls/reality v0.0.0-20250516070713-4df2ec9a5b47 // indirect 40 | go.uber.org/mock v0.5.0 // indirect 41 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect 42 | golang.org/x/crypto v0.38.0 // indirect 43 | golang.org/x/mobile v0.0.0-20250506005352-78cd7a343bde // indirect 44 | golang.org/x/mod v0.24.0 // indirect 45 | golang.org/x/net v0.40.0 // indirect 46 | golang.org/x/sync v0.14.0 // indirect 47 | golang.org/x/sys v0.33.0 // indirect 48 | golang.org/x/text v0.25.0 // indirect 49 | golang.org/x/time v0.7.0 // indirect 50 | golang.org/x/tools v0.33.0 // indirect 51 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 52 | golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect 53 | google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9 // indirect 54 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect 55 | google.golang.org/grpc v1.72.1 // indirect 56 | google.golang.org/protobuf v1.36.6 // indirect 57 | gopkg.in/yaml.v2 v2.4.0 // indirect 58 | gopkg.in/yaml.v3 v3.0.1 // indirect 59 | gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 // indirect 60 | lukechampine.com/blake3 v1.4.1 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /XrayCore/lib/core.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "github.com/xtls/xray-core/common/cmdarg" 5 | "github.com/xtls/xray-core/core" 6 | _ "github.com/xtls/xray-core/main/distro/all" 7 | ) 8 | 9 | var coreServer *core.Instance 10 | 11 | func Server(config string) (*core.Instance, error) { 12 | file := cmdarg.Arg{config} 13 | json, err := core.LoadConfig("json", file) 14 | if err != nil { 15 | return nil, err 16 | } 17 | server, err := core.New(json) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return server, nil 22 | } 23 | 24 | func Start(dir string, config string) (err error) { 25 | SetEnv(dir) 26 | coreServer, err = Server(config) 27 | if err != nil { 28 | return 29 | } 30 | if err = coreServer.Start(); err != nil { 31 | return 32 | } 33 | return nil 34 | } 35 | 36 | func Stop() error { 37 | if coreServer != nil { 38 | err := coreServer.Close() 39 | coreServer = nil 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | func Version() string { 48 | return core.Version() 49 | } 50 | -------------------------------------------------------------------------------- /XrayCore/lib/env.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import "os" 4 | 5 | func SetEnv(dir string) { 6 | os.Setenv("xray.location.asset", dir) 7 | } 8 | -------------------------------------------------------------------------------- /XrayCore/lib/error.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | func WrapError(err error) string { 4 | if err != nil { 5 | return err.Error() 6 | } 7 | return "" 8 | } 9 | -------------------------------------------------------------------------------- /XrayCore/lib/test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | func Test(dir string, config string) error { 4 | SetEnv(dir) 5 | _, err := Server(config) 6 | if err != nil { 7 | return err 8 | } 9 | return nil 10 | } 11 | -------------------------------------------------------------------------------- /XrayCore/main.go: -------------------------------------------------------------------------------- 1 | package XrayCore 2 | 3 | import ( 4 | "github.com/xtls/xray-core/infra/conf" 5 | "github.com/xtls/libxray/nodep" 6 | "github.com/xtls/libxray/share" 7 | "XrayCore/lib" 8 | ) 9 | 10 | func Test(dir string, config string) string { 11 | err := lib.Test(dir, config) 12 | return lib.WrapError(err) 13 | } 14 | 15 | func Start(dir string, config string) string { 16 | err := lib.Start(dir, config) 17 | return lib.WrapError(err) 18 | } 19 | 20 | func Stop() string { 21 | err := lib.Stop() 22 | return lib.WrapError(err) 23 | } 24 | 25 | func Version() string { 26 | return lib.Version() 27 | } 28 | 29 | func Json(link string) string { 30 | var response nodep.CallResponse[*conf.Config] 31 | xrayJson, err := share.ConvertShareLinksToXrayJson(link) 32 | return response.EncodeToBase64(xrayJson, err) 33 | } 34 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.devtools.ksp) 5 | id("kotlin-parcelize") 6 | } 7 | 8 | val abiId: String by project 9 | val abiTarget: String by project 10 | 11 | fun calcVersionCode(): Int { 12 | val versionCodeFile = file("versionCode.txt") 13 | val versionCode = versionCodeFile.readText().trim().toInt() 14 | return versionCode + abiId.toInt() 15 | } 16 | 17 | android { 18 | namespace = "io.github.saeeddev94.xray" 19 | compileSdk = 35 20 | 21 | defaultConfig { 22 | applicationId = "io.github.saeeddev94.xray" 23 | minSdk = 26 24 | targetSdk = 36 25 | versionCode = calcVersionCode() 26 | versionName = "10.8.4" 27 | 28 | vectorDrawables { 29 | useSupportLibrary = true 30 | } 31 | } 32 | 33 | buildFeatures { 34 | buildConfig = true 35 | viewBinding = true 36 | } 37 | 38 | buildTypes { 39 | release { 40 | isMinifyEnabled = false 41 | proguardFiles( 42 | getDefaultProguardFile("proguard-android-optimize.txt"), 43 | "proguard-rules.pro" 44 | ) 45 | } 46 | } 47 | 48 | compileOptions { 49 | sourceCompatibility = JavaVersion.VERSION_17 50 | targetCompatibility = JavaVersion.VERSION_17 51 | } 52 | 53 | kotlinOptions { 54 | jvmTarget = "17" 55 | } 56 | 57 | externalNativeBuild { 58 | ndkVersion = "27.1.12297006" 59 | ndkBuild { 60 | path = file("src/main/jni/Android.mk") 61 | } 62 | } 63 | 64 | packaging { 65 | jniLibs { 66 | keepDebugSymbols += "**/*.so" 67 | } 68 | } 69 | 70 | splits { 71 | abi { 72 | isEnable = true 73 | isUniversalApk = false 74 | reset() 75 | //noinspection ChromeOsAbiSupport 76 | include(*abiTarget.split(",").toTypedArray()) 77 | } 78 | } 79 | } 80 | 81 | dependencies { 82 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar")))) 83 | implementation(libs.androidx.activity.ktx) 84 | implementation(libs.androidx.core.ktx) 85 | implementation(libs.androidx.appcompat) 86 | implementation(libs.androidx.lifecycle.viewmodel.ktx) 87 | implementation(libs.androidx.room.ktx) 88 | implementation(libs.androidx.room.runtime) 89 | ksp(libs.androidx.room.compiler) 90 | implementation(libs.blacksquircle.ui.editorkit) 91 | implementation(libs.blacksquircle.ui.language.json) 92 | implementation(libs.google.material) 93 | } 94 | -------------------------------------------------------------------------------- /app/libs/.gitignore: -------------------------------------------------------------------------------- 1 | *.aar 2 | *.jar 3 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | # Disable R8 optimizations 24 | -keep class kotlinx.coroutines.CoroutineExceptionHandler 25 | -keep class kotlinx.coroutines.internal.MainDispatcherFactory 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 17 | 18 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 63 | 66 | 69 | 72 | 75 | 80 | 81 | 82 | 83 | 86 | 87 | 93 | 94 | 95 | 96 | 99 | 102 | 103 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/Settings.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray 2 | 3 | import android.content.Context 4 | import androidx.core.content.edit 5 | import java.io.File 6 | 7 | class Settings(private val context: Context) { 8 | 9 | private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE) 10 | 11 | /** Active Link ID */ 12 | var selectedLink: Long 13 | get() = sharedPreferences.getLong("selectedLink", 0L) 14 | set(value) = sharedPreferences.edit { putLong("selectedLink", value) } 15 | 16 | /** Active Profile ID */ 17 | var selectedProfile: Long 18 | get() = sharedPreferences.getLong("selectedProfile", 0L) 19 | set(value) = sharedPreferences.edit { putLong("selectedProfile", value) } 20 | 21 | /** 22 | * Apps Routing 23 | * Mode: true -> exclude, false -> include 24 | * Default: exclude 25 | */ 26 | var appsRoutingMode: Boolean 27 | get() = sharedPreferences.getBoolean("appsRoutingMode", true) 28 | set(value) = sharedPreferences.edit { putBoolean("appsRoutingMode", value) } 29 | var appsRouting: String 30 | get() = sharedPreferences.getString("excludedApps", "")!! 31 | set(value) = sharedPreferences.edit { putString("excludedApps", value) } 32 | 33 | /** Basic */ 34 | var socksAddress: String 35 | get() = sharedPreferences.getString("socksAddress", "127.0.0.1")!! 36 | set(value) = sharedPreferences.edit { putString("socksAddress", value) } 37 | var socksPort: String 38 | get() = sharedPreferences.getString("socksPort", "10808")!! 39 | set(value) = sharedPreferences.edit { putString("socksPort", value) } 40 | var socksUsername: String 41 | get() = sharedPreferences.getString("socksUsername", "")!! 42 | set(value) = sharedPreferences.edit { putString("socksUsername", value) } 43 | var socksPassword: String 44 | get() = sharedPreferences.getString("socksPassword", "")!! 45 | set(value) = sharedPreferences.edit { putString("socksPassword", value) } 46 | var geoIpAddress: String 47 | get() = sharedPreferences.getString( 48 | "geoIpAddress", 49 | "https://github.com/v2fly/geoip/releases/latest/download/geoip.dat" 50 | )!! 51 | set(value) = sharedPreferences.edit { putString("geoIpAddress", value) } 52 | var geoSiteAddress: String 53 | get() = sharedPreferences.getString( 54 | "geoSiteAddress", 55 | "https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat" 56 | )!! 57 | set(value) = sharedPreferences.edit { putString("geoSiteAddress", value) } 58 | var pingAddress: String 59 | get() = sharedPreferences.getString("pingAddress", "https://www.google.com")!! 60 | set(value) = sharedPreferences.edit { putString("pingAddress", value) } 61 | var pingTimeout: Int 62 | get() = sharedPreferences.getInt("pingTimeout", 5) 63 | set(value) = sharedPreferences.edit { putInt("pingTimeout", value) } 64 | var bypassLan: Boolean 65 | get() = sharedPreferences.getBoolean("bypassLan", true) 66 | set(value) = sharedPreferences.edit { putBoolean("bypassLan", value) } 67 | var enableIpV6: Boolean 68 | get() = sharedPreferences.getBoolean("enableIpV6", true) 69 | set(value) = sharedPreferences.edit { putBoolean("enableIpV6", value) } 70 | var socksUdp: Boolean 71 | get() = sharedPreferences.getBoolean("socksUdp", true) 72 | set(value) = sharedPreferences.edit { putBoolean("socksUdp", value) } 73 | var bootAutoStart: Boolean 74 | get() = sharedPreferences.getBoolean("bootAutoStart", false) 75 | set(value) = sharedPreferences.edit { putBoolean("bootAutoStart", value) } 76 | 77 | /** Advanced */ 78 | var primaryDns: String 79 | get() = sharedPreferences.getString("primaryDns", "1.1.1.1")!! 80 | set(value) = sharedPreferences.edit { putString("primaryDns", value) } 81 | var secondaryDns: String 82 | get() = sharedPreferences.getString("secondaryDns", "1.0.0.1")!! 83 | set(value) = sharedPreferences.edit { putString("secondaryDns", value) } 84 | var primaryDnsV6: String 85 | get() = sharedPreferences.getString("primaryDnsV6", "2606:4700:4700::1111")!! 86 | set(value) = sharedPreferences.edit { putString("primaryDnsV6", value) } 87 | var secondaryDnsV6: String 88 | get() = sharedPreferences.getString("secondaryDnsV6", "2606:4700:4700::1001")!! 89 | set(value) = sharedPreferences.edit { putString("secondaryDnsV6", value) } 90 | var tunName: String 91 | get() = sharedPreferences.getString("tunName", "tun0")!! 92 | set(value) = sharedPreferences.edit { putString("tunName", value) } 93 | var tunMtu: Int 94 | get() = sharedPreferences.getInt("tunMtu", 8500) 95 | set(value) = sharedPreferences.edit { putInt("tunMtu", value) } 96 | var tunAddress: String 97 | get() = sharedPreferences.getString("tunAddress", "10.10.10.10")!! 98 | set(value) = sharedPreferences.edit { putString("tunAddress", value) } 99 | var tunPrefix: Int 100 | get() = sharedPreferences.getInt("tunPrefix", 32) 101 | set(value) = sharedPreferences.edit { putInt("tunPrefix", value) } 102 | var tunAddressV6: String 103 | get() = sharedPreferences.getString("tunAddressV6", "fc00::1")!! 104 | set(value) = sharedPreferences.edit { putString("tunAddressV6", value) } 105 | var tunPrefixV6: Int 106 | get() = sharedPreferences.getInt("tunPrefixV6", 128) 107 | set(value) = sharedPreferences.edit { putInt("tunPrefixV6", value) } 108 | 109 | fun testConfig(): File = File(context.filesDir, "test.json") 110 | fun xrayConfig(): File = File(context.filesDir, "config.json") 111 | fun tun2socksConfig(): File = File(context.filesDir, "tun2socks.yml") 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/Xray.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray 2 | 3 | import android.app.Application 4 | import io.github.saeeddev94.xray.database.XrayDatabase 5 | import io.github.saeeddev94.xray.repository.LinkRepository 6 | import io.github.saeeddev94.xray.repository.ProfileRepository 7 | 8 | class Xray : Application() { 9 | 10 | private val xrayDatabase by lazy { XrayDatabase.ref(this) } 11 | val linkRepository by lazy { LinkRepository(xrayDatabase.linkDao()) } 12 | val profileRepository by lazy { ProfileRepository(xrayDatabase.profileDao()) } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/activity/AppsRoutingActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.activity 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import android.view.View 10 | import android.widget.ImageView 11 | import android.widget.Toast 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.appcompat.widget.SearchView 14 | import androidx.lifecycle.lifecycleScope 15 | import androidx.recyclerview.widget.LinearLayoutManager 16 | import androidx.recyclerview.widget.RecyclerView 17 | import io.github.saeeddev94.xray.R 18 | import io.github.saeeddev94.xray.Settings 19 | import io.github.saeeddev94.xray.adapter.AppsRoutingAdapter 20 | import io.github.saeeddev94.xray.databinding.ActivityAppsRoutingBinding 21 | import io.github.saeeddev94.xray.dto.AppList 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.launch 24 | import kotlinx.coroutines.withContext 25 | 26 | class AppsRoutingActivity : AppCompatActivity() { 27 | 28 | private val settings by lazy { Settings(applicationContext) } 29 | private lateinit var binding: ActivityAppsRoutingBinding 30 | private lateinit var appsList: RecyclerView 31 | private lateinit var appsRoutingAdapter: AppsRoutingAdapter 32 | private lateinit var apps: ArrayList 33 | private lateinit var filtered: MutableList 34 | private lateinit var appsRouting: MutableSet 35 | private lateinit var menu: Menu 36 | private var appsRoutingMode: Boolean = true 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | super.onCreate(savedInstanceState) 40 | title = "" 41 | binding = ActivityAppsRoutingBinding.inflate(layoutInflater) 42 | appsRoutingMode = settings.appsRoutingMode 43 | setContentView(binding.root) 44 | setSupportActionBar(binding.toolbar) 45 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 46 | 47 | binding.search.focusable = View.NOT_FOCUSABLE 48 | binding.search.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 49 | override fun onQueryTextChange(newText: String?): Boolean { 50 | search(newText) 51 | return false 52 | } 53 | 54 | override fun onQueryTextSubmit(query: String?): Boolean { 55 | binding.search.clearFocus() 56 | search(query) 57 | return false 58 | } 59 | }) 60 | binding.search.findViewById(androidx.appcompat.R.id.search_close_btn) 61 | ?.setOnClickListener { 62 | binding.search.setQuery("", false) 63 | binding.search.clearFocus() 64 | } 65 | 66 | getApps() 67 | } 68 | 69 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 70 | this.menu = menu 71 | menuInflater.inflate(R.menu.menu_apps_routing, menu) 72 | handleMode() 73 | return true 74 | } 75 | 76 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 77 | when (item.itemId) { 78 | R.id.appsRoutingSave -> saveAppsRouting() 79 | R.id.appsRoutingExcludeMode -> setMode(false) 80 | R.id.appsRoutingIncludeMode -> setMode(true) 81 | else -> finish() 82 | } 83 | return true 84 | } 85 | 86 | private fun setMode(appsRoutingMode: Boolean) { 87 | this.appsRoutingMode = appsRoutingMode 88 | handleMode().also { message -> 89 | Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show() 90 | } 91 | } 92 | 93 | private fun handleMode(): String { 94 | val excludeItem = menu.findItem(R.id.appsRoutingExcludeMode) 95 | val includeItem = menu.findItem(R.id.appsRoutingIncludeMode) 96 | return when (this.appsRoutingMode) { 97 | true -> { 98 | excludeItem.isVisible = true 99 | includeItem.isVisible = false 100 | getString(R.string.appsRoutingExcludeMode) 101 | } 102 | 103 | false -> { 104 | excludeItem.isVisible = false 105 | includeItem.isVisible = true 106 | getString(R.string.appsRoutingIncludeMode) 107 | } 108 | } 109 | } 110 | 111 | @SuppressLint("NotifyDataSetChanged") 112 | private fun search(query: String?) { 113 | val keyword = query?.trim()?.lowercase() ?: "" 114 | if (keyword.isEmpty()) { 115 | if (apps.size > filtered.size) { 116 | filtered.clear() 117 | filtered.addAll(apps.toMutableList()) 118 | appsRoutingAdapter.notifyDataSetChanged() 119 | } 120 | return 121 | } 122 | val list = ArrayList() 123 | apps.forEach { 124 | if (it.appName.lowercase().contains(keyword) || it.packageName.contains(keyword)) { 125 | list.add(it) 126 | } 127 | } 128 | filtered.clear() 129 | filtered.addAll(list.toMutableList()) 130 | appsRoutingAdapter.notifyDataSetChanged() 131 | } 132 | 133 | private fun getApps() { 134 | lifecycleScope.launch { 135 | val selected = ArrayList() 136 | val unselected = ArrayList() 137 | packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS).forEach { 138 | val permissions = it.requestedPermissions 139 | if ( 140 | permissions == null || !permissions.contains(Manifest.permission.INTERNET) 141 | ) return@forEach 142 | val appIcon = it.applicationInfo!!.loadIcon(packageManager) 143 | val appName = it.applicationInfo!!.loadLabel(packageManager).toString() 144 | val packageName = it.packageName 145 | val app = AppList(appIcon, appName, packageName) 146 | val isSelected = settings.appsRouting.contains(packageName) 147 | if (isSelected) selected.add(app) else unselected.add(app) 148 | } 149 | withContext(Dispatchers.Main) { 150 | apps = ArrayList(selected + unselected) 151 | filtered = apps.toMutableList() 152 | appsRouting = settings.appsRouting.split("\n").toMutableSet() 153 | appsList = binding.appsList 154 | appsRoutingAdapter = AppsRoutingAdapter( 155 | this@AppsRoutingActivity, filtered, appsRouting 156 | ) 157 | appsList.adapter = appsRoutingAdapter 158 | appsList.layoutManager = LinearLayoutManager(applicationContext) 159 | } 160 | } 161 | } 162 | 163 | private fun saveAppsRouting() { 164 | binding.search.clearFocus() 165 | settings.appsRoutingMode = appsRoutingMode 166 | settings.appsRouting = appsRouting.joinToString("\n") 167 | finish() 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/activity/AssetsActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.activity 2 | 3 | import android.annotation.SuppressLint 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.View 7 | import android.widget.LinearLayout 8 | import android.widget.ProgressBar 9 | import android.widget.Toast 10 | import androidx.activity.result.contract.ActivityResultContracts 11 | import androidx.appcompat.app.AppCompatActivity 12 | import androidx.lifecycle.lifecycleScope 13 | import io.github.saeeddev94.xray.R 14 | import io.github.saeeddev94.xray.Settings 15 | import io.github.saeeddev94.xray.databinding.ActivityAssetsBinding 16 | import io.github.saeeddev94.xray.helper.DownloadHelper 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.launch 19 | import kotlinx.coroutines.withContext 20 | import java.io.File 21 | import java.io.FileOutputStream 22 | import java.text.SimpleDateFormat 23 | import java.util.Date 24 | 25 | class AssetsActivity : AppCompatActivity() { 26 | 27 | private lateinit var binding: ActivityAssetsBinding 28 | private var downloading: Boolean = false 29 | 30 | private val settings by lazy { Settings(applicationContext) } 31 | private val geoIpLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { 32 | writeToFile(it, geoIpFile()) 33 | } 34 | private val geoSiteLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { 35 | writeToFile(it, geoSiteFile()) 36 | } 37 | 38 | private fun geoIpFile(): File = File(applicationContext.filesDir, "geoip.dat") 39 | private fun geoSiteFile(): File = File(applicationContext.filesDir, "geosite.dat") 40 | 41 | override fun onCreate(savedInstanceState: Bundle?) { 42 | super.onCreate(savedInstanceState) 43 | val mimeType = "application/octet-stream" 44 | title = getString(R.string.assets) 45 | binding = ActivityAssetsBinding.inflate(layoutInflater) 46 | setContentView(binding.root) 47 | setSupportActionBar(binding.toolbar) 48 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 49 | setAssetStatus() 50 | 51 | // GeoIP 52 | binding.geoIpDownload.setOnClickListener { 53 | download(settings.geoIpAddress, geoIpFile(), binding.geoIpSetup, binding.geoIpProgress) 54 | } 55 | binding.geoIpFile.setOnClickListener { geoIpLauncher.launch(mimeType) } 56 | binding.geoIpDelete.setOnClickListener { delete(geoIpFile()) } 57 | 58 | // GeoSite 59 | binding.geoSiteDownload.setOnClickListener { 60 | download( 61 | settings.geoSiteAddress, 62 | geoSiteFile(), 63 | binding.geoSiteSetup, 64 | binding.geoSiteProgress 65 | ) 66 | } 67 | binding.geoSiteFile.setOnClickListener { geoSiteLauncher.launch(mimeType) } 68 | binding.geoSiteDelete.setOnClickListener { delete(geoSiteFile()) } 69 | } 70 | 71 | @SuppressLint("SimpleDateFormat") 72 | private fun getFileDate(file: File): String { 73 | return if (file.exists()) { 74 | val date = Date(file.lastModified()) 75 | SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(date) 76 | } else { 77 | getString(R.string.noValue) 78 | } 79 | } 80 | 81 | private fun setAssetStatus() { 82 | val geoIp = geoIpFile() 83 | val geoIpExists = geoIp.exists() 84 | binding.geoIpDate.text = getFileDate(geoIp) 85 | binding.geoIpSetup.visibility = if (geoIpExists) View.GONE else View.VISIBLE 86 | binding.geoIpInstalled.visibility = if (geoIpExists) View.VISIBLE else View.GONE 87 | binding.geoIpProgress.visibility = View.GONE 88 | 89 | val geoSite = geoSiteFile() 90 | val geoSiteExists = geoSite.exists() 91 | binding.geoSiteDate.text = getFileDate(geoSite) 92 | binding.geoSiteSetup.visibility = if (geoSiteExists) View.GONE else View.VISIBLE 93 | binding.geoSiteInstalled.visibility = if (geoSiteExists) View.VISIBLE else View.GONE 94 | binding.geoSiteProgress.visibility = View.GONE 95 | } 96 | 97 | private fun download(url: String, file: File, setup: LinearLayout, progressBar: ProgressBar) { 98 | if (downloading) { 99 | Toast.makeText( 100 | applicationContext, "Another download is running, please wait", Toast.LENGTH_SHORT 101 | ).show() 102 | return 103 | } 104 | 105 | setup.visibility = View.GONE 106 | progressBar.visibility = View.VISIBLE 107 | progressBar.progress = 0 108 | 109 | downloading = true 110 | DownloadHelper(lifecycleScope, url, file, object : DownloadHelper.DownloadListener { 111 | override fun onProgress(progress: Int) { 112 | progressBar.progress = progress 113 | } 114 | 115 | override fun onError(exception: Exception) { 116 | downloading = false 117 | Toast.makeText(applicationContext, exception.message, Toast.LENGTH_SHORT).show() 118 | setAssetStatus() 119 | } 120 | 121 | override fun onComplete() { 122 | downloading = false 123 | setAssetStatus() 124 | } 125 | }).start() 126 | } 127 | 128 | private fun writeToFile(uri: Uri?, file: File) { 129 | if (uri == null) return 130 | lifecycleScope.launch { 131 | contentResolver.openInputStream(uri).use { input -> 132 | FileOutputStream(file).use { output -> 133 | input?.copyTo(output) 134 | } 135 | } 136 | withContext(Dispatchers.Main) { 137 | setAssetStatus() 138 | } 139 | } 140 | } 141 | 142 | private fun delete(file: File) { 143 | lifecycleScope.launch { 144 | file.delete() 145 | withContext(Dispatchers.Main) { 146 | setAssetStatus() 147 | } 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/activity/LinksActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.activity 2 | 3 | import android.os.Bundle 4 | import android.view.Menu 5 | import android.view.MenuItem 6 | import androidx.activity.viewModels 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.lifecycleScope 10 | import androidx.lifecycle.repeatOnLifecycle 11 | import androidx.recyclerview.widget.DefaultItemAnimator 12 | import androidx.recyclerview.widget.LinearLayoutManager 13 | import androidx.recyclerview.widget.RecyclerView 14 | import io.github.saeeddev94.xray.R 15 | import io.github.saeeddev94.xray.adapter.LinkAdapter 16 | import io.github.saeeddev94.xray.database.Link 17 | import io.github.saeeddev94.xray.databinding.ActivityLinksBinding 18 | import io.github.saeeddev94.xray.viewmodel.LinkViewModel 19 | import kotlinx.coroutines.flow.collectLatest 20 | import kotlinx.coroutines.launch 21 | 22 | class LinksActivity : AppCompatActivity() { 23 | 24 | private val linkViewModel: LinkViewModel by viewModels() 25 | private val adapter by lazy { LinkAdapter() } 26 | private val linksRecyclerView by lazy { findViewById(R.id.linksRecyclerView) } 27 | private var links: MutableList = mutableListOf() 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | title = getString(R.string.links) 32 | val binding = ActivityLinksBinding.inflate(layoutInflater) 33 | setContentView(binding.root) 34 | setSupportActionBar(binding.toolbar) 35 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 36 | adapter.onEditClick = { link -> openLink(link) } 37 | adapter.onDeleteClick = { link -> deleteLink(link) } 38 | linksRecyclerView.layoutManager = LinearLayoutManager(this) 39 | linksRecyclerView.itemAnimator = DefaultItemAnimator() 40 | linksRecyclerView.adapter = adapter 41 | lifecycleScope.launch { 42 | lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 43 | linkViewModel.links.collectLatest { 44 | links = it.toMutableList() 45 | adapter.submitList(it) 46 | } 47 | } 48 | } 49 | } 50 | 51 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 52 | menuInflater.inflate(R.menu.menu_links, menu) 53 | return true 54 | } 55 | 56 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 57 | when (item.itemId) { 58 | R.id.refreshLinks -> refreshLinks() 59 | R.id.newLink -> openLink() 60 | else -> finish() 61 | } 62 | return true 63 | } 64 | 65 | private fun refreshLinks() { 66 | val intent = LinksManagerActivity.refreshLinks(applicationContext) 67 | startActivity(intent) 68 | } 69 | 70 | private fun openLink(link: Link = Link()) { 71 | val intent = LinksManagerActivity.openLink(applicationContext, link) 72 | startActivity(intent) 73 | } 74 | 75 | private fun deleteLink(link: Link) { 76 | val intent = LinksManagerActivity.deleteLink(applicationContext, link) 77 | startActivity(intent) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/activity/LogsActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.activity 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ClipData 5 | import android.content.ClipboardManager 6 | import android.content.Context 7 | import android.os.Bundle 8 | import android.util.Log 9 | import android.view.Menu 10 | import android.view.MenuItem 11 | import android.view.View 12 | import android.widget.Toast 13 | import androidx.appcompat.app.AppCompatActivity 14 | import androidx.lifecycle.lifecycleScope 15 | import io.github.saeeddev94.xray.BuildConfig 16 | import io.github.saeeddev94.xray.R 17 | import io.github.saeeddev94.xray.databinding.ActivityLogsBinding 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.launch 20 | import kotlinx.coroutines.withContext 21 | import java.io.BufferedReader 22 | import java.io.IOException 23 | import java.io.InputStreamReader 24 | import java.nio.charset.StandardCharsets 25 | 26 | class LogsActivity : AppCompatActivity() { 27 | 28 | private lateinit var binding: ActivityLogsBinding 29 | 30 | companion object { 31 | private const val MAX_BUFFERED_LINES = (1 shl 14) - 1 32 | } 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | title = getString(R.string.logs) 37 | binding = ActivityLogsBinding.inflate(layoutInflater) 38 | setContentView(binding.root) 39 | setSupportActionBar(binding.toolbar) 40 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 41 | 42 | lifecycleScope.launch(Dispatchers.IO) { streamingLog() } 43 | } 44 | 45 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 46 | menuInflater.inflate(R.menu.menu_logs, menu) 47 | return true 48 | } 49 | 50 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 51 | when (item.itemId) { 52 | R.id.deleteLogs -> flush() 53 | R.id.copyLogs -> copyToClipboard(binding.logsTextView.text.toString()) 54 | else -> finish() 55 | } 56 | return true 57 | } 58 | 59 | private fun flush() { 60 | lifecycleScope.launch(Dispatchers.IO) { 61 | val command = listOf("logcat", "-c") 62 | val process = ProcessBuilder(command).start() 63 | process.waitFor() 64 | withContext(Dispatchers.Main) { 65 | binding.logsTextView.text = "" 66 | } 67 | } 68 | } 69 | 70 | private fun copyToClipboard(text: String) { 71 | if (text.isBlank()) return 72 | try { 73 | val clipData = ClipData.newPlainText(null, text) 74 | val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 75 | clipboardManager.setPrimaryClip(clipData) 76 | Toast.makeText(applicationContext, "Logs copied", Toast.LENGTH_SHORT).show() 77 | } catch (error: Exception) { 78 | error.printStackTrace() 79 | } 80 | } 81 | 82 | @SuppressLint("SetTextI18n") 83 | private suspend fun streamingLog() = withContext(Dispatchers.IO) { 84 | val cmd = listOf("logcat", "-v", "time", "-s", "GoLog,${BuildConfig.APPLICATION_ID}") 85 | val builder = ProcessBuilder(cmd) 86 | builder.environment()["LC_ALL"] = "C" 87 | var process: Process? = null 88 | try { 89 | process = try { 90 | builder.start() 91 | } catch (e: IOException) { 92 | Log.e(packageName, Log.getStackTraceString(e)) 93 | return@withContext 94 | } 95 | 96 | val stdout = BufferedReader( 97 | InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8) 98 | ) 99 | val bufferedLogLines = arrayListOf() 100 | 101 | var timeLastNotify = System.nanoTime() 102 | // The timeout is initially small so that the view gets populated immediately. 103 | var timeout = 1000000000L / 2 104 | 105 | while (true) { 106 | val line = stdout.readLine() ?: break 107 | bufferedLogLines.add(line) 108 | val timeNow = System.nanoTime() 109 | 110 | if ( 111 | bufferedLogLines.size < MAX_BUFFERED_LINES && 112 | (timeNow - timeLastNotify) < timeout && stdout.ready() 113 | ) continue 114 | 115 | // Increase the timeout after the initial view has something in it. 116 | timeout = 1000000000L * 5 / 2 117 | timeLastNotify = timeNow 118 | 119 | withContext(Dispatchers.Main) { 120 | val contentHeight = binding.logsTextView.height 121 | val scrollViewHeight = binding.logsScrollView.height 122 | val isScrolledToBottomAlready = 123 | (binding.logsScrollView.scrollY + scrollViewHeight) >= contentHeight * 0.95 124 | binding.logsTextView.text = 125 | binding.logsTextView.text.toString() + bufferedLogLines.joinToString( 126 | separator = "\n", 127 | postfix = "\n" 128 | ) 129 | bufferedLogLines.clear() 130 | if (isScrolledToBottomAlready) { 131 | binding.logsScrollView.post { 132 | binding.logsScrollView.fullScroll(View.FOCUS_DOWN) 133 | } 134 | } 135 | } 136 | } 137 | } finally { 138 | process?.destroy() 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/activity/ProfileActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.activity 2 | 3 | import XrayCore.XrayCore 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.view.Menu 9 | import android.view.MenuItem 10 | import androidx.activity.viewModels 11 | import androidx.appcompat.app.AppCompatActivity 12 | import androidx.lifecycle.lifecycleScope 13 | import com.blacksquircle.ui.editorkit.plugin.autoindent.autoIndentation 14 | import com.blacksquircle.ui.editorkit.plugin.base.PluginSupplier 15 | import com.blacksquircle.ui.editorkit.plugin.delimiters.highlightDelimiters 16 | import com.blacksquircle.ui.editorkit.plugin.linenumbers.lineNumbers 17 | import com.blacksquircle.ui.language.json.JsonLanguage 18 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 19 | import io.github.saeeddev94.xray.R 20 | import io.github.saeeddev94.xray.Settings 21 | import io.github.saeeddev94.xray.database.Profile 22 | import io.github.saeeddev94.xray.databinding.ActivityProfileBinding 23 | import io.github.saeeddev94.xray.helper.FileHelper 24 | import io.github.saeeddev94.xray.viewmodel.ProfileViewModel 25 | import kotlinx.coroutines.Dispatchers 26 | import kotlinx.coroutines.launch 27 | import kotlinx.coroutines.withContext 28 | import java.io.BufferedReader 29 | import java.io.InputStreamReader 30 | 31 | class ProfileActivity : AppCompatActivity() { 32 | 33 | companion object { 34 | private const val PROFILE_ID = "id" 35 | private const val PROFILE_NAME = "name" 36 | private const val PROFILE_CONFIG = "config" 37 | 38 | fun getIntent( 39 | context: Context, id: Long = 0L, name: String = "", config: String = "" 40 | ) = Intent(context, ProfileActivity::class.java).also { 41 | it.putExtra(PROFILE_ID, id) 42 | if (name.isNotEmpty()) it.putExtra(PROFILE_NAME, name) 43 | if (config.isNotEmpty()) it.putExtra( 44 | PROFILE_CONFIG, 45 | config.replace("\\/", "/") 46 | ) 47 | } 48 | } 49 | 50 | private val settings by lazy { Settings(applicationContext) } 51 | private val profileViewModel: ProfileViewModel by viewModels() 52 | private lateinit var binding: ActivityProfileBinding 53 | private lateinit var profile: Profile 54 | private var id: Long = 0L 55 | 56 | override fun onCreate(savedInstanceState: Bundle?) { 57 | super.onCreate(savedInstanceState) 58 | id = intent.getLongExtra(PROFILE_ID, 0L) 59 | title = if (isNew()) getString(R.string.newProfile) else getString(R.string.editProfile) 60 | binding = ActivityProfileBinding.inflate(layoutInflater) 61 | setContentView(binding.root) 62 | setSupportActionBar(binding.toolbar) 63 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 64 | val jsonUri = intent.data 65 | if (Intent.ACTION_VIEW == intent.action && jsonUri != null) { 66 | val profile = Profile() 67 | profile.config = readJsonFile(jsonUri) 68 | resolved(profile) 69 | } else if (isNew()) { 70 | val profile = Profile() 71 | profile.name = intent.getStringExtra(PROFILE_NAME) ?: "" 72 | profile.config = intent.getStringExtra(PROFILE_CONFIG) ?: "" 73 | resolved(profile) 74 | } else { 75 | lifecycleScope.launch { 76 | val profile = profileViewModel.find(id) 77 | withContext(Dispatchers.Main) { 78 | resolved(profile) 79 | } 80 | } 81 | } 82 | } 83 | 84 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 85 | menuInflater.inflate(R.menu.menu_profile, menu) 86 | return true 87 | } 88 | 89 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 90 | when (item.itemId) { 91 | R.id.saveProfile -> save() 92 | else -> finish() 93 | } 94 | return true 95 | } 96 | 97 | private fun isNew() = id == 0L 98 | 99 | private fun readJsonFile(uri: Uri): String { 100 | val content = StringBuilder() 101 | try { 102 | contentResolver.openInputStream(uri)?.use { input -> 103 | BufferedReader(InputStreamReader(input)).forEachLine { content.append("$it\n") } 104 | } 105 | } catch (error: Exception) { 106 | error.printStackTrace() 107 | } 108 | return content.toString() 109 | } 110 | 111 | private fun resolved(value: Profile) { 112 | profile = value 113 | binding.profileName.setText(profile.name) 114 | 115 | val editor = binding.profileConfig 116 | val pluginSupplier = PluginSupplier.create { 117 | lineNumbers { 118 | lineNumbers = true 119 | highlightCurrentLine = true 120 | } 121 | highlightDelimiters() 122 | autoIndentation { 123 | autoIndentLines = true 124 | autoCloseBrackets = true 125 | autoCloseQuotes = true 126 | } 127 | } 128 | editor.language = JsonLanguage() 129 | editor.setTextContent(profile.config) 130 | editor.plugins(pluginSupplier) 131 | } 132 | 133 | private fun save(check: Boolean = true) { 134 | profile.name = binding.profileName.text.toString() 135 | profile.config = binding.profileConfig.text.toString() 136 | lifecycleScope.launch { 137 | val error = isValid(profile.config) 138 | if (check && error.isNotEmpty()) { 139 | withContext(Dispatchers.Main) { 140 | showError(error) 141 | } 142 | return@launch 143 | } 144 | if (profile.id == 0L) { 145 | profileViewModel.create(profile) 146 | } else { 147 | profileViewModel.update(profile) 148 | } 149 | withContext(Dispatchers.Main) { 150 | finish() 151 | } 152 | } 153 | } 154 | 155 | private suspend fun isValid(json: String): String { 156 | return withContext(Dispatchers.IO) { 157 | val pwd = filesDir.absolutePath 158 | val testConfig = settings.testConfig() 159 | FileHelper.createOrUpdate(testConfig, json) 160 | XrayCore.test(pwd, testConfig.absolutePath) 161 | } 162 | } 163 | 164 | private fun showError(message: String) { 165 | MaterialAlertDialogBuilder(this) 166 | .setTitle(getString(R.string.invalidProfile)) 167 | .setMessage(message) 168 | .setNegativeButton(getString(R.string.cancel)) { _, _ -> } 169 | .setPositiveButton(getString(R.string.ignore)) { _, _ -> save(false) } 170 | .show() 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/activity/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.activity 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.Menu 6 | import android.view.MenuItem 7 | import android.view.View 8 | import android.widget.EditText 9 | import androidx.appcompat.app.AppCompatActivity 10 | import com.google.android.material.materialswitch.MaterialSwitch 11 | import io.github.saeeddev94.xray.R 12 | import io.github.saeeddev94.xray.Settings 13 | import io.github.saeeddev94.xray.adapter.SettingAdapter 14 | import io.github.saeeddev94.xray.databinding.ActivitySettingsBinding 15 | 16 | class SettingsActivity : AppCompatActivity() { 17 | 18 | private val settings by lazy { Settings(applicationContext) } 19 | private lateinit var binding: ActivitySettingsBinding 20 | private lateinit var adapter: SettingAdapter 21 | private lateinit var basic: View 22 | private lateinit var advanced: View 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | title = getString(R.string.settings) 27 | binding = ActivitySettingsBinding.inflate(layoutInflater) 28 | setContentView(binding.root) 29 | setSupportActionBar(binding.toolbar) 30 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 31 | val tabs = listOf("Basic", "Advanced") 32 | val layouts = listOf(R.layout.tab_basic_settings, R.layout.tab_advanced_settings) 33 | adapter = SettingAdapter(this, tabs, layouts, object : SettingAdapter.ViewsReady { 34 | override fun rendered(views: List) { 35 | basic = views[0] 36 | advanced = views[1] 37 | setupBasic() 38 | setupAdvanced() 39 | } 40 | }) 41 | binding.viewPager.adapter = adapter 42 | binding.tabLayout.setupWithViewPager(binding.viewPager) 43 | } 44 | 45 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 46 | menuInflater.inflate(R.menu.menu_settings, menu) 47 | return true 48 | } 49 | 50 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 51 | when (item.itemId) { 52 | R.id.saveSettings -> saveSettings() 53 | else -> finish() 54 | } 55 | return true 56 | } 57 | 58 | @SuppressLint("SetTextI18n") 59 | private fun setupBasic() { 60 | basic.findViewById(R.id.socksAddress).setText(settings.socksAddress) 61 | basic.findViewById(R.id.socksPort).setText(settings.socksPort) 62 | basic.findViewById(R.id.socksUsername).setText(settings.socksUsername) 63 | basic.findViewById(R.id.socksPassword).setText(settings.socksPassword) 64 | basic.findViewById(R.id.geoIpAddress).setText(settings.geoIpAddress) 65 | basic.findViewById(R.id.geoSiteAddress).setText(settings.geoSiteAddress) 66 | basic.findViewById(R.id.pingAddress).setText(settings.pingAddress) 67 | basic.findViewById(R.id.pingTimeout).setText(settings.pingTimeout.toString()) 68 | basic.findViewById(R.id.bypassLan).isChecked = settings.bypassLan 69 | basic.findViewById(R.id.enableIpV6).isChecked = settings.enableIpV6 70 | basic.findViewById(R.id.socksUdp).isChecked = settings.socksUdp 71 | basic.findViewById(R.id.bootAutoStart).isChecked = settings.bootAutoStart 72 | } 73 | 74 | @SuppressLint("SetTextI18n") 75 | private fun setupAdvanced() { 76 | advanced.findViewById(R.id.primaryDns).setText(settings.primaryDns) 77 | advanced.findViewById(R.id.secondaryDns).setText(settings.secondaryDns) 78 | advanced.findViewById(R.id.primaryDnsV6).setText(settings.primaryDnsV6) 79 | advanced.findViewById(R.id.secondaryDnsV6).setText(settings.secondaryDnsV6) 80 | advanced.findViewById(R.id.tunName).setText(settings.tunName) 81 | advanced.findViewById(R.id.tunMtu).setText(settings.tunMtu.toString()) 82 | advanced.findViewById(R.id.tunAddress).setText(settings.tunAddress) 83 | advanced.findViewById(R.id.tunPrefix).setText(settings.tunPrefix.toString()) 84 | advanced.findViewById(R.id.tunAddressV6).setText(settings.tunAddressV6) 85 | advanced.findViewById(R.id.tunPrefixV6).setText(settings.tunPrefixV6.toString()) 86 | } 87 | 88 | private fun saveSettings() { 89 | /** Basic */ 90 | settings.socksAddress = basic.findViewById(R.id.socksAddress).text.toString() 91 | settings.socksPort = basic.findViewById(R.id.socksPort).text.toString() 92 | settings.socksUsername = basic.findViewById(R.id.socksUsername).text.toString() 93 | settings.socksPassword = basic.findViewById(R.id.socksPassword).text.toString() 94 | settings.geoIpAddress = basic.findViewById(R.id.geoIpAddress).text.toString() 95 | settings.geoSiteAddress = basic.findViewById(R.id.geoSiteAddress).text.toString() 96 | settings.pingAddress = basic.findViewById(R.id.pingAddress).text.toString() 97 | settings.pingTimeout = basic.findViewById(R.id.pingTimeout).text.toString().toInt() 98 | settings.bypassLan = basic.findViewById(R.id.bypassLan).isChecked 99 | settings.enableIpV6 = basic.findViewById(R.id.enableIpV6).isChecked 100 | settings.socksUdp = basic.findViewById(R.id.socksUdp).isChecked 101 | settings.bootAutoStart = basic.findViewById(R.id.bootAutoStart).isChecked 102 | 103 | /** Advanced */ 104 | settings.primaryDns = advanced.findViewById(R.id.primaryDns).text.toString() 105 | settings.secondaryDns = advanced.findViewById(R.id.secondaryDns).text.toString() 106 | settings.primaryDnsV6 = advanced.findViewById(R.id.primaryDnsV6).text.toString() 107 | settings.secondaryDnsV6 = advanced.findViewById(R.id.secondaryDnsV6).text.toString() 108 | settings.tunName = advanced.findViewById(R.id.tunName).text.toString() 109 | settings.tunMtu = advanced.findViewById(R.id.tunMtu).text.toString().toInt() 110 | settings.tunAddress = advanced.findViewById(R.id.tunAddress).text.toString() 111 | settings.tunPrefix = advanced.findViewById(R.id.tunPrefix).text.toString().toInt() 112 | settings.tunAddressV6 = advanced.findViewById(R.id.tunAddressV6).text.toString() 113 | settings.tunPrefixV6 = advanced.findViewById(R.id.tunPrefixV6).text.toString().toInt() 114 | 115 | finish() 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/adapter/AppsRoutingAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.adapter 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.LinearLayout 9 | import android.widget.TextView 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.google.android.material.checkbox.MaterialCheckBox 12 | import io.github.saeeddev94.xray.R 13 | import io.github.saeeddev94.xray.dto.AppList 14 | 15 | class AppsRoutingAdapter( 16 | private var context: Context, 17 | private var apps: MutableList, 18 | private var appsRouting: MutableSet, 19 | ) : RecyclerView.Adapter() { 20 | 21 | override fun onCreateViewHolder(container: ViewGroup, type: Int): ViewHolder { 22 | val linearLayout = LinearLayout(context) 23 | val item: View = LayoutInflater.from(context).inflate( 24 | R.layout.item_recycler_exclude, linearLayout, false 25 | ) 26 | return ViewHolder(item) 27 | } 28 | 29 | override fun getItemCount(): Int { 30 | return apps.size 31 | } 32 | 33 | override fun onBindViewHolder(holder: ViewHolder, index: Int) { 34 | val app = apps[index] 35 | val isSelected = appsRouting.contains(app.packageName) 36 | holder.appIcon.setImageDrawable(app.appIcon) 37 | holder.appName.text = app.appName 38 | holder.packageName.text = app.packageName 39 | holder.isSelected.isChecked = isSelected 40 | holder.appContainer.setOnClickListener { 41 | if (isSelected) { 42 | appsRouting.remove(app.packageName) 43 | } else { 44 | appsRouting.add(app.packageName) 45 | } 46 | notifyItemChanged(index) 47 | } 48 | } 49 | 50 | class ViewHolder(item: View) : RecyclerView.ViewHolder(item) { 51 | var appContainer: LinearLayout = item.findViewById(R.id.appContainer) 52 | var appIcon: ImageView = item.findViewById(R.id.appIcon) 53 | var appName: TextView = item.findViewById(R.id.appName) 54 | var packageName: TextView = item.findViewById(R.id.packageName) 55 | var isSelected: MaterialCheckBox = item.findViewById(R.id.isSelected) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/adapter/LinkAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.adapter 2 | 3 | import android.content.res.ColorStateList 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.LinearLayout 8 | import android.widget.TextView 9 | import androidx.cardview.widget.CardView 10 | import androidx.core.content.ContextCompat 11 | import androidx.recyclerview.widget.DiffUtil 12 | import androidx.recyclerview.widget.ListAdapter 13 | import androidx.recyclerview.widget.RecyclerView 14 | import io.github.saeeddev94.xray.R 15 | import io.github.saeeddev94.xray.database.Link 16 | 17 | class LinkAdapter : ListAdapter(diffCallback) { 18 | 19 | var onEditClick: (link: Link) -> Unit = {} 20 | var onDeleteClick: (link: Link) -> Unit = {} 21 | 22 | override fun onCreateViewHolder(parent: ViewGroup, type: Int) = LinkHolder( 23 | LayoutInflater.from(parent.context).inflate( 24 | R.layout.layout_link_item, parent, false 25 | ) 26 | ) 27 | 28 | override fun onBindViewHolder(holder: LinkHolder, position: Int) { 29 | holder.bind(position) 30 | } 31 | 32 | inner class LinkHolder(view: View) : RecyclerView.ViewHolder(view) { 33 | private val card = view.findViewById(R.id.linkCard) 34 | private val name = view.findViewById(R.id.linkName) 35 | private val type = view.findViewById(R.id.linkType) 36 | private val edit = view.findViewById(R.id.linkEdit) 37 | private val delete = view.findViewById(R.id.linkDelete) 38 | 39 | fun bind(index: Int) { 40 | val link = getItem(index) 41 | val color = if (link.isActive) R.color.btnColor else R.color.btnColorDisabled 42 | card.backgroundTintList = ColorStateList.valueOf( 43 | ContextCompat.getColor(card.context, color) 44 | ) 45 | name.text = link.name 46 | type.text = link.type.name 47 | edit.setOnClickListener { onEditClick(link) } 48 | delete.setOnClickListener { onDeleteClick(link) } 49 | } 50 | } 51 | 52 | companion object { 53 | private val diffCallback = object : DiffUtil.ItemCallback() { 54 | override fun areItemsTheSame(oldItem: Link, newItem: Link): Boolean = 55 | oldItem.id == newItem.id 56 | 57 | override fun areContentsTheSame(oldItem: Link, newItem: Link): Boolean = 58 | oldItem == newItem 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/adapter/ProfileAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.adapter 2 | 3 | import android.content.res.ColorStateList 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.LinearLayout 8 | import android.widget.TextView 9 | import androidx.cardview.widget.CardView 10 | import androidx.core.content.ContextCompat 11 | import androidx.recyclerview.widget.RecyclerView 12 | import io.github.saeeddev94.xray.R 13 | import io.github.saeeddev94.xray.Settings 14 | import io.github.saeeddev94.xray.dto.ProfileList 15 | import io.github.saeeddev94.xray.helper.ProfileTouchHelper 16 | import io.github.saeeddev94.xray.viewmodel.ProfileViewModel 17 | import kotlinx.coroutines.CoroutineScope 18 | import kotlinx.coroutines.launch 19 | 20 | class ProfileAdapter( 21 | private val scope: CoroutineScope, 22 | private val settings: Settings, 23 | private val profileViewModel: ProfileViewModel, 24 | private val profiles: ArrayList, 25 | private val profileSelect: (index: Int, profile: ProfileList) -> Unit, 26 | private val profileEdit: (profile: ProfileList) -> Unit, 27 | private val profileDelete: (profile: ProfileList) -> Unit, 28 | ) : RecyclerView.Adapter(), ProfileTouchHelper.ProfileTouchCallback { 29 | 30 | override fun onCreateViewHolder(container: ViewGroup, type: Int): ViewHolder { 31 | val linearLayout = LinearLayout(container.context) 32 | val item: View = LayoutInflater.from(container.context).inflate( 33 | R.layout.item_recycler_main, linearLayout, false 34 | ) 35 | return ViewHolder(item) 36 | } 37 | 38 | override fun getItemCount(): Int { 39 | return profiles.size 40 | } 41 | 42 | override fun onBindViewHolder(holder: ViewHolder, index: Int) { 43 | val profile = profiles[index] 44 | val color = 45 | if (settings.selectedProfile == profile.id) R.color.primaryColor else R.color.btnColor 46 | holder.activeIndicator.backgroundTintList = ColorStateList.valueOf( 47 | ContextCompat.getColor(holder.profileCard.context, color) 48 | ) 49 | holder.profileName.text = profile.name 50 | holder.profileCard.setOnClickListener { 51 | profileSelect(index, profile) 52 | } 53 | holder.profileEdit.setOnClickListener { 54 | profileEdit(profile) 55 | } 56 | holder.profileDelete.setOnClickListener { 57 | profileDelete(profile) 58 | } 59 | } 60 | 61 | override fun onItemMoved(fromPosition: Int, toPosition: Int): Boolean { 62 | profiles.add(toPosition, profiles.removeAt(fromPosition)) 63 | notifyItemMoved(fromPosition, toPosition) 64 | if (toPosition > fromPosition) { 65 | notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1) 66 | } else { 67 | notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1) 68 | } 69 | return true 70 | } 71 | 72 | override fun onItemMoveCompleted(startPosition: Int, endPosition: Int) { 73 | val isMoveUp = startPosition > endPosition 74 | val start = if (isMoveUp) profiles[endPosition + 1] else profiles[endPosition - 1] 75 | val end = profiles[endPosition] 76 | scope.launch { 77 | if (isMoveUp) profileViewModel.moveUp(start.index, end.index, end.id) 78 | else profileViewModel.moveDown(start.index, end.index, end.id) 79 | } 80 | } 81 | 82 | class ViewHolder(item: View) : RecyclerView.ViewHolder(item) { 83 | var activeIndicator: LinearLayout = item.findViewById(R.id.activeIndicator) 84 | var profileCard: CardView = item.findViewById(R.id.profileCard) 85 | var profileName: TextView = item.findViewById(R.id.profileName) 86 | var profileEdit: LinearLayout = item.findViewById(R.id.profileEdit) 87 | var profileDelete: LinearLayout = item.findViewById(R.id.profileDelete) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/adapter/SettingAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.adapter 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.viewpager.widget.PagerAdapter 8 | 9 | class SettingAdapter( 10 | private var context: Context, 11 | private var tabs: List, 12 | private var layouts: List, 13 | private var callback: ViewsReady, 14 | ) : PagerAdapter() { 15 | 16 | private val views: MutableList = mutableListOf() 17 | 18 | override fun instantiateItem(container: ViewGroup, position: Int): Any { 19 | val layoutInflater = 20 | context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 21 | val view = layoutInflater.inflate(layouts[position], container, false) 22 | container.addView(view) 23 | views.add(view) 24 | if (views.size == tabs.size) { 25 | callback.rendered(views) 26 | } 27 | return view 28 | } 29 | 30 | override fun getCount(): Int { 31 | return tabs.size 32 | } 33 | 34 | override fun getPageTitle(position: Int): CharSequence { 35 | return tabs[position] 36 | } 37 | 38 | override fun isViewFromObject(view: View, `object`: Any): Boolean { 39 | return view == `object` 40 | } 41 | 42 | interface ViewsReady { 43 | fun rendered(views: List) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/component/EmptySubmitSearchView.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.component 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.util.AttributeSet 6 | import androidx.appcompat.widget.SearchView 7 | 8 | class EmptySubmitSearchView : SearchView { 9 | 10 | constructor(context: Context) : super(context) 11 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 12 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( 13 | context, 14 | attrs, 15 | defStyleAttr 16 | ) 17 | 18 | @SuppressLint("RestrictedApi") 19 | override fun setOnQueryTextListener(listener: OnQueryTextListener?) { 20 | super.setOnQueryTextListener(listener) 21 | val searchAutoComplete = 22 | this.findViewById(androidx.appcompat.R.id.search_src_text) 23 | searchAutoComplete.setOnEditorActionListener { _, _, _ -> 24 | listener?.onQueryTextSubmit(query.toString()) 25 | return@setOnEditorActionListener true 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/database/Link.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.database 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import androidx.room.TypeConverter 8 | import kotlinx.parcelize.Parcelize 9 | 10 | @Parcelize 11 | @Entity(tableName = "links") 12 | data class Link( 13 | @PrimaryKey(autoGenerate = true) 14 | @ColumnInfo(name = "id") 15 | var id: Long = 0L, 16 | @ColumnInfo(name = "name") 17 | var name: String = "", 18 | @ColumnInfo(name = "address") 19 | var address: String = "", 20 | @ColumnInfo(name = "type") 21 | var type: Type = Type.Json, 22 | @ColumnInfo(name = "is_active") 23 | var isActive: Boolean = false, 24 | @ColumnInfo(name = "user_agent") 25 | var userAgent: String? = null, 26 | ) : Parcelable { 27 | enum class Type(val value: Int) { 28 | Json(0), 29 | Subscription(1); 30 | 31 | class Convertor { 32 | @TypeConverter 33 | fun fromType(type: Type): Int = type.value 34 | 35 | @TypeConverter 36 | fun toType(value: Int): Type = entries[value] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/database/LinkDao.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.database 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import androidx.room.Update 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao 11 | interface LinkDao { 12 | @Query("SELECT * FROM links ORDER BY id ASC") 13 | fun all(): Flow> 14 | 15 | @Query("SELECT * FROM links WHERE is_active = 1 ORDER BY id ASC") 16 | fun tabs(): Flow> 17 | 18 | @Query("SELECT * FROM links WHERE is_active = 1 ORDER BY id ASC") 19 | suspend fun activeLinks(): List 20 | 21 | @Insert 22 | suspend fun insert(link: Link): Long 23 | 24 | @Update 25 | suspend fun update(link: Link) 26 | 27 | @Delete 28 | suspend fun delete(link: Link) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/database/Profile.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.database 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.ForeignKey 7 | import androidx.room.Index 8 | import androidx.room.PrimaryKey 9 | import kotlinx.parcelize.Parcelize 10 | 11 | @Parcelize 12 | @Entity( 13 | tableName = "profiles", 14 | foreignKeys = [ 15 | ForeignKey( 16 | entity = Link::class, 17 | parentColumns = ["id"], 18 | childColumns = ["link_id"], 19 | onUpdate = ForeignKey.CASCADE, 20 | onDelete = ForeignKey.CASCADE, 21 | ), 22 | ], 23 | indices = [ 24 | Index( 25 | name = "profiles_link_id_foreign", 26 | value = ["link_id"] 27 | ), 28 | ], 29 | ) 30 | data class Profile( 31 | @PrimaryKey(autoGenerate = true) 32 | @ColumnInfo(name = "id") 33 | var id: Long = 0L, 34 | @ColumnInfo(name = "link_id") 35 | var linkId: Long? = null, 36 | @ColumnInfo(name = "index") 37 | var index: Int = -1, 38 | @ColumnInfo(name = "name") 39 | var name: String = "", 40 | @ColumnInfo(name = "config") 41 | var config: String = "", 42 | ) : Parcelable 43 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/database/ProfileDao.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.database 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import androidx.room.Transaction 8 | import androidx.room.Update 9 | import io.github.saeeddev94.xray.dto.ProfileList 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | @Dao 13 | interface ProfileDao { 14 | @Query( 15 | "SELECT `profiles`.`id`, `profiles`.`index`, `profiles`.`name`, `profiles`.`link_id` AS `link`" + 16 | " FROM `profiles`" + 17 | " LEFT JOIN `links` ON `profiles`.`link_id` = `links`.`id`" + 18 | " WHERE `links`.`is_active` IS NULL OR `links`.`is_active` = 1" + 19 | " ORDER BY `profiles`.`index` ASC" 20 | ) 21 | fun all(): Flow> 22 | 23 | @Query("SELECT * FROM profiles WHERE link_id = :linkId ORDER BY `index` DESC") 24 | suspend fun linkProfiles(linkId: Long): List 25 | 26 | @Query("SELECT * FROM profiles WHERE `id` = :id") 27 | suspend fun find(id: Long): Profile 28 | 29 | @Insert 30 | suspend fun insert(profile: Profile): Long 31 | 32 | @Update 33 | suspend fun update(profile: Profile) 34 | 35 | @Delete 36 | suspend fun delete(profile: Profile) 37 | 38 | @Query("UPDATE profiles SET `index` = :index WHERE `id` = :id") 39 | suspend fun updateIndex(index: Int, id: Long) 40 | 41 | @Query("UPDATE profiles SET `index` = `index` + 1") 42 | suspend fun fixInsertIndex() 43 | 44 | @Query("UPDATE profiles SET `index` = `index` - 1 WHERE `index` > :index") 45 | suspend fun fixDeleteIndex(index: Int) 46 | 47 | @Query( 48 | "UPDATE profiles" + 49 | " SET `index` = `index` + 1" + 50 | " WHERE `index` >= :start" + 51 | " AND `index` < :end" + 52 | " AND `id` NOT IN (:exclude)" 53 | ) 54 | suspend fun fixMoveUpIndex(start: Int, end: Int, exclude: Long) 55 | 56 | @Query( 57 | "UPDATE profiles" + 58 | " SET `index` = `index` - 1" + 59 | " WHERE `index` > :start" + 60 | " AND `index` <= :end" + 61 | " AND `id` NOT IN (:exclude)" 62 | ) 63 | suspend fun fixMoveDownIndex(start: Int, end: Int, exclude: Long) 64 | 65 | @Transaction 66 | suspend fun create(profile: Profile) { 67 | insert(profile) 68 | fixInsertIndex() 69 | } 70 | 71 | @Transaction 72 | suspend fun remove(profile: Profile) { 73 | delete(profile) 74 | fixDeleteIndex(profile.index) 75 | } 76 | 77 | @Transaction 78 | suspend fun moveUp(start: Int, end: Int, exclude: Long) { 79 | updateIndex(start, exclude) 80 | fixMoveUpIndex(start, end, exclude) 81 | } 82 | 83 | @Transaction 84 | suspend fun moveDown(start: Int, end: Int, exclude: Long) { 85 | updateIndex(start, exclude) 86 | fixMoveDownIndex(end, start, exclude) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/database/XrayDatabase.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.database 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import androidx.room.TypeConverters 8 | import androidx.room.migration.Migration 9 | import androidx.sqlite.db.SupportSQLiteDatabase 10 | 11 | @Database( 12 | entities = [ 13 | Link::class, 14 | Profile::class, 15 | ], 16 | version = 3, 17 | exportSchema = false, 18 | ) 19 | @TypeConverters(Link.Type.Convertor::class) 20 | abstract class XrayDatabase : RoomDatabase() { 21 | 22 | abstract fun linkDao(): LinkDao 23 | abstract fun profileDao(): ProfileDao 24 | 25 | companion object { 26 | private val MIGRATION_1_2 = object : Migration(1, 2) { 27 | override fun migrate(db: SupportSQLiteDatabase) { 28 | // create links table 29 | db.execSQL(""" 30 | CREATE TABLE links ( 31 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 32 | name TEXT NOT NULL, 33 | address TEXT NOT NULL, 34 | type INTEGER NOT NULL, 35 | is_active INTEGER NOT NULL 36 | ) 37 | """) 38 | 39 | // add link_id to profiles table 40 | db.execSQL("ALTER TABLE profiles ADD COLUMN link_id INTEGER") 41 | 42 | // create profiles_new table similar to profiles but with new column (link_id) 43 | db.execSQL(""" 44 | CREATE TABLE profiles_new ( 45 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 46 | link_id INTEGER, 47 | "index" INTEGER NOT NULL, 48 | name TEXT NOT NULL, 49 | config TEXT NOT NULL, 50 | FOREIGN KEY (link_id) REFERENCES links(id) ON UPDATE CASCADE ON DELETE CASCADE 51 | ) 52 | """) 53 | 54 | // create index for link_id 55 | db.execSQL("CREATE INDEX profiles_link_id_foreign ON profiles_new(link_id)") 56 | 57 | // importing data from profile to profiles_new 58 | db.execSQL(""" 59 | INSERT INTO profiles_new (id, link_id, "index", name, config) 60 | SELECT id, link_id, "index", name, config FROM profiles 61 | """) 62 | 63 | // drop profiles table 64 | db.execSQL("DROP TABLE profiles") 65 | 66 | // rename profiles_new to profiles 67 | db.execSQL("ALTER TABLE profiles_new RENAME TO profiles") 68 | } 69 | } 70 | 71 | private val MIGRATION_2_3 = object : Migration(2, 3) { 72 | override fun migrate(db: SupportSQLiteDatabase) { 73 | db.execSQL("ALTER TABLE links ADD COLUMN user_agent TEXT") 74 | } 75 | } 76 | 77 | @Volatile 78 | private var db: XrayDatabase? = null 79 | 80 | fun ref(context: Context): XrayDatabase { 81 | if (db == null) { 82 | synchronized(this) { 83 | if (db == null) { 84 | db = Room.databaseBuilder( 85 | context.applicationContext, 86 | XrayDatabase::class.java, 87 | "xray" 88 | ).addMigrations(MIGRATION_1_2) 89 | .addMigrations(MIGRATION_2_3) 90 | .build() 91 | } 92 | } 93 | } 94 | return db!! 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/dto/AppList.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.dto 2 | 3 | import android.graphics.drawable.Drawable 4 | 5 | data class AppList( 6 | var appIcon: Drawable, 7 | var appName: String, 8 | var packageName: String, 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/dto/ProfileList.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.dto 2 | 3 | data class ProfileList( 4 | var id: Long, 5 | var index: Int, 6 | var name: String, 7 | var link: Long?, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/dto/XrayConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.dto 2 | 3 | data class XrayConfig( 4 | val dir: String, 5 | val file: String, 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/fragment/LinkFormFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.fragment 2 | 3 | import android.app.Dialog 4 | import android.content.DialogInterface 5 | import android.os.Bundle 6 | import android.widget.EditText 7 | import android.widget.LinearLayout 8 | import android.widget.RadioButton 9 | import android.widget.RadioGroup 10 | import android.widget.Toast 11 | import androidx.fragment.app.DialogFragment 12 | import androidx.fragment.app.FragmentActivity 13 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 14 | import com.google.android.material.materialswitch.MaterialSwitch 15 | import com.google.android.material.radiobutton.MaterialRadioButton 16 | import io.github.saeeddev94.xray.R 17 | import io.github.saeeddev94.xray.database.Link 18 | import java.net.URI 19 | import kotlin.reflect.cast 20 | 21 | class LinkFormFragment( 22 | private val link: Link, 23 | private val onConfirm: () -> Unit, 24 | ) : DialogFragment() { 25 | 26 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 27 | return openLink(requireActivity()) 28 | } 29 | 30 | override fun onDismiss(dialog: DialogInterface) { 31 | super.onDismiss(dialog) 32 | requireActivity().finish() 33 | } 34 | 35 | private fun openLink(context: FragmentActivity): Dialog = 36 | MaterialAlertDialogBuilder(context).apply { 37 | val layout = context.layoutInflater.inflate( 38 | R.layout.layout_link_form, 39 | LinearLayout(context) 40 | ) 41 | 42 | val typeRadioGroup = layout.findViewById(R.id.typeRadioGroup) 43 | val nameEditText = layout.findViewById(R.id.nameEditText) 44 | val addressEditText = layout.findViewById(R.id.addressEditText) 45 | val userAgentEditText = layout.findViewById(R.id.userAgentEditText) 46 | val isActiveSwitch = layout.findViewById(R.id.isActiveSwitch) 47 | Link.Type.entries.forEach { 48 | val radio = MaterialRadioButton(context) 49 | radio.text = it.name 50 | radio.tag = it 51 | typeRadioGroup.addView(radio) 52 | if (it == link.type) typeRadioGroup.check(radio.id) 53 | } 54 | nameEditText.setText(link.name) 55 | addressEditText.setText(link.address) 56 | userAgentEditText.setText(link.userAgent) 57 | isActiveSwitch.isChecked = if (link.id == 0L) { 58 | true 59 | } else { 60 | link.isActive 61 | } 62 | 63 | setView(layout) 64 | setTitle( 65 | if (link.id == 0L) context.getString(R.string.newLink) 66 | else context.getString(R.string.editLink) 67 | ) 68 | setPositiveButton( 69 | if (link.id == 0L) context.getString(R.string.createLink) 70 | else context.getString(R.string.updateLink) 71 | ) { _, _ -> 72 | val address = addressEditText.text.toString() 73 | val typeRadioButton = typeRadioGroup.findViewById( 74 | typeRadioGroup.checkedRadioButtonId 75 | ) 76 | val uri = runCatching { URI(address) }.getOrNull() 77 | val invalidLink = context.getString(R.string.invalidLink) 78 | val onlyHttps = context.getString(R.string.onlyHttps) 79 | if (uri == null) { 80 | Toast.makeText(context, invalidLink, Toast.LENGTH_SHORT).show() 81 | return@setPositiveButton 82 | } 83 | if (uri.scheme != "https") { 84 | Toast.makeText(context, onlyHttps, Toast.LENGTH_SHORT).show() 85 | return@setPositiveButton 86 | } 87 | link.type = Link.Type::class.cast(typeRadioButton.tag) 88 | link.name = nameEditText.text.toString() 89 | link.address = address 90 | link.userAgent = userAgentEditText.text.toString().ifBlank { null } 91 | link.isActive = isActiveSwitch.isChecked 92 | onConfirm() 93 | } 94 | setNegativeButton(context.getString(R.string.closeLink)) { _, _ -> } 95 | }.create() 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/helper/DownloadHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.helper 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.withContext 7 | import java.io.File 8 | import java.io.FileOutputStream 9 | import java.io.IOException 10 | import java.io.InputStream 11 | import java.io.OutputStream 12 | import java.net.HttpURLConnection 13 | import java.net.URL 14 | 15 | class DownloadHelper( 16 | private val scope: CoroutineScope, 17 | private val url: String, 18 | private val file: File, 19 | private val callback: DownloadListener, 20 | ) { 21 | 22 | fun start() { 23 | scope.launch(Dispatchers.IO) { 24 | var input: InputStream? = null 25 | var output: OutputStream? = null 26 | var connection: HttpURLConnection? = null 27 | 28 | try { 29 | connection = URL(url).openConnection() as HttpURLConnection 30 | connection.connect() 31 | 32 | if (connection.responseCode != HttpURLConnection.HTTP_OK) { 33 | throw Exception("Expected HTTP ${HttpURLConnection.HTTP_OK} but received HTTP ${connection.responseCode}") 34 | } 35 | 36 | input = connection.inputStream 37 | output = FileOutputStream(file) 38 | 39 | val fileLength = connection.contentLength 40 | val data = ByteArray(4096) 41 | var total: Long = 0 42 | var count: Int 43 | while (input.read(data).also { count = it } != -1) { 44 | total += count.toLong() 45 | if (fileLength > 0) { 46 | val progress = (total * 100 / fileLength).toInt() 47 | withContext(Dispatchers.Main) { 48 | callback.onProgress(progress) 49 | } 50 | } 51 | output.write(data, 0, count) 52 | } 53 | withContext(Dispatchers.Main) { 54 | callback.onComplete() 55 | } 56 | } catch (exception: Exception) { 57 | withContext(Dispatchers.Main) { 58 | callback.onError(exception) 59 | } 60 | } finally { 61 | try { 62 | output?.close() 63 | input?.close() 64 | } catch (_: IOException) { 65 | } 66 | 67 | connection?.disconnect() 68 | } 69 | } 70 | } 71 | 72 | interface DownloadListener { 73 | fun onProgress(progress: Int) 74 | fun onError(exception: Exception) 75 | fun onComplete() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/helper/FileHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.helper 2 | 3 | import java.io.File 4 | 5 | class FileHelper { 6 | 7 | companion object { 8 | fun createOrUpdate(file: File, content: String) { 9 | val fileContent = if (file.exists()) file.bufferedReader().use { it.readText() } else "" 10 | if (content != fileContent) file.writeText(content) 11 | } 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/helper/HttpHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.helper 2 | 3 | import io.github.saeeddev94.xray.BuildConfig 4 | import io.github.saeeddev94.xray.Settings 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.withContext 9 | import java.net.Authenticator 10 | import java.net.HttpURLConnection 11 | import java.net.InetSocketAddress 12 | import java.net.PasswordAuthentication 13 | import java.net.Proxy 14 | import java.net.URL 15 | 16 | class HttpHelper( 17 | val scope: CoroutineScope, 18 | val settings: Settings, 19 | ) { 20 | 21 | companion object { 22 | private fun getConnection( 23 | link: String, 24 | method: String = "GET", 25 | proxy: Proxy? = null, 26 | timeout: Int = 5000, 27 | userAgent: String? = null, 28 | ): HttpURLConnection { 29 | val url = URL(link) 30 | val connection = if (proxy == null) { 31 | url.openConnection() as HttpURLConnection 32 | } else { 33 | url.openConnection(proxy) as HttpURLConnection 34 | } 35 | connection.requestMethod = method 36 | connection.connectTimeout = timeout 37 | connection.readTimeout = timeout 38 | userAgent?.let { connection.setRequestProperty("User-Agent", it) } 39 | connection.setRequestProperty("Connection", "close") 40 | return connection 41 | } 42 | 43 | suspend fun get(link: String, userAgent: String? = null): String { 44 | return withContext(Dispatchers.IO) { 45 | val defaultUserAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME}" 46 | val connection = getConnection(link, userAgent = userAgent ?: defaultUserAgent) 47 | var responseCode = 0 48 | val responseBody = try { 49 | connection.connect() 50 | responseCode = connection.responseCode 51 | connection.inputStream.bufferedReader().use { it.readText() } 52 | } catch (error: Exception) { 53 | null 54 | } finally { 55 | connection.disconnect() 56 | } 57 | if (responseCode != HttpURLConnection.HTTP_OK || responseBody == null) { 58 | throw Exception("HTTP Error: $responseCode") 59 | } 60 | responseBody 61 | } 62 | } 63 | } 64 | 65 | fun measureDelay(callback: (result: String) -> Unit) { 66 | scope.launch(Dispatchers.IO) { 67 | val start = System.currentTimeMillis() 68 | val connection = getConnection() 69 | var result = "HTTP {status}, {delay} ms" 70 | 71 | result = try { 72 | setSocksAuth(getSocksAuth()) 73 | val responseCode = connection.responseCode 74 | result.replace("{status}", "$responseCode") 75 | } catch (error: Exception) { 76 | error.message ?: "Http delay measure failed" 77 | } finally { 78 | connection.disconnect() 79 | setSocksAuth(null) 80 | } 81 | 82 | val delay = System.currentTimeMillis() - start 83 | withContext(Dispatchers.Main) { 84 | callback(result.replace("{delay}", "$delay")) 85 | } 86 | } 87 | } 88 | 89 | private suspend fun getConnection(): HttpURLConnection { 90 | return withContext(Dispatchers.IO) { 91 | val link = settings.pingAddress 92 | val method = "HEAD" 93 | val address = InetSocketAddress(settings.socksAddress, settings.socksPort.toInt()) 94 | val proxy = Proxy(Proxy.Type.SOCKS, address) 95 | val timeout = settings.pingTimeout * 1000 96 | 97 | getConnection(link, method, proxy, timeout) 98 | } 99 | } 100 | 101 | private fun getSocksAuth(): Authenticator? { 102 | if ( 103 | settings.socksUsername.trim().isEmpty() || settings.socksPassword.trim().isEmpty() 104 | ) return null 105 | return object : Authenticator() { 106 | override fun getPasswordAuthentication(): PasswordAuthentication { 107 | return PasswordAuthentication( 108 | settings.socksUsername, 109 | settings.socksPassword.toCharArray() 110 | ) 111 | } 112 | } 113 | } 114 | 115 | private fun setSocksAuth(auth: Authenticator?) { 116 | Authenticator.setDefault(auth) 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/helper/IntentHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.helper 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | 6 | class IntentHelper { 7 | companion object { 8 | fun getParcelable(intent: Intent, name: String, clazz: Class): T? { 9 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 10 | intent.getParcelableExtra(name, clazz) 11 | } else { 12 | @Suppress("deprecation") 13 | intent.getParcelableExtra(name) 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/helper/LinkHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.helper 2 | 3 | import XrayCore.XrayCore 4 | import android.util.Base64 5 | import io.github.saeeddev94.xray.Settings 6 | import org.json.JSONArray 7 | import org.json.JSONException 8 | import org.json.JSONObject 9 | import java.net.URI 10 | 11 | class LinkHelper( 12 | private val settings: Settings, 13 | link: String 14 | ) { 15 | 16 | private val success: Boolean 17 | private val outbound: JSONObject? 18 | private var remark: String = REMARK_DEFAULT 19 | 20 | init { 21 | val base64: String = XrayCore.json(link) 22 | val decoded = tryDecodeBase64(base64) 23 | val response = try { 24 | JSONObject(decoded) 25 | } catch (error: JSONException) { 26 | JSONObject() 27 | } 28 | val data = response.optJSONObject("data") ?: JSONObject() 29 | val outbounds = data.optJSONArray("outbounds") ?: JSONArray() 30 | success = response.optBoolean("success", false) 31 | outbound = if (outbounds.length() > 0) outbounds[0] as JSONObject else null 32 | } 33 | 34 | companion object { 35 | const val REMARK_DEFAULT = "New Profile" 36 | const val LINK_DEFAULT = "New Link" 37 | 38 | fun remark(uri: URI, default: String = ""): String { 39 | val name = uri.fragment ?: "" 40 | return name.ifEmpty { default } 41 | } 42 | 43 | fun tryDecodeBase64(value: String): String { 44 | return runCatching { 45 | val byteArray = Base64.decode(value, Base64.DEFAULT) 46 | String(byteArray) 47 | }.getOrNull() ?: value 48 | } 49 | } 50 | 51 | fun isValid(): Boolean { 52 | return success && outbound != null 53 | } 54 | 55 | fun json(): String { 56 | return config().toString(2) + "\n" 57 | } 58 | 59 | fun remark(): String { 60 | return remark 61 | } 62 | 63 | private fun log(): JSONObject { 64 | val log = JSONObject() 65 | log.put("loglevel", "warning") 66 | return log 67 | } 68 | 69 | private fun dns(): JSONObject { 70 | val dns = JSONObject() 71 | val servers = JSONArray() 72 | servers.put(settings.primaryDns) 73 | servers.put(settings.secondaryDns) 74 | dns.put("servers", servers) 75 | return dns 76 | } 77 | 78 | private fun inbounds(): JSONArray { 79 | val inbounds = JSONArray() 80 | 81 | val socks = JSONObject() 82 | socks.put("listen", settings.socksAddress) 83 | socks.put("port", settings.socksPort.toInt()) 84 | socks.put("protocol", "socks") 85 | 86 | val socksSettings = JSONObject() 87 | socksSettings.put("udp", true) 88 | if ( 89 | settings.socksUsername.trim().isNotEmpty() && 90 | settings.socksPassword.trim().isNotEmpty() 91 | ) { 92 | val account = JSONObject() 93 | account.put("user", settings.socksUsername) 94 | account.put("pass", settings.socksPassword) 95 | val accounts = JSONArray() 96 | accounts.put(account) 97 | 98 | socksSettings.put("auth", "password") 99 | socksSettings.put("accounts", accounts) 100 | } 101 | 102 | val sniffing = JSONObject() 103 | sniffing.put("enabled", true) 104 | val sniffingDestOverride = JSONArray() 105 | sniffingDestOverride.put("http") 106 | sniffingDestOverride.put("tls") 107 | sniffingDestOverride.put("quic") 108 | sniffing.put("destOverride", sniffingDestOverride) 109 | 110 | socks.put("settings", socksSettings) 111 | socks.put("sniffing", sniffing) 112 | socks.put("tag", "socks") 113 | 114 | inbounds.put(socks) 115 | 116 | return inbounds 117 | } 118 | 119 | private fun outbounds(): JSONArray { 120 | val outbounds = JSONArray() 121 | 122 | val proxy = JSONObject(outbound!!.toString()) 123 | if (proxy.has("sendThrough")) { 124 | remark = proxy.optString("sendThrough", REMARK_DEFAULT) 125 | proxy.remove("sendThrough") 126 | } 127 | proxy.put("tag", "proxy") 128 | 129 | val direct = JSONObject() 130 | direct.put("protocol", "freedom") 131 | direct.put("tag", "direct") 132 | 133 | val block = JSONObject() 134 | block.put("protocol", "blackhole") 135 | block.put("tag", "block") 136 | 137 | outbounds.put(proxy) 138 | outbounds.put(direct) 139 | outbounds.put(block) 140 | 141 | return outbounds 142 | } 143 | 144 | private fun routing(): JSONObject { 145 | val routing = JSONObject() 146 | routing.put("domainStrategy", "IPIfNonMatch") 147 | 148 | val rules = JSONArray() 149 | 150 | val proxyDns = JSONObject() 151 | val proxyDnsIp = JSONArray() 152 | proxyDnsIp.put(settings.primaryDns) 153 | proxyDnsIp.put(settings.secondaryDns) 154 | proxyDns.put("ip", proxyDnsIp) 155 | proxyDns.put("port", 53) 156 | proxyDns.put("outboundTag", "proxy") 157 | 158 | val directPrivate = JSONObject() 159 | val directPrivateIp = JSONArray() 160 | directPrivateIp.put("geoip:private") 161 | directPrivate.put("ip", directPrivateIp) 162 | directPrivate.put("outboundTag", "direct") 163 | 164 | rules.put(proxyDns) 165 | rules.put(directPrivate) 166 | 167 | routing.put("rules", rules) 168 | 169 | return routing 170 | } 171 | 172 | private fun config(): JSONObject { 173 | val config = JSONObject() 174 | config.put("log", log()) 175 | config.put("dns", dns()) 176 | config.put("inbounds", inbounds()) 177 | config.put("outbounds", outbounds()) 178 | config.put("routing", routing()) 179 | return config 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/helper/ProfileTouchHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.helper 2 | 3 | import androidx.recyclerview.widget.ItemTouchHelper 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | class ProfileTouchHelper(private var adapter: ProfileTouchCallback) : ItemTouchHelper.Callback() { 7 | 8 | private var startPosition: Int = -1 9 | 10 | override fun getMovementFlags( 11 | recyclerView: RecyclerView, 12 | viewHolder: RecyclerView.ViewHolder 13 | ): Int = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) 14 | 15 | override fun onMove( 16 | recyclerView: RecyclerView, 17 | source: RecyclerView.ViewHolder, 18 | target: RecyclerView.ViewHolder 19 | ): Boolean = adapter.onItemMoved(source.adapterPosition, target.adapterPosition) 20 | 21 | override fun onSwiped( 22 | viewHolder: RecyclerView.ViewHolder, 23 | direction: Int 24 | ) { 25 | } 26 | 27 | override fun onSelectedChanged( 28 | viewHolder: RecyclerView.ViewHolder?, 29 | actionState: Int 30 | ) { 31 | if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) return 32 | startPosition = viewHolder!!.adapterPosition 33 | } 34 | 35 | override fun clearView( 36 | recyclerView: RecyclerView, 37 | viewHolder: RecyclerView.ViewHolder 38 | ) { 39 | val endPosition = viewHolder.adapterPosition 40 | adapter.onItemMoveCompleted(startPosition, endPosition) 41 | } 42 | 43 | interface ProfileTouchCallback { 44 | fun onItemMoved(fromPosition: Int, toPosition: Int): Boolean 45 | fun onItemMoveCompleted(startPosition: Int, endPosition: Int) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/receiver/BootReceiver.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import io.github.saeeddev94.xray.Settings 7 | import io.github.saeeddev94.xray.service.TProxyService 8 | 9 | class BootReceiver : BroadcastReceiver() { 10 | 11 | override fun onReceive(context: Context?, intent: Intent?) { 12 | if (context == null || intent == null || intent.action != Intent.ACTION_BOOT_COMPLETED) return 13 | val settings = Settings(context) 14 | if (!settings.bootAutoStart) return 15 | TProxyService.start(context) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/receiver/VpnActionReceiver.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import io.github.saeeddev94.xray.R 7 | import io.github.saeeddev94.xray.service.TProxyService 8 | import io.github.saeeddev94.xray.service.VpnTileService 9 | 10 | class VpnActionReceiver : BroadcastReceiver() { 11 | 12 | override fun onReceive(context: Context?, intent: Intent?) { 13 | if (context == null || intent == null) return 14 | val allowed = listOf( 15 | TProxyService.START_VPN_SERVICE_ACTION_NAME, 16 | TProxyService.STOP_VPN_SERVICE_ACTION_NAME, 17 | TProxyService.NEW_CONFIG_SERVICE_ACTION_NAME, 18 | ) 19 | val action = intent.action ?: "" 20 | val label = intent.getStringExtra("profile") ?: context.getString(R.string.appName) 21 | if (!allowed.contains(action)) return 22 | Intent(context, VpnTileService::class.java).also { 23 | it.putExtra("action", action) 24 | it.putExtra("label", label) 25 | context.startService(it) 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/repository/LinkRepository.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.repository 2 | 3 | import io.github.saeeddev94.xray.database.Link 4 | import io.github.saeeddev94.xray.database.LinkDao 5 | 6 | class LinkRepository(private val linkDao: LinkDao) { 7 | 8 | val all = linkDao.all() 9 | val tabs = linkDao.tabs() 10 | 11 | suspend fun activeLinks(): List { 12 | return linkDao.activeLinks() 13 | } 14 | 15 | suspend fun insert(link: Link) { 16 | linkDao.insert(link) 17 | } 18 | 19 | suspend fun update(link: Link) { 20 | linkDao.update(link) 21 | } 22 | 23 | suspend fun delete(link: Link) { 24 | linkDao.delete(link) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/repository/ProfileRepository.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.repository 2 | 3 | import io.github.saeeddev94.xray.database.Profile 4 | import io.github.saeeddev94.xray.database.ProfileDao 5 | 6 | class ProfileRepository(private val profileDao: ProfileDao) { 7 | 8 | val all = profileDao.all() 9 | 10 | suspend fun linkProfiles(linkId: Long): List { 11 | return profileDao.linkProfiles(linkId) 12 | } 13 | 14 | suspend fun find(id: Long): Profile { 15 | return profileDao.find(id) 16 | } 17 | 18 | suspend fun update(profile: Profile) { 19 | profileDao.update(profile) 20 | } 21 | 22 | suspend fun create(profile: Profile) { 23 | profileDao.create(profile) 24 | } 25 | 26 | suspend fun remove(profile: Profile) { 27 | profileDao.remove(profile) 28 | } 29 | 30 | suspend fun moveUp(start: Int, end: Int, exclude: Long) { 31 | profileDao.moveUp(start, end, exclude) 32 | } 33 | 34 | suspend fun moveDown(start: Int, end: Int, exclude: Long) { 35 | profileDao.moveDown(start, end, exclude) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/service/VpnTileService.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.service 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.SharedPreferences 7 | import android.graphics.drawable.Icon 8 | import android.service.quicksettings.Tile 9 | import android.service.quicksettings.TileService 10 | import androidx.core.content.edit 11 | import io.github.saeeddev94.xray.R 12 | 13 | class VpnTileService : TileService() { 14 | 15 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 16 | requestListeningState(this, ComponentName(this, VpnTileService::class.java)) 17 | val action = intent?.getStringExtra("action") ?: "" 18 | val label = intent?.getStringExtra("label") ?: "" 19 | val sharedPref = sharedPref() 20 | sharedPref.edit { 21 | putString("action", action) 22 | putString("label", label) 23 | } 24 | handleUpdate(action, label) 25 | return START_STICKY 26 | } 27 | 28 | override fun onStartListening() { 29 | super.onStartListening() 30 | handleUpdate() 31 | } 32 | 33 | override fun onClick() { 34 | super.onClick() 35 | when (qsTile?.state) { 36 | Tile.STATE_INACTIVE -> TProxyService.start(applicationContext) 37 | Tile.STATE_ACTIVE -> TProxyService.stop(applicationContext) 38 | } 39 | } 40 | 41 | private fun handleUpdate(newAction: String? = null, newLabel: String? = null) { 42 | val sharedPref = sharedPref() 43 | val action = newAction ?: sharedPref.getString("action", "")!! 44 | val label = newLabel ?: sharedPref.getString("label", "")!! 45 | if (action.isNotEmpty() && label.isNotEmpty()) { 46 | when (action) { 47 | TProxyService.START_VPN_SERVICE_ACTION_NAME, 48 | TProxyService.NEW_CONFIG_SERVICE_ACTION_NAME -> updateTile(Tile.STATE_ACTIVE, label) 49 | 50 | TProxyService.STOP_VPN_SERVICE_ACTION_NAME -> updateTile(Tile.STATE_INACTIVE, label) 51 | } 52 | } 53 | } 54 | 55 | private fun updateTile(newState: Int, newLabel: String) { 56 | val tile = qsTile ?: return 57 | tile.apply { 58 | state = newState 59 | label = newLabel 60 | icon = Icon.createWithResource(applicationContext, R.drawable.vpn_key) 61 | updateTile() 62 | } 63 | } 64 | 65 | private fun sharedPref(): SharedPreferences { 66 | return getSharedPreferences("vpn_tile", Context.MODE_PRIVATE) 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/viewmodel/LinkViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.viewmodel 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import io.github.saeeddev94.xray.Xray 7 | import io.github.saeeddev94.xray.database.Link 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.flow.SharingStarted 10 | import kotlinx.coroutines.flow.flowOn 11 | import kotlinx.coroutines.flow.stateIn 12 | import kotlinx.coroutines.launch 13 | 14 | class LinkViewModel(application: Application) : AndroidViewModel(application) { 15 | 16 | private val linkRepository by lazy { getApplication().linkRepository } 17 | 18 | val tabs = linkRepository.tabs.flowOn(Dispatchers.IO).stateIn( 19 | viewModelScope, 20 | SharingStarted.Eagerly, 21 | listOf(), 22 | ) 23 | val links = linkRepository.all.flowOn(Dispatchers.IO).stateIn( 24 | viewModelScope, 25 | SharingStarted.Eagerly, 26 | listOf(), 27 | ) 28 | 29 | suspend fun activeLinks(): List { 30 | return linkRepository.activeLinks() 31 | } 32 | 33 | fun insert(link: Link) = viewModelScope.launch { 34 | linkRepository.insert(link) 35 | } 36 | 37 | fun update(link: Link) = viewModelScope.launch { 38 | linkRepository.update(link) 39 | } 40 | 41 | fun delete(link: Link) = viewModelScope.launch { 42 | linkRepository.delete(link) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/saeeddev94/xray/viewmodel/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.saeeddev94.xray.viewmodel 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import io.github.saeeddev94.xray.Xray 7 | import io.github.saeeddev94.xray.database.Profile 8 | import io.github.saeeddev94.xray.dto.ProfileList 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.flow.MutableSharedFlow 11 | import kotlinx.coroutines.flow.SharingStarted 12 | import kotlinx.coroutines.flow.flowOn 13 | import kotlinx.coroutines.flow.stateIn 14 | import kotlinx.coroutines.launch 15 | 16 | class ProfileViewModel(application: Application) : AndroidViewModel(application) { 17 | 18 | private val profileRepository by lazy { getApplication().profileRepository } 19 | 20 | val profiles = profileRepository.all.flowOn(Dispatchers.IO).stateIn( 21 | viewModelScope, 22 | SharingStarted.Eagerly, 23 | listOf(), 24 | ) 25 | val filtered = MutableSharedFlow>() 26 | 27 | fun next(link: Long) = viewModelScope.launch { 28 | val list = profiles.value.filter { link == 0L || link == it.link } 29 | filtered.emit(list) 30 | } 31 | 32 | suspend fun linkProfiles(linkId: Long): List { 33 | return profileRepository.linkProfiles(linkId) 34 | } 35 | 36 | suspend fun find(id: Long): Profile { 37 | return profileRepository.find(id) 38 | } 39 | 40 | suspend fun create(profile: Profile) { 41 | return profileRepository.create(profile) 42 | } 43 | 44 | suspend fun update(profile: Profile) { 45 | profileRepository.update(profile) 46 | } 47 | 48 | suspend fun remove(profile: Profile) { 49 | profileRepository.remove(profile) 50 | } 51 | 52 | suspend fun moveUp(start: Int, end: Int, exclude: Long) { 53 | profileRepository.moveUp(start, end, exclude) 54 | } 55 | 56 | suspend fun moveDown(start: Int, end: Int, exclude: Long) { 57 | profileRepository.moveDown(start, end, exclude) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/jni/Android.mk: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | include $(call all-subdir-makefiles) 17 | -------------------------------------------------------------------------------- /app/src/main/jni/Application.mk: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | APP_OPTIM := release 17 | APP_PLATFORM := android-26 18 | APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 19 | APP_CFLAGS := -O3 -DPKGNAME=io/github/saeeddev94/xray/service 20 | APP_CPPFLAGS := -O3 -std=c++11 21 | NDK_TOOLCHAIN_VERSION := clang 22 | LOCAL_LDFLAGS += -Wl,--build-id=none -Wl,--hash-style=gnu 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_adb.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_alt_route.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_content_copy.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_delete.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_done.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_download.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_edit.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_file_open.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_folder_open.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_link.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_refresh.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_vpn_key.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_vpn_lock.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_xray.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/vpn_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/app/src/main/res/drawable/vpn_key.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_apps_routing.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 13 | 18 | 24 | 25 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_links.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 13 | 18 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_logs.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 13 | 18 | 19 | 24 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 14 | 17 | 22 | 23 | 27 | 34 | 40 |