├── .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 |
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 | 


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 | [](https://github.com/SaeedDev94/Xray/actions)
17 |
18 |
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 |
46 |
56 |
65 |
66 |
67 |
68 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_profile.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
13 |
18 |
19 |
23 |
27 |
32 |
37 |
43 |
48 |
49 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
14 |
19 |
25 |
26 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_recycler_exclude.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
18 |
23 |
24 |
29 |
33 |
37 |
38 |
42 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_recycler_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
21 |
28 |
36 |
43 |
44 |
49 |
59 |
64 |
65 |
75 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_link_form.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
17 |
24 |
25 |
29 |
36 |
37 |
41 |
48 |
49 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_link_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
14 |
19 |
27 |
34 |
35 |
40 |
46 |
52 |
53 |
63 |
69 |
70 |
80 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/loading_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_apps_routing.xml:
--------------------------------------------------------------------------------
1 |
2 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_links.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_logs.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_profile.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/app/src/main/res/mipmap/banner.png
--------------------------------------------------------------------------------
/app/src/main/res/values/array.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - 0.0.0.0/5
5 | - 8.0.0.0/7
6 | - 11.0.0.0/8
7 | - 12.0.0.0/6
8 | - 16.0.0.0/4
9 | - 32.0.0.0/3
10 | - 64.0.0.0/2
11 | - 128.0.0.0/3
12 | - 160.0.0.0/5
13 | - 168.0.0.0/6
14 | - 172.0.0.0/12
15 | - 172.32.0.0/11
16 | - 172.64.0.0/10
17 | - 172.128.0.0/9
18 | - 173.0.0.0/8
19 | - 174.0.0.0/7
20 | - 176.0.0.0/4
21 | - 192.0.0.0/9
22 | - 192.128.0.0/11
23 | - 192.160.0.0/13
24 | - 192.169.0.0/16
25 | - 192.170.0.0/15
26 | - 192.172.0.0/14
27 | - 192.176.0.0/12
28 | - 192.192.0.0/10
29 | - 193.0.0.0/8
30 | - 194.0.0.0/7
31 | - 196.0.0.0/6
32 | - 200.0.0.0/5
33 | - 208.0.0.0/4
34 | - 240.0.0.0/4
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #009966
4 | #636363
5 | #313131
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Xray
3 | -
4 | Open drawer
5 | Close drawer
6 | From Clipboard
7 | Loading…
8 | START
9 | STOP
10 | Not Connected
11 | Connected, tap to check connection
12 | Testing…
13 | SaeedDev94/Xray
14 | XTLS/Xray-core
15 | heiher/hev-socks5-tunnel
16 | 2.10.0
17 | Assets
18 | Links
19 | Logs
20 | Apps Routing
21 | Settings
22 | GeoIP
23 | GeoSite
24 | Refresh Links
25 | New Link
26 | Edit Link
27 | Name
28 | Address
29 | User-Agent
30 | Is Active?
31 | Create
32 | Update
33 | Close
34 | New Profile
35 | Edit Profile
36 | Name
37 | Config
38 | Edit Icon
39 | Delete Icon
40 | Download Icon
41 | File Open Icon
42 | Copy Logs
43 | Delete Logs
44 | App Icon
45 | Open Json
46 | Save
47 | Mode: Exclude
48 | Mode: Include
49 | Save Settings
50 | Save Profile
51 | Invalid Profile
52 | Invalid Link
53 | HTTP is forbidden
54 | Only HTTPS is acceptable
55 | Cancel
56 | Ignore
57 | OK
58 |
59 |
60 | Socks Address
61 | Socks Port
62 | Socks Username
63 | Socks Password
64 | GeoIP Address
65 | GeoSite Address
66 | Ping Address
67 | Ping Timeout
68 | Bypass LAN (IPv4)
69 | Enable IPv6
70 | Socks UDP
71 | Boot Auto Start
72 |
73 |
74 | Primary DNS (v4)
75 | Secondary DNS (v4)
76 | Primary DNS (v6)
77 | Secondary DNS (v6)
78 | Tun Name
79 | Tun Mtu
80 | Tun Address (IPv4)
81 | Prefix
82 | Tun Address (IPv6)
83 | Prefix
84 |
85 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
8 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/versionCode.txt:
--------------------------------------------------------------------------------
1 | 780
2 |
--------------------------------------------------------------------------------
/build-xray.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Update repo
4 | apt-get update
5 | apt-get install -y ca-certificates
6 | echo "deb https://deb.debian.org/debian bookworm-backports main" > /etc/apt/sources.list.d/backports.list
7 | apt-get update || apt-get update
8 | apt-get dist-upgrade -y
9 |
10 | # Tools version
11 | ANDROID_PLATFORM_VERSION="android-35"
12 | ANDROID_SDK_VERSION="35.0.0"
13 | ANDROID_NDK_VERSION="27.1.12297006"
14 | JAVA_VERSION="17"
15 | GRADLE_VERSION="8.11.1"
16 | GO_VERSION="go1.24.3"
17 | GO_MOBILE_VERSION="v0.0.0-20250506005352-78cd7a343bde"
18 |
19 | # Install Tools
20 | apt-get install -t bookworm-backports -y golang-go
21 | apt-get install -y git openjdk-$JAVA_VERSION-jdk-headless sdkmanager wget unzip gcc libc-dev
22 | sdkmanager "platform-tools" "platforms;$ANDROID_PLATFORM_VERSION" "build-tools;$ANDROID_SDK_VERSION"
23 | sdkmanager --install "ndk;$ANDROID_NDK_VERSION" --channel=3
24 |
25 | # Define dirs
26 | HOME_DIR="/home/vagrant"
27 | BUILD_DIR="$HOME_DIR/build"
28 | REPO_DIR="$BUILD_DIR/io.github.saeeddev94.xray"
29 | GRADLE_DIR="$BUILD_DIR/gradle"
30 | SRC_DIR="$BUILD_DIR/srclib"
31 | GO_ROOT_DIR="$SRC_DIR/go"
32 | GO_PATH_DIR="$HOME_DIR/go"
33 |
34 | # Create directories
35 | mkdir -p $GRADLE_DIR
36 | mkdir -p $SRC_DIR
37 |
38 | # Download gradle
39 | pushd $GRADLE_DIR
40 | GRADLE_ARCHIVE="gradle-$GRADLE_VERSION-bin.zip"
41 | wget "https://services.gradle.org/distributions/$GRADLE_ARCHIVE"
42 | unzip "$GRADLE_ARCHIVE"
43 | rm "$GRADLE_ARCHIVE"
44 | mv * "$GRADLE_VERSION"
45 | popd
46 |
47 | # Build go
48 | git clone https://github.com/golang/go.git $GO_ROOT_DIR
49 | pushd $GO_ROOT_DIR
50 | git checkout "$GO_VERSION"
51 | cd src
52 | ./make.bash
53 | popd
54 |
55 | # Set vars
56 | export JAVA_HOME="/usr/lib/jvm/java-$JAVA_VERSION-openjdk-amd64"
57 | export ANDROID_HOME="/opt/android-sdk"
58 | export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$ANDROID_NDK_VERSION"
59 | export GOROOT="$GO_ROOT_DIR"
60 | export GOPATH="$GO_PATH_DIR"
61 |
62 | # Set path
63 | export PATH="$JAVA_HOME/bin:$PATH"
64 | export PATH="$GRADLE_DIR/$GRADLE_VERSION/bin:$PATH"
65 | export PATH="$ANDROID_HOME/platform-tools:$PATH"
66 | export PATH="$ANDROID_HOME/build-tools/$ANDROID_SDK_VERSION:$PATH"
67 | export PATH="$GOROOT/bin:$PATH"
68 | export PATH="$GOPATH/bin:$PATH"
69 |
70 | # Clone repo
71 | git clone https://github.com/SaeedDev94/Xray.git $REPO_DIR
72 | cd $REPO_DIR
73 | git submodule update --init --recursive
74 | git checkout "$RELEASE_TAG"
75 |
76 | # Clean task
77 | rm gradle/wrapper/gradle-wrapper.jar
78 | cd app
79 | gradle clean
80 |
81 | # Build XrayCore
82 | pushd ../XrayCore
83 | go install golang.org/x/mobile/cmd/gomobile@$GO_MOBILE_VERSION
84 | go mod download
85 | gomobile init
86 | gomobile bind -o "../app/libs/XrayCore.aar" -androidapi 26 -target "android/$NATIVE_ARCH" -ldflags="-buildid=" -trimpath
87 | popd
88 |
89 | # Build app
90 | gradle -PabiId=$ABI_ID -PabiTarget=$ABI_TARGET assembleRelease
91 |
92 | # Sign app
93 | VERSION_CODE=$(cat versionCode.txt)
94 | ((VERSION_CODE += ABI_ID))
95 | BUILD_NAME="Xray-$RELEASE_TAG-$VERSION_CODE.apk"
96 | cd build/outputs/apk/release
97 | echo "$KS_FILE" > /tmp/xray_base64.txt
98 | base64 -d /tmp/xray_base64.txt > /tmp/xray.jks
99 | zipalign -p -f -v 4 "app-$ABI_TARGET-release-unsigned.apk" "$BUILD_NAME"
100 | apksigner sign --ks /tmp/xray.jks --ks-pass "pass:$KS_PASSWORD" --ks-key-alias "$KEY_ALIAS" --key-pass "pass:$KEY_PASSWORD" "$BUILD_NAME"
101 | rm /tmp/xray_base64.txt /tmp/xray.jks
102 |
103 | # Move app to dist dir
104 | mv "$BUILD_NAME" "$DIST_DIR"
105 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.devtools.ksp) apply false
6 | }
7 |
--------------------------------------------------------------------------------
/buildXrayCore.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | TARGET="$1"
4 | ARCHS=(arm arm64 386 amd64)
5 | DEST="../app/libs"
6 |
7 | is_in_array() {
8 | local value="$1"
9 | local array=("${@:2}")
10 |
11 | for item in "${array[@]}"; do
12 | if [[ "$item" == "$value" ]]; then
13 | return 0
14 | fi
15 | done
16 |
17 | return 1
18 | }
19 |
20 | check_target() {
21 | if ! is_in_array "$TARGET" "${ARCHS[@]}"; then
22 | echo "Not supported"
23 | exit 1
24 | fi
25 | }
26 |
27 | prepare_go() {
28 | echo "Install dependencies"
29 | cd XrayCore
30 | # rm go*
31 | # go mod init XrayCore
32 | # go mod edit -replace github.com/xtls/xray-core=./Xray-core
33 | # go mod edit -replace github.com/xtls/libxray=./libXray
34 | # go mod tidy
35 | # go get golang.org/x/mobile
36 | # go get google.golang.org/genproto
37 | go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20250506005352-78cd7a343bde
38 | go mod download
39 | }
40 |
41 | build_android() {
42 | echo "Building XrayCore for $TARGET"
43 | rm -f "$DEST/XrayCore*"
44 | gomobile init
45 | gomobile bind -o "$DEST/XrayCore.aar" -androidapi 26 -target "android/$TARGET" -ldflags="-buildid=" -trimpath
46 | }
47 |
48 | refresh_dependencies() {
49 | echo "Gradle: refresh dependencies"
50 | cd ..
51 | ./gradlew --refresh-dependencies clean
52 | }
53 |
54 | check_target
55 | prepare_go
56 | build_android
57 | refresh_dependencies
58 |
--------------------------------------------------------------------------------
/get-it-on-fdroid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/get-it-on-fdroid.png
--------------------------------------------------------------------------------
/get-it-on-github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/get-it-on-github.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | # abiId to add versionCode (all: 0) (armeabi-v7a: 1) (arm64-v8a: 2) (x86: 3) (x86_64: 4)
25 | abiId=0
26 | # abiTarget on build time
27 | abiTarget=armeabi-v7a,arm64-v8a,x86,x86_64
28 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.10.0"
3 | kotlin = "2.1.20"
4 | ksp = "2.1.20-2.0.1"
5 | androidxActivityKtx = "1.10.1"
6 | androidxCoreKtx = "1.16.0"
7 | androidxAppCompat = "1.7.0"
8 | androidxLifecycleViewmodelKtx = "2.9.0"
9 | androidxRoom = "2.7.1"
10 | blacksquircleUi = "2.9.0"
11 | googleMaterial = "1.12.0"
12 |
13 | [libraries]
14 | androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidxActivityKtx" }
15 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCoreKtx" }
16 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
17 | androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycleViewmodelKtx" }
18 | androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidxRoom" }
19 | androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" }
20 | androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" }
21 | blacksquircle-ui-editorkit = { group = "com.blacksquircle.ui", name = "editorkit", version.ref = "blacksquircleUi" }
22 | blacksquircle-ui-language-json = { group = "com.blacksquircle.ui", name = "language-json", version.ref = "blacksquircleUi" }
23 | google-material = { group = "com.google.android.material", name = "material", version.ref = "googleMaterial" }
24 |
25 | [plugins]
26 | android-application = { id = "com.android.application", version.ref = "agp" }
27 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
28 | devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
29 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jul 11 19:53:33 IRST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/104.txt:
--------------------------------------------------------------------------------
1 | - Fresh new design
2 | - Profile CRUD
3 | - Ping active connection
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/114.txt:
--------------------------------------------------------------------------------
1 | - minSdk 26 (Android 8)
2 | - Vpn service notification
3 | - New dark Material3 theme
4 | - New settings layout
5 | - New settings: Tun Name, Tun Mtu, Ping Address
6 | - New output format for ping result
7 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/124.txt:
--------------------------------------------------------------------------------
1 | - Increase profile CardView corner radius
2 | - Close drawer on app info items click
3 | - Profile delete confirm dialog
4 | - Allow selected profile to delete
5 | - Assets manager (download, select, delete) dat files (GeoIP, GeoSite)
6 | - Improve ping, thanks to @maskedeken
7 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/134.txt:
--------------------------------------------------------------------------------
1 | - List drag and drop
2 | - SmartTV app launcher support
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/144.txt:
--------------------------------------------------------------------------------
1 | - libXray: fix coreServer is always nil
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/154.txt:
--------------------------------------------------------------------------------
1 | - New Settings:
2 | - ++ GeoIP Address
3 | - ++ GeoSite Address
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/164.txt:
--------------------------------------------------------------------------------
1 | - Settings: [Basic, Advanced] Tabs
2 | - New Settings:
3 | - ++ Ping Timeout
4 | - ++ Enable IPv6
5 | - ++ IPv6 DNS
6 | - ++ Tun IPv4, IPv6 address
7 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/174.txt:
--------------------------------------------------------------------------------
1 | - Allow CRUD while VpnService is running except selected profile
2 | - Xray QS Tile
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/184.txt:
--------------------------------------------------------------------------------
1 | - Fix: selected profile save issue
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/194.txt:
--------------------------------------------------------------------------------
1 | - Fix ping: socks auth error
2 | - heiher/hev-socks5-tunnel @ 2.6.7
3 | - LogsActivity: Xray-core live logs!
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/204.txt:
--------------------------------------------------------------------------------
1 | - New Activity: Excluded Apps
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/214.txt:
--------------------------------------------------------------------------------
1 | - XTLS/Xray-core@v1.8.8
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/224.txt:
--------------------------------------------------------------------------------
1 | - Json config editor
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/234.txt:
--------------------------------------------------------------------------------
1 | - Profile: Full width Config Editor
2 | - Profile: Remove save button and add save to menu
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/244.txt:
--------------------------------------------------------------------------------
1 | - Open Json File With Xray
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/254.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v1.8.9
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/264.txt:
--------------------------------------------------------------------------------
1 | - Fix qsTile problem on some devices
2 | - Optional notification permission
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/274.txt:
--------------------------------------------------------------------------------
1 | - QS Tile: Fix app crash on some scenarios
2 | - Fix UI update issue on VPN disconnect by system settings
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/284.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ 1.8.10
2 | - hev-socks5-tunnel @ 2.6.8
3 | - Fix: Excluded Apps: deselect issue (thanks to @maskedeken)
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/294.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ 1.8.11
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/304.txt:
--------------------------------------------------------------------------------
1 | - hev-socks5-tunnel @ v2.6.9
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/314.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v1.8.13
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/324.txt:
--------------------------------------------------------------------------------
1 | - hev-socks5-tunnel @ v2.7.0
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/334.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v1.8.16
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/344.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v1.8.18
2 | - hev-socks5-tunnel @ v2.7.1
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/354.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v1.8.19
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/364.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v1.8.21
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/374.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v1.8.23
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/384.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v1.8.24
2 | - New: add config via url
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/394.txt:
--------------------------------------------------------------------------------
1 | - hev-socks5-tunnel @ v2.7.4
2 | - New Profile > From Clipboard: now supports import json config from http(s)
3 | - Added deep link: "xray://import-profile/{LINK}"
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/404.txt:
--------------------------------------------------------------------------------
1 | - Prevent slash character escaping on importing profile via link (Fix #31)
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/414.txt:
--------------------------------------------------------------------------------
1 | - Take uri fragment as profile name (#32)
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/424.txt:
--------------------------------------------------------------------------------
1 | - Logs optimizations by @maskedeken (#27)
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/504.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v24.10.31
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/514.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v24.11.5
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/524.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v24.12.15
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/534.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v24.12.18
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/54.txt:
--------------------------------------------------------------------------------
1 | - New package name `io.github.saeeddev94.xray`
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/544.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v25.1.1
2 | - Improve importing config links
3 | - Android 15: Fix layouts overlap with status bar
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/604.txt:
--------------------------------------------------------------------------------
1 | - Migrate to kotlin CoroutineScope
2 | - New Activity: LinksActivity
3 | - Ability to add and manage "Json and Subscription" Links
4 | - Manual update profiles via links
5 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/614.txt:
--------------------------------------------------------------------------------
1 | - Some optimizations for lower android versions (8-12)
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/624.txt:
--------------------------------------------------------------------------------
1 | - Some optimizations for lower android versions (8-12)
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/634.txt:
--------------------------------------------------------------------------------
1 | - Some optimizations for lower android versions (8-12)
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/64.txt:
--------------------------------------------------------------------------------
1 | - New settings:
2 | - ++ Socks auth
3 | - ++ Bypass LAN
4 | - Set Xray-core version on activity start
5 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/644.txt:
--------------------------------------------------------------------------------
1 | - Remove redundant permissions
2 | - New Settings: Boot Auto Start
3 | - Fix notification stop action
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/654.txt:
--------------------------------------------------------------------------------
1 | - Breaking Change:
2 | - **The app expects a list of configs for a Json link**
3 | - The app doesn't accept HTTP link anymore
4 | - Details: https://github.com/XTLS/Xray-core/discussions/3765
5 | - Only prevent invalid config on start
6 | - Show Invalid Profile error message on save inside a dialog
7 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/664.txt:
--------------------------------------------------------------------------------
1 | - Rename Excluded Apps to Apps Routing
2 | - Apps Routing has 2 modes: Exclude, Include
3 | - Default mode is Exclude
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/674.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v25.2.21
2 | - hev-socks5-tunnel @ v2.8.0
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/684.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v25.3.31
2 | - hev-socks5-tunnel @ v2.10.0
3 | - open app on qs tile long press
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/694.txt:
--------------------------------------------------------------------------------
1 | - Allow user certificates
2 | - Custom "User-Agent" header for Links
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/704.txt:
--------------------------------------------------------------------------------
1 | - App default User-Agent header
2 | - Change default ping address to `https://www.google.com`
3 | - Xray-core @ v25.4.30
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/714.txt:
--------------------------------------------------------------------------------
1 | - Drop support for importing json url via clipboard (convert it to json sub instead)
2 | - Ability to add new link (Json, Subscription) via clipboard directly from MainActivity
3 | - Ability to refresh links directly from MainActivity
4 | - Show loading dialog while refreshing links instead of toast
5 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/724.txt:
--------------------------------------------------------------------------------
1 | - Shows active links as tabs
2 | - Shows only profiles with active links
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/734.txt:
--------------------------------------------------------------------------------
1 | - Shows active links as tabs
2 | - Shows only profiles with active links
3 | - New links sort: oldest first
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/74.txt:
--------------------------------------------------------------------------------
1 | - Split settings: Basic and Advanced
2 | - Advanced settings are hidden by default
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/744.txt:
--------------------------------------------------------------------------------
1 | - Xray-core @ v25.5.16
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/754.txt:
--------------------------------------------------------------------------------
1 | - Allow profile change while the app is in start mode
2 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/764.txt:
--------------------------------------------------------------------------------
1 | - Update qs tile and notification on profile select
2 | - Save last selected link tab
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/774.txt:
--------------------------------------------------------------------------------
1 | - Target SDK 36 (Android 16)
2 | - Migrate to Gradle kts and version catalogs
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/784.txt:
--------------------------------------------------------------------------------
1 | - Reload xray after links refresh
2 | - Show active profile name on new config
3 |
--------------------------------------------------------------------------------
/metadata/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | This is a simple GUI client for XTLS/Xray-core
2 | Also you can use it independent of Xray-core just for your socks proxy (Like SocksDroid)
3 |
--------------------------------------------------------------------------------
/metadata/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/metadata/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/metadata/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/metadata/en-US/images/icon.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot-01-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/metadata/en-US/images/phoneScreenshots/screenshot-01-home.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot-02-assets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/metadata/en-US/images/phoneScreenshots/screenshot-02-assets.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot-03-settings-basic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/metadata/en-US/images/phoneScreenshots/screenshot-03-settings-basic.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot-04-settings-advanced.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaeedDev94/Xray/ef1c79505fcfa538463f8e9990192a41e75c614e/metadata/en-US/images/phoneScreenshots/screenshot-04-settings-advanced.png
--------------------------------------------------------------------------------
/metadata/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Xray GUI Client For Android
2 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Xray"
16 | include(":app")
17 |
--------------------------------------------------------------------------------