mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed4eaf6bd0 | |||
| 6ed13ed6fd | |||
| 4ddda4902f | |||
| ba53203dbc | |||
| 146f20e13b | |||
| 38554d4e55 | |||
| 23725a3210 | |||
| 50f435f250 | |||
| 80572e566c | |||
| 3051309138 | |||
| 48373cfa0d | |||
| fb00560c72 | |||
| f81fb78702 | |||
| 340112272e | |||
| e220c380e2 | |||
| 611ac81228 | |||
| db511c9ab1 | |||
| 2623b3110b | |||
| 4bf125b4f5 | |||
| 50297d88cc | |||
| 2975b0cd1a | |||
| ea1f584422 |
+24
-15
@@ -66,21 +66,30 @@ jobs:
|
||||
run: |
|
||||
cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
|
||||
|
||||
- name: Fetch AndroidLibXrayLite tag
|
||||
run: |
|
||||
pushd AndroidLibXrayLite
|
||||
CURRENT_TAG=$(git describe --tags --abbrev=0)
|
||||
echo "Current tag in this repo: $CURRENT_TAG"
|
||||
echo "CURRENT_TAG=$CURRENT_TAG" >> $GITHUB_ENV
|
||||
popd
|
||||
|
||||
- name: Download libv2ray
|
||||
uses: robinraju/release-downloader@v1.12
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
repository: '2dust/AndroidLibXrayLite'
|
||||
tag: ${{ env.CURRENT_TAG }}
|
||||
fileName: 'libv2ray.aar'
|
||||
out-file-path: V2rayNG/app/libs/
|
||||
go-version: '1.22'
|
||||
cache-dependency-path: AndroidLibXrayLite/go.sum
|
||||
|
||||
- name: Build libv2ray
|
||||
run: |
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go install golang.org/x/mobile/cmd/gobind@latest
|
||||
export PATH=$PATH:$(go env GOPATH)/bin
|
||||
gomobile init
|
||||
|
||||
pushd AndroidLibXrayLite
|
||||
mkdir -p assets data
|
||||
# Download geo assets if needed
|
||||
bash gen_assets.sh download
|
||||
cp -v data/*.dat assets/
|
||||
go mod tidy
|
||||
gomobile bind -v -androidapi 24 -trimpath -ldflags='-s -w -buildid=' -o libv2ray.aar ./
|
||||
popd
|
||||
|
||||
mkdir -p V2rayNG/app/libs/
|
||||
cp AndroidLibXrayLite/libv2ray.aar V2rayNG/app/libs/
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
@@ -167,5 +176,5 @@ jobs:
|
||||
with:
|
||||
files: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*.apk
|
||||
tag_name: ${{ github.event.inputs.release_tag || github.ref_name }}
|
||||
prerelease: true
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
[submodule "AndroidLibXrayLite"]
|
||||
path = AndroidLibXrayLite
|
||||
url = https://github.com/2dust/AndroidLibXrayLite
|
||||
[submodule "hev-socks5-tunnel"]
|
||||
path = hev-socks5-tunnel
|
||||
url = https://github.com/heiher/hev-socks5-tunnel
|
||||
|
||||
Submodule AndroidLibXrayLite deleted from b1c89e2da6
+70
@@ -0,0 +1,70 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
required: false
|
||||
type: string
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6.0.0
|
||||
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v6.1.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install gomobile
|
||||
run: |
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
export PATH=$PATH:~/go/bin
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3.2.0
|
||||
with:
|
||||
log-accepted-android-sdk-licenses: false
|
||||
cmdline-tools-version: '12266719'
|
||||
packages: 'platforms;android-35 build-tools;35.0.0 platform-tools'
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
|
||||
--channel=0 \
|
||||
--install "ndk;28.2.13676358"
|
||||
echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir -p assets data
|
||||
bash gen_assets.sh download
|
||||
cp -v data/*.dat assets/
|
||||
gomobile init
|
||||
go mod tidy
|
||||
gomobile bind -v -androidapi 24 -trimpath -ldflags='-s -w -buildid=' ./
|
||||
|
||||
- name: Upload build artifacts
|
||||
if: github.event.inputs.release_tag == ''
|
||||
uses: actions/upload-artifact@v6.0.0
|
||||
with:
|
||||
name: libv2ray
|
||||
path: |
|
||||
${{ github.workspace }}/libv2ray*r
|
||||
|
||||
- name: Upload AndroidLibXrayLite to release
|
||||
if: github.event.inputs.release_tag != ''
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
file: ./libv2ray*r
|
||||
tag: ${{ github.event.inputs.release_tag }}
|
||||
file_glob: true
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
name: Check and Update xray-core
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout our repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Fetch latest release tag from external repository
|
||||
id: fetch-release
|
||||
run: |
|
||||
EXTERNAL_REPO="XTLS/Xray-core"
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/$EXTERNAL_REPO/tags | jq -r '.[0]')
|
||||
LATEST_TAG_NAME=$(echo $LATEST_TAG | jq -r .name)
|
||||
LATEST_TAG_SHA=$(echo $LATEST_TAG | jq -r .commit.sha)
|
||||
echo "Latest tag from external repo: $LATEST_TAG_NAME"
|
||||
echo "LATEST_TAG_NAME=$LATEST_TAG_NAME" >> $GITHUB_ENV
|
||||
echo "LATEST_TAG_SHA=$LATEST_TAG_SHA" >> $GITHUB_ENV
|
||||
|
||||
- name: Fetch current repository release tag
|
||||
id: fetch-current-tag
|
||||
run: |
|
||||
CURRENT_TAG_NAME=$(git describe --tags --abbrev=0)
|
||||
echo "Current tag in this repo: $CURRENT_TAG_NAME"
|
||||
echo "CURRENT_TAG_NAME=$CURRENT_TAG_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Compare tags
|
||||
id: compare-tags
|
||||
run: |
|
||||
if [ "$LATEST_TAG_NAME" != "$CURRENT_TAG_NAME" ]; then
|
||||
if [ "$(printf '%s\n' "$LATEST_TAG_NAME" "$CURRENT_TAG_NAME" | sort -V | tail -n1)" == "$CURRENT_TAG_NAME" ]; then
|
||||
echo "Upstream LATEST_TAG_NAME less than the CURRENT_TAG_NAME, no update needed."
|
||||
else
|
||||
echo "Tags are different. Updating..."
|
||||
echo "needs_update=true" >> $GITHUB_ENV
|
||||
fi
|
||||
else
|
||||
echo "Tags are the same. No update needed."
|
||||
echo "needs_update=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Setup Golang
|
||||
if: env.needs_update == 'true'
|
||||
uses: actions/setup-go@v5.4.0
|
||||
with:
|
||||
go-version: 'stable'
|
||||
|
||||
- name: Update and commit changes
|
||||
if: env.needs_update == 'true'
|
||||
run: |
|
||||
# Sync Go version from upstream go.mod
|
||||
EXTERNAL_REPO="XTLS/Xray-core"
|
||||
GO_VERSION=$(curl -s https://raw.githubusercontent.com/$EXTERNAL_REPO/${{ env.LATEST_TAG_NAME }}/go.mod | awk '/^go / {print $2; exit}')
|
||||
if [ -n "$GO_VERSION" ]; then
|
||||
echo "Syncing Go version to $GO_VERSION"
|
||||
go mod edit -go=$GO_VERSION
|
||||
else
|
||||
echo "Failed to detect Go version from upstream go.mod"
|
||||
fi
|
||||
|
||||
go get github.com/xtls/xray-core@${{ env.LATEST_TAG_SHA }}
|
||||
go get golang.org/x/mobile@latest
|
||||
|
||||
# Clean up and verify module dependencies
|
||||
go mod tidy -v
|
||||
|
||||
# Show changes
|
||||
git diff
|
||||
|
||||
- name: Commit and push changes
|
||||
id: auto-commit-action
|
||||
if: env.needs_update == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v5.1.0
|
||||
with:
|
||||
commit_message: Updating xray-core to ${{ env.LATEST_TAG_NAME }} ${{ env.LATEST_TAG_SHA }}
|
||||
tagging_message: ${{ env.LATEST_TAG_NAME }}
|
||||
|
||||
- name: Trigger build
|
||||
if: env.needs_update == 'true' && steps.auto-commit-action.outputs.changes_detected == 'true'
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
https://api.github.com/repos/${{ github.repository }}/actions/workflows/main.yml/dispatches \
|
||||
-d "{
|
||||
\"ref\": \"main\",
|
||||
\"inputs\": {
|
||||
\"release_tag\": \"${{ env.LATEST_TAG_NAME }}\"
|
||||
}
|
||||
}"
|
||||
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,13 @@
|
||||
# AndroidLibXrayLite
|
||||
|
||||
## Build requirements
|
||||
* JDK
|
||||
* Android SDK
|
||||
* Go
|
||||
* gomobile
|
||||
|
||||
## Build instructions
|
||||
1. `git clone [repo] && cd AndroidLibXrayLite`
|
||||
2. `gomobile init`
|
||||
3. `go mod tidy -v`
|
||||
4. `gomobile bind -v -androidapi 21 -ldflags='-s -w' ./`
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
# Set magic variables for current file & dir
|
||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
||||
__base="$(basename "${__file}" .sh)"
|
||||
|
||||
DATADIR="${__dir}/data"
|
||||
|
||||
|
||||
# Check for required dependencies
|
||||
check_dependencies() {
|
||||
command -v jq >/dev/null 2>&1 || { echo >&2 "jq is required but it's not installed. Aborting."; exit 1; }
|
||||
command -v go >/dev/null 2>&1 || { echo >&2 "Go is required but it's not installed. Aborting."; exit 1; }
|
||||
}
|
||||
|
||||
|
||||
# Download data function
|
||||
download_dat() {
|
||||
echo "Downloading geoip.dat..."
|
||||
curl -sL https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat -o "$DATADIR/geoip.dat"
|
||||
|
||||
echo "Downloading geosite.dat..."
|
||||
curl -sL https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat -o "$DATADIR/geosite.dat"
|
||||
|
||||
echo "Downloading geoip-only-cn-private.dat..."
|
||||
curl -sL https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat -o "$DATADIR/geoip-only-cn-private.dat"
|
||||
}
|
||||
|
||||
# Main execution logic
|
||||
ACTION="${1:-download}"
|
||||
|
||||
check_dependencies
|
||||
|
||||
case $ACTION in
|
||||
"download") download_dat ;;
|
||||
*) echo "Invalid action: $ACTION" ; exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,50 @@
|
||||
module github.com/2dust/AndroidLibXrayLite
|
||||
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/xtls/xray-core v1.260327.0
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pires/go-proxyproto v0.11.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagernet/sing v0.7.13 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,130 @@
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
|
||||
github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
|
||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
|
||||
github.com/xtls/xray-core v1.260327.0 h1:g4TzxMwyPrxslZh6uD+FiG3lXKTrnNO+b4ky2OhogHE=
|
||||
github.com/xtls/xray-core v1.260327.0/go.mod h1:OXMlhBloFry8mw0KwWLWLd3RQyXJzEYsCGlgsX36h60=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 h1:MOzyaj0wu2xneBkzkg9LHNYjDBB4W5vP043A2SYQRPA=
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60/go.mod h1:th6VJvzjMbrYF8SduQY5rpD0HG0GleGxjadkqSxFs3k=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
@@ -0,0 +1,441 @@
|
||||
package libv2ray
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
coreapplog "github.com/xtls/xray-core/app/log"
|
||||
corecommlog "github.com/xtls/xray-core/common/log"
|
||||
corenet "github.com/xtls/xray-core/common/net"
|
||||
corefilesystem "github.com/xtls/xray-core/common/platform/filesystem"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
core "github.com/xtls/xray-core/core"
|
||||
corestats "github.com/xtls/xray-core/features/stats"
|
||||
coreserial "github.com/xtls/xray-core/infra/conf/serial"
|
||||
_ "github.com/xtls/xray-core/main/distro/all"
|
||||
mobasset "golang.org/x/mobile/asset"
|
||||
)
|
||||
|
||||
// Constants for environment variables
|
||||
const (
|
||||
coreAsset = "xray.location.asset"
|
||||
coreCert = "xray.location.cert"
|
||||
xudpBaseKey = "xray.xudp.basekey"
|
||||
tunFdKey = "xray.tun.fd"
|
||||
)
|
||||
|
||||
// CoreController represents a controller for managing Xray core instance lifecycle
|
||||
type CoreController struct {
|
||||
CallbackHandler CoreCallbackHandler
|
||||
statsManager corestats.Manager
|
||||
coreMutex sync.Mutex
|
||||
coreInstance *core.Instance
|
||||
IsRunning bool
|
||||
}
|
||||
|
||||
// PingCallback defines interface for receiving individual ping results in batch mode
|
||||
type PingCallback interface {
|
||||
OnResult(guid string, delay int64)
|
||||
}
|
||||
|
||||
// CoreCallbackHandler defines interface for receiving callbacks and notifications from the core service
|
||||
type CoreCallbackHandler interface {
|
||||
Startup() int
|
||||
Shutdown() int
|
||||
OnEmitStatus(int, string) int
|
||||
}
|
||||
|
||||
// consoleLogWriter implements a log writer without datetime stamps
|
||||
// as Android system already adds timestamps to each log line
|
||||
type consoleLogWriter struct {
|
||||
logger *log.Logger // Standard logger
|
||||
}
|
||||
|
||||
// setEnvVariable safely sets an environment variable and logs any errors encountered.
|
||||
func setEnvVariable(key, value string) {
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
log.Printf("Failed to set environment variable %s: %v. Please check your configuration.", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// InitCoreEnv initializes environment variables and file system handlers for the core
|
||||
// It sets up asset path, certificate path, XUDP base key and customizes the file reader
|
||||
// to support Android asset system
|
||||
func InitCoreEnv(envPath string, key string) {
|
||||
// Set asset/cert paths
|
||||
if len(envPath) > 0 {
|
||||
setEnvVariable(coreAsset, envPath)
|
||||
setEnvVariable(coreCert, envPath)
|
||||
}
|
||||
|
||||
// Set XUDP encryption key
|
||||
if len(key) > 0 {
|
||||
setEnvVariable(xudpBaseKey, key)
|
||||
}
|
||||
|
||||
// Custom file reader with path validation
|
||||
corefilesystem.NewFileReader = func(path string) (io.ReadCloser, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
_, file := filepath.Split(path)
|
||||
return mobasset.Open(file)
|
||||
}
|
||||
return os.Open(path)
|
||||
}
|
||||
}
|
||||
|
||||
// NewCoreController initializes and returns a new CoreController instance
|
||||
// Sets up the console log handler and associates it with the provided callback handler
|
||||
func NewCoreController(s CoreCallbackHandler) *CoreController {
|
||||
// Register custom logger
|
||||
if err := coreapplog.RegisterHandlerCreator(
|
||||
coreapplog.LogType_Console,
|
||||
func(lt coreapplog.LogType, options coreapplog.HandlerCreatorOptions) (corecommlog.Handler, error) {
|
||||
return corecommlog.NewLogger(createStdoutLogWriter()), nil
|
||||
},
|
||||
); err != nil {
|
||||
log.Printf("Failed to register log handler: %v", err)
|
||||
}
|
||||
|
||||
return &CoreController{
|
||||
CallbackHandler: s,
|
||||
}
|
||||
}
|
||||
|
||||
// StartLoop initializes and starts the core processing loop
|
||||
// Thread-safe method that configures and runs the Xray core with the provided configuration
|
||||
// Returns immediately if the core is already running
|
||||
func (x *CoreController) StartLoop(configContent string, tunFd int32) (err error) {
|
||||
// Set TUN fd key, 0 means do not use TUN
|
||||
setEnvVariable(tunFdKey, strconv.Itoa(int(tunFd)))
|
||||
|
||||
x.coreMutex.Lock()
|
||||
defer x.coreMutex.Unlock()
|
||||
|
||||
if x.IsRunning {
|
||||
log.Println("Core is already running")
|
||||
return nil
|
||||
}
|
||||
|
||||
return x.doStartLoop(configContent)
|
||||
}
|
||||
|
||||
// StopLoop safely stops the core processing loop and releases resources
|
||||
// Thread-safe method that shuts down the core instance and triggers necessary callbacks
|
||||
func (x *CoreController) StopLoop() error {
|
||||
x.coreMutex.Lock()
|
||||
defer x.coreMutex.Unlock()
|
||||
|
||||
if x.IsRunning {
|
||||
x.doShutdown()
|
||||
x.CallbackHandler.OnEmitStatus(0, "Core stopped")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueryStats retrieves and resets traffic statistics for a specific outbound tag and direction
|
||||
// Returns the accumulated traffic value and resets the counter to zero
|
||||
// Returns 0 if the stats manager is not initialized or the counter doesn't exist
|
||||
func (x *CoreController) QueryStats(tag string, direct string) int64 {
|
||||
if x.statsManager == nil {
|
||||
return 0
|
||||
}
|
||||
counter := x.statsManager.GetCounter(fmt.Sprintf("outbound>>>%s>>>traffic>>>%s", tag, direct))
|
||||
if counter == nil {
|
||||
return 0
|
||||
}
|
||||
return counter.Set(0)
|
||||
}
|
||||
|
||||
// MeasureDelay measures network latency to a specified URL through the current core instance
|
||||
// Uses a 12-second timeout context and returns the round-trip time in milliseconds
|
||||
// An error is returned if the connection fails or returns an unexpected status
|
||||
func (x *CoreController) MeasureDelay(url string) (int64, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return measureInstDelay(ctx, x.coreInstance, url)
|
||||
}
|
||||
|
||||
// MeasureOutboundDelay measures the outbound delay for a given configuration and URL
|
||||
func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||||
defer cancel()
|
||||
return measureOutboundDelayInternal(ctx, ConfigureFileContent, url)
|
||||
}
|
||||
|
||||
// MeasureOutboundDelayBatch measures the outbound delay for multiple configurations in parallel
|
||||
func MeasureOutboundDelayBatch(itemsJson string, url string, callback PingCallback) {
|
||||
type PingItem struct {
|
||||
Guid string `json:"guid"`
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
var items []PingItem
|
||||
if err := json.Unmarshal([]byte(itemsJson), &items); err != nil {
|
||||
log.Printf("Failed to unmarshal batch items: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Use a worker pool to process items
|
||||
// Performance tuning: 24 concurrency
|
||||
concurrency := 24
|
||||
if len(items) < concurrency {
|
||||
concurrency = len(items)
|
||||
}
|
||||
|
||||
itemChan := make(chan PingItem, len(items))
|
||||
for _, item := range items {
|
||||
itemChan <- item
|
||||
}
|
||||
close(itemChan)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(concurrency)
|
||||
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for it := range itemChan {
|
||||
// Set a reasonable timeout for each individual test
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
|
||||
delay, _ := measureOutboundDelayInternal(ctx, it.Config, url)
|
||||
cancel() // cancel context as soon as we have a result
|
||||
|
||||
if callback != nil {
|
||||
callback.OnResult(it.Guid, delay)
|
||||
}
|
||||
|
||||
// Small sleep to prevent system congestion
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func measureOutboundDelayInternal(ctx context.Context, ConfigureFileContent string, url string) (int64, error) {
|
||||
config, err := coreserial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("config load error: %w", err)
|
||||
}
|
||||
|
||||
// Simplify config for testing
|
||||
config.Inbound = nil
|
||||
var essentialApp []*serial.TypedMessage
|
||||
for _, app := range config.App {
|
||||
if app.Type == "xray.app.proxyman.OutboundConfig" ||
|
||||
app.Type == "xray.app.dispatcher.Config" ||
|
||||
app.Type == "xray.app.log.Config" {
|
||||
essentialApp = append(essentialApp, app)
|
||||
}
|
||||
}
|
||||
config.App = essentialApp
|
||||
|
||||
inst, err := core.New(config)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("instance creation failed: %w", err)
|
||||
}
|
||||
|
||||
if err := inst.Start(); err != nil {
|
||||
return -1, fmt.Errorf("startup failed: %w", err)
|
||||
}
|
||||
|
||||
// Measure delay
|
||||
delay, err := measureInstDelay(ctx, inst, url)
|
||||
|
||||
// Close instance with a short timeout to prevent hanging the worker
|
||||
closeCtx, closeCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer closeCancel()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
inst.Close()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Closed successfully
|
||||
case <-closeCtx.Done():
|
||||
// Close timed out, move on
|
||||
}
|
||||
|
||||
return delay, err
|
||||
}
|
||||
|
||||
// CheckVersionX returns the library and Xray versions
|
||||
func CheckVersionX() string {
|
||||
var version = 35
|
||||
return fmt.Sprintf("Lib v%d, Xray-core v%s", version, core.Version())
|
||||
}
|
||||
|
||||
// doShutdown shuts down the Xray instance and cleans up resources
|
||||
func (x *CoreController) doShutdown() {
|
||||
if x.coreInstance != nil {
|
||||
if err := x.coreInstance.Close(); err != nil {
|
||||
log.Printf("core shutdown error: %v", err)
|
||||
}
|
||||
x.coreInstance = nil
|
||||
}
|
||||
x.IsRunning = false
|
||||
x.statsManager = nil
|
||||
}
|
||||
|
||||
// doStartLoop sets up and starts the Xray core
|
||||
func (x *CoreController) doStartLoop(configContent string) error {
|
||||
log.Println("initializing core...")
|
||||
config, err := coreserial.LoadJSONConfig(strings.NewReader(configContent))
|
||||
if err != nil {
|
||||
return fmt.Errorf("config error: %w", err)
|
||||
}
|
||||
|
||||
x.coreInstance, err = core.New(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("core init failed: %w", err)
|
||||
}
|
||||
x.statsManager = x.coreInstance.GetFeature(corestats.ManagerType()).(corestats.Manager)
|
||||
|
||||
log.Println("starting core...")
|
||||
x.IsRunning = true
|
||||
if err := x.coreInstance.Start(); err != nil {
|
||||
x.IsRunning = false
|
||||
return fmt.Errorf("startup failed: %w", err)
|
||||
}
|
||||
|
||||
x.CallbackHandler.Startup()
|
||||
x.CallbackHandler.OnEmitStatus(0, "Started successfully, running")
|
||||
|
||||
log.Println("Starting core successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// measureInstDelay measures the delay for an instance to a given URL
|
||||
func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int64, error) {
|
||||
if inst == nil {
|
||||
return -1, errors.New("core instance is nil")
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSHandshakeTimeout: 6 * time.Second,
|
||||
DisableKeepAlives: true,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dest, err := corenet.ParseDestination(fmt.Sprintf("%s:%s", network, addr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return core.Dial(ctx, inst, dest)
|
||||
},
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: 6 * time.Second,
|
||||
}
|
||||
|
||||
if url == "" {
|
||||
url = "https://www.google.com/generate_204"
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("failed to create HTTP request: %w", err)
|
||||
}
|
||||
|
||||
var minDuration int64 = -1
|
||||
success := false
|
||||
var lastErr error
|
||||
|
||||
// Use 2 attempts
|
||||
const attempts = 2
|
||||
for i := 0; i < attempts; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Return immediately when context is canceled
|
||||
if !success {
|
||||
return -1, ctx.Err()
|
||||
}
|
||||
return minDuration, nil
|
||||
default:
|
||||
// Continue execution
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
// Limit reading to 64KB to prevent OOM
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||||
resp.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("failed to read response body: %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||
lastErr = fmt.Errorf("invalid status: %s", resp.Status)
|
||||
continue
|
||||
}
|
||||
|
||||
// Relaxed IP check: only if the URL seems like an IP check service
|
||||
isIPCheckUrl := strings.Contains(strings.ToLower(url), "ip")
|
||||
if isIPCheckUrl {
|
||||
ipStr := strings.TrimSpace(string(body))
|
||||
if net.ParseIP(ipStr) == nil {
|
||||
lastErr = fmt.Errorf("response body is not a valid IP: %s", ipStr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
duration := time.Since(start).Milliseconds()
|
||||
if !success || duration < minDuration {
|
||||
minDuration = duration
|
||||
}
|
||||
|
||||
success = true
|
||||
}
|
||||
if !success {
|
||||
return -1, lastErr
|
||||
}
|
||||
return minDuration, nil
|
||||
}
|
||||
|
||||
// Log writer implementation
|
||||
func (w *consoleLogWriter) Write(s string) error {
|
||||
w.logger.Print(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *consoleLogWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// createStdoutLogWriter creates a logger that won't print date/time stamps
|
||||
func createStdoutLogWriter() corecommlog.WriterCreator {
|
||||
return func() corecommlog.Writer {
|
||||
return &consoleLogWriter{
|
||||
logger: log.New(os.Stdout, "", 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +211,16 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.ServiceControlReceiver"
|
||||
android:exported="false"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<intent-filter>
|
||||
<action android:name="${applicationId}.action.service.stop" />
|
||||
<action android:name="${applicationId}.action.service.start" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".service.QSTileService"
|
||||
android:exported="true"
|
||||
|
||||
@@ -84,6 +84,8 @@ object AppConfig {
|
||||
const val BROADCAST_ACTION_SERVICE = "$ANG_PACKAGE.action.service"
|
||||
const val BROADCAST_ACTION_ACTIVITY = "$ANG_PACKAGE.action.activity"
|
||||
const val BROADCAST_ACTION_WIDGET_CLICK = "$ANG_PACKAGE.action.widget.click"
|
||||
const val BROADCAST_ACTION_SERVICE_STOP = "$ANG_PACKAGE.action.service.stop"
|
||||
const val BROADCAST_ACTION_SERVICE_START = "$ANG_PACKAGE.action.service.start"
|
||||
|
||||
/** Tasker extras. */
|
||||
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
|
||||
@@ -117,8 +119,8 @@ object AppConfig {
|
||||
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
||||
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
||||
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
||||
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
||||
const val DELAY_TEST_URL = "https://icanhazip.com"
|
||||
const val DELAY_TEST_URL2 = "https://api64.ipify.org"
|
||||
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
||||
const val IP_API_URL = "https://api.ip.sb/geoip"
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ data class ProfileItem(
|
||||
val configType: EConfigType,
|
||||
var subscriptionId: String = "",
|
||||
var addedTime: Long = System.currentTimeMillis(),
|
||||
var isFavorite: Boolean = false,
|
||||
|
||||
var remarks: String = "",
|
||||
var description: String? = null,
|
||||
|
||||
@@ -2,9 +2,12 @@ package xyz.zarazaex.olc.dto
|
||||
|
||||
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
||||
fun getTestDelayString(): String {
|
||||
if (testDelayMillis == 0L) {
|
||||
return ""
|
||||
return when {
|
||||
testDelayMillis == 0L -> ""
|
||||
testDelayMillis < 0L -> "Error"
|
||||
else -> "${testDelayMillis}ms"
|
||||
}
|
||||
return testDelayMillis.toString() + "ms"
|
||||
}
|
||||
|
||||
fun isReachable(): Boolean = testDelayMillis > 0L
|
||||
}
|
||||
|
||||
@@ -42,8 +42,18 @@ open class FmtBase {
|
||||
* @return a map of query parameters
|
||||
*/
|
||||
fun getQueryParam(uri: URI): Map<String, String> {
|
||||
return uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.decodeURIComponent(v) } }
|
||||
return uri.rawQuery.orEmpty().split("&")
|
||||
.mapNotNull {
|
||||
val parts = it.split("=", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
parts[0] to Utils.decodeURIComponent(parts[1])
|
||||
} else if (parts.isNotEmpty() && parts[0].isNotEmpty()) {
|
||||
parts[0] to ""
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -308,6 +308,7 @@ object AngConfigManager {
|
||||
}?.key
|
||||
|
||||
if (existingKey != null) {
|
||||
config.isFavorite = existingProfiles[existingKey]?.isFavorite ?: false
|
||||
MmkvManager.encodeProfileDirect(existingKey, JsonUtil.toJson(config))
|
||||
keyToProfile[existingKey] = config
|
||||
} else {
|
||||
|
||||
@@ -42,6 +42,7 @@ object SettingsManager {
|
||||
migrateServerListToSubscriptions()
|
||||
migrateHysteria2PinSHA256()
|
||||
migrateAutoSort()
|
||||
migrateDelayTestUrl()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -572,5 +573,17 @@ object SettingsManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateDelayTestUrl() {
|
||||
val migrationKey = "delay_test_url_migrated_v3"
|
||||
if (MmkvManager.decodeSettingsBool(migrationKey, false)) {
|
||||
return
|
||||
}
|
||||
val currentUrl = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
||||
if (currentUrl == null || currentUrl.contains("generate_204") || currentUrl.contains("gstatic.com") || currentUrl.contains("google.com") || currentUrl.contains("api.ip.sb")) {
|
||||
MmkvManager.encodeSettings(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL)
|
||||
}
|
||||
MmkvManager.encodeSettings(migrationKey, true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -74,6 +74,21 @@ object V2RayNativeManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure outbound connection delay for multiple configs in batch.
|
||||
*
|
||||
* @param configsJson JSON array of {guid, config}
|
||||
* @param testUrl The URL to test against
|
||||
* @param callback Callback for individual results
|
||||
*/
|
||||
fun measureOutboundDelayBatch(configsJson: String, testUrl: String, callback: libv2ray.PingCallback) {
|
||||
try {
|
||||
Libv2ray.measureOutboundDelayBatch(configsJson, testUrl, callback)
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to measure outbound delay batch", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new core controller instance.
|
||||
*
|
||||
|
||||
@@ -32,6 +32,7 @@ object V2RayServiceManager {
|
||||
private var currentConfig: ProfileItem? = null
|
||||
private val operationLock = Any()
|
||||
@Volatile private var isOperationInProgress = false
|
||||
@Volatile var isIntentionalStop = false
|
||||
|
||||
var serviceControl: SoftReference<ServiceControl>? = null
|
||||
set(value) {
|
||||
@@ -49,6 +50,7 @@ object V2RayServiceManager {
|
||||
context.toast(R.string.app_tile_first_use)
|
||||
return false
|
||||
}
|
||||
isIntentionalStop = false
|
||||
startContextService(context)
|
||||
return true
|
||||
}
|
||||
@@ -74,6 +76,7 @@ object V2RayServiceManager {
|
||||
MmkvManager.setSelectServer(guid)
|
||||
}
|
||||
|
||||
isIntentionalStop = false
|
||||
startContextService(context)
|
||||
} finally {
|
||||
synchronized(operationLock) {
|
||||
@@ -87,8 +90,16 @@ object V2RayServiceManager {
|
||||
* @param context The context from which the service is stopped.
|
||||
*/
|
||||
fun stopVService(context: Context) {
|
||||
Log.i(AppConfig.TAG, "StartCore-Manager: stopVService called, serviceControl=${serviceControl?.get()}")
|
||||
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
||||
Log.i(AppConfig.TAG, "StartCore-Manager: stopVService called")
|
||||
isIntentionalStop = true
|
||||
val svc = serviceControl?.get()
|
||||
if (svc != null) {
|
||||
svc.stopService()
|
||||
return
|
||||
}
|
||||
val intent = Intent(AppConfig.BROADCAST_ACTION_SERVICE_STOP)
|
||||
intent.setPackage(AppConfig.ANG_PACKAGE)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,6 +366,11 @@ object V2RayServiceManager {
|
||||
return try {
|
||||
Log.w(AppConfig.TAG, "StartCore-Manager: Core shutdown callback, attempting restart")
|
||||
val service = serviceControl.getService()
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||
if (isIntentionalStop) {
|
||||
Log.i(AppConfig.TAG, "StartCore-Manager: Intentional stop, skipping restart")
|
||||
return 0
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
kotlinx.coroutines.delay(1000L)
|
||||
val ctx = service.applicationContext
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package xyz.zarazaex.olc.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import xyz.zarazaex.olc.AppConfig
|
||||
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
||||
|
||||
class ServiceControlReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
AppConfig.BROADCAST_ACTION_SERVICE_STOP -> {
|
||||
V2RayServiceManager.isIntentionalStop = true
|
||||
V2RayServiceManager.stopVService(context)
|
||||
}
|
||||
AppConfig.BROADCAST_ACTION_SERVICE_START -> V2RayServiceManager.startVServiceFromToggle(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,106 @@
|
||||
package xyz.zarazaex.olc.service
|
||||
|
||||
import android.content.Context
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.zarazaex.olc.AppConfig
|
||||
import xyz.zarazaex.olc.handler.SettingsManager
|
||||
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
||||
import xyz.zarazaex.olc.handler.V2rayConfigManager
|
||||
import xyz.zarazaex.olc.util.JsonUtil
|
||||
import xyz.zarazaex.olc.util.MessageUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* Worker that runs a batch of real-ping tests independently.
|
||||
* Each batch owns its own CoroutineScope/dispatcher and can be cancelled separately.
|
||||
* Worker that runs a batch of real-ping tests independently. Optimized to use Go-level concurrency
|
||||
* for improved performance.
|
||||
*/
|
||||
class RealPingWorkerService(
|
||||
private val context: Context,
|
||||
private val guids: List<String>,
|
||||
private val onFinish: (status: String) -> Unit = {}
|
||||
private val context: Context,
|
||||
private val guids: List<String>,
|
||||
private val onFinish: (status: String) -> Unit = {}
|
||||
) {
|
||||
private val job = SupervisorJob()
|
||||
private val cpu = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
|
||||
private val dispatcher = Executors.newFixedThreadPool(cpu * 16).asCoroutineDispatcher()
|
||||
private val scope = CoroutineScope(job + dispatcher + CoroutineName("RealPingBatchWorker"))
|
||||
private val scope =
|
||||
CoroutineScope(job + Dispatchers.Default + CoroutineName("RealPingBatchWorker"))
|
||||
|
||||
private val runningCount = AtomicInteger(0)
|
||||
private val totalCount = AtomicInteger(0)
|
||||
private val totalCount = AtomicInteger(guids.size)
|
||||
private val finishedCount = AtomicInteger(0)
|
||||
|
||||
private val delayTestUrl = SettingsManager.getDelayTestUrl()
|
||||
|
||||
data class PingItem(val guid: String, val config: String)
|
||||
|
||||
fun start() {
|
||||
val jobs = guids.map { guid ->
|
||||
totalCount.incrementAndGet()
|
||||
scope.launch {
|
||||
runningCount.incrementAndGet()
|
||||
try {
|
||||
val result = startRealPing(guid)
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
|
||||
} catch (e: Exception) {
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, -1L))
|
||||
} finally {
|
||||
val count = totalCount.decrementAndGet()
|
||||
val left = runningCount.decrementAndGet()
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $count")
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Prepare configurations for batch test and shuffle for better async feel
|
||||
val items =
|
||||
guids.shuffled().mapNotNull { guid ->
|
||||
try {
|
||||
val configResult =
|
||||
V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||
if (configResult.status) {
|
||||
PingItem(guid, configResult.content)
|
||||
} else {
|
||||
// Notify failure immediately for invalid configs
|
||||
reportResult(guid, -1L)
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e(AppConfig.TAG, "Failed to prepare config for $guid", e)
|
||||
reportResult(guid, -1L)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (items.isNotEmpty()) {
|
||||
val configsJson = JsonUtil.toJson(items)
|
||||
|
||||
V2RayNativeManager.measureOutboundDelayBatch(
|
||||
configsJson,
|
||||
delayTestUrl,
|
||||
object : libv2ray.PingCallback {
|
||||
override fun onResult(guid: String?, delay: Long) {
|
||||
if (guid != null) {
|
||||
reportResult(guid, delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (job.isActive) {
|
||||
onFinish("0")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (job.isActive) {
|
||||
onFinish("-1")
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportResult(guid: String, delay: Long) {
|
||||
if (!job.isActive) return
|
||||
|
||||
// Launch in scope to unblock Go worker immediately
|
||||
scope.launch {
|
||||
try {
|
||||
joinAll(*jobs.toTypedArray())
|
||||
onFinish("0")
|
||||
} catch (_: CancellationException) {
|
||||
onFinish("-1")
|
||||
} finally {
|
||||
close()
|
||||
val finished = finishedCount.incrementAndGet()
|
||||
val total = guids.size
|
||||
|
||||
// Notify UI about the individual result
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, delay))
|
||||
|
||||
// Throttle progress updates: every 10 items or the very last one
|
||||
if (finished % 10 == 0 || finished == total) {
|
||||
val left = total - finished
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $total")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,60 +108,4 @@ class RealPingWorkerService(
|
||||
fun cancel() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun close() {
|
||||
try {
|
||||
dispatcher.close()
|
||||
} catch (_: Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startRealPing(guid: String): Long {
|
||||
val retFailure = -1L
|
||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||
if (!configResult.status) {
|
||||
return retFailure
|
||||
}
|
||||
|
||||
var bestDelay = retFailure
|
||||
|
||||
for (attempt in 0 until 2) {
|
||||
try {
|
||||
val delay = withTimeout(10000L) {
|
||||
V2RayNativeManager.measureOutboundDelay(
|
||||
configResult.content,
|
||||
SettingsManager.getDelayTestUrl()
|
||||
)
|
||||
}
|
||||
|
||||
if (delay > 0 && (bestDelay == retFailure || delay < bestDelay)) {
|
||||
bestDelay = delay
|
||||
}
|
||||
|
||||
if (bestDelay > 0) {
|
||||
break
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (attempt == 0) {
|
||||
try {
|
||||
val delay = withTimeout(10000L) {
|
||||
V2RayNativeManager.measureOutboundDelay(
|
||||
configResult.content,
|
||||
SettingsManager.getDelayTestUrl(true)
|
||||
)
|
||||
}
|
||||
|
||||
if (delay > 0 && (bestDelay == retFailure || delay < bestDelay)) {
|
||||
bestDelay = delay
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestDelay
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.i(AppConfig.TAG, "StartCore-Proxy: Service command received")
|
||||
V2RayServiceManager.startCoreLoop(null)
|
||||
return START_STICKY
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -99,18 +99,16 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
|
||||
stopAllService(false)
|
||||
wakeLock?.let { if (it.isHeld) it.release() }
|
||||
wakeLock = null
|
||||
NotificationManager.cancelNotification()
|
||||
MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.i(AppConfig.TAG, "StartCore-VPN: Service command received")
|
||||
setupVpnService()
|
||||
startService()
|
||||
return START_STICKY
|
||||
//return super.onStartCommand(intent, flags, startId)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun getService(): Service {
|
||||
|
||||
@@ -34,6 +34,7 @@ import xyz.zarazaex.olc.handler.SettingsChangeManager
|
||||
import xyz.zarazaex.olc.handler.SettingsManager
|
||||
import xyz.zarazaex.olc.handler.UpdateCheckerManager
|
||||
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
||||
import xyz.zarazaex.olc.util.MessageUtil
|
||||
import xyz.zarazaex.olc.util.Utils
|
||||
import xyz.zarazaex.olc.viewmodel.MainViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -147,7 +148,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
setupGroupTab()
|
||||
setupViewModel()
|
||||
mainViewModel.reloadServerList()
|
||||
importConfigViaSub()
|
||||
importAllSubsOnStartup()
|
||||
|
||||
checkAndRequestPermission(PermissionType.POST_NOTIFICATIONS) {
|
||||
}
|
||||
@@ -163,14 +164,16 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
isLiteTesting = false
|
||||
mainViewModel.sortByTestResults()
|
||||
mainViewModel.reloadServerList()
|
||||
|
||||
val firstServer = mainViewModel.serversCache.firstOrNull()
|
||||
if (firstServer != null) {
|
||||
MmkvManager.setSelectServer(firstServer.guid)
|
||||
|
||||
val firstReachable = mainViewModel.serversCache.firstOrNull { cache ->
|
||||
(MmkvManager.decodeServerAffiliationInfo(cache.guid)?.testDelayMillis ?: 0L) > 0L
|
||||
}
|
||||
if (firstReachable != null) {
|
||||
MmkvManager.setSelectServer(firstReachable.guid)
|
||||
showStatus("Подключаемся к быстрейшему серверу")
|
||||
startV2RayWithPermission()
|
||||
} else {
|
||||
showStatus("Серверы не найдены!")
|
||||
showStatus("Нет доступных серверов!")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,20 +208,22 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
isFabOperationInProgress = true
|
||||
|
||||
val isRunning = mainViewModel.isRunning.value == true
|
||||
|
||||
applyRunningState(isLoading = true, isRunning = false)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
Log.d(AppConfig.TAG, "FAB: stopping service, isRunning=${mainViewModel.isRunning.value}")
|
||||
if (isRunning) {
|
||||
Log.d(AppConfig.TAG, "FAB: stopping service")
|
||||
V2RayServiceManager.stopVService(this@MainActivity)
|
||||
} else {
|
||||
Log.d(AppConfig.TAG, "FAB: starting service, isRunning=${mainViewModel.isRunning.value}")
|
||||
Log.d(AppConfig.TAG, "FAB: starting service")
|
||||
startV2RayWithPermission()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "FAB: error", e)
|
||||
applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value ?: false)
|
||||
applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value == true)
|
||||
} finally {
|
||||
isFabOperationInProgress = false
|
||||
}
|
||||
@@ -367,6 +372,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -375,6 +381,32 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_main, menu)
|
||||
|
||||
val searchItem = menu.findItem(R.id.search_view)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
mainViewModel.filterConfig(newText.orEmpty())
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
mainViewModel.filterConfig("")
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
@@ -482,6 +514,20 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
|
||||
|
||||
private fun importAllSubsOnStartup() {
|
||||
showLoading()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val result = AngConfigManager.updateConfigViaSubAll()
|
||||
delay(500L)
|
||||
launch(Dispatchers.Main) {
|
||||
if (result.configCount > 0) {
|
||||
mainViewModel.reloadServerList()
|
||||
showStatus(getString(R.string.title_update_config_count, result.configCount))
|
||||
}
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* import config from sub
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.util.Collections
|
||||
import xyz.zarazaex.olc.AppConfig
|
||||
import xyz.zarazaex.olc.R
|
||||
import xyz.zarazaex.olc.contracts.MainAdapterListener
|
||||
@@ -14,39 +15,144 @@ import xyz.zarazaex.olc.databinding.ItemRecyclerFooterBinding
|
||||
import xyz.zarazaex.olc.databinding.ItemRecyclerMainBinding
|
||||
import xyz.zarazaex.olc.dto.ProfileItem
|
||||
import xyz.zarazaex.olc.dto.ServersCache
|
||||
import xyz.zarazaex.olc.extension.nullIfBlank
|
||||
import xyz.zarazaex.olc.extension.toastError
|
||||
import xyz.zarazaex.olc.extension.toastSuccess
|
||||
import xyz.zarazaex.olc.handler.AngConfigManager
|
||||
import xyz.zarazaex.olc.handler.MmkvManager
|
||||
import xyz.zarazaex.olc.helper.ItemTouchHelperAdapter
|
||||
import xyz.zarazaex.olc.helper.ItemTouchHelperViewHolder
|
||||
import xyz.zarazaex.olc.viewmodel.MainViewModel
|
||||
import java.util.Collections
|
||||
|
||||
class MainRecyclerAdapter(
|
||||
private val mainViewModel: MainViewModel,
|
||||
private val adapterListener: MainAdapterListener?
|
||||
private val mainViewModel: MainViewModel,
|
||||
private val adapterListener: MainAdapterListener?
|
||||
) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||
companion object {
|
||||
private const val VIEW_TYPE_ITEM = 1
|
||||
private const val VIEW_TYPE_FOOTER = 2
|
||||
private const val PAYLOAD_FAVORITE = "PAYLOAD_FAVORITE"
|
||||
}
|
||||
|
||||
private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
|
||||
private val doubleColumnDisplay =
|
||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
|
||||
private var data: MutableList<ServersCache> = mutableListOf()
|
||||
private var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onAttachedToRecyclerView(rv: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(rv)
|
||||
recyclerView = rv
|
||||
(rv.itemAnimator as? androidx.recyclerview.widget.SimpleItemAnimator)?.apply {
|
||||
moveDuration = 400
|
||||
removeDuration = 300
|
||||
addDuration = 300
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(rv: RecyclerView) {
|
||||
super.onDetachedFromRecyclerView(rv)
|
||||
recyclerView = null
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) {
|
||||
data = newData?.toMutableList() ?: mutableListOf()
|
||||
val parsedNewData = newData?.toList() ?: emptyList()
|
||||
|
||||
if (position >= 0 && position in data.indices) {
|
||||
notifyItemChanged(position)
|
||||
} else {
|
||||
notifyDataSetChanged()
|
||||
if (data.isEmpty() || parsedNewData.isEmpty() || position >= 0) {
|
||||
data = parsedNewData.toMutableList()
|
||||
if (position >= 0 && position in data.indices) {
|
||||
notifyItemChanged(position)
|
||||
} else {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val oldData = data
|
||||
val lm = recyclerView?.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager
|
||||
val firstVisible = lm?.findFirstVisibleItemPosition()?.coerceAtLeast(0) ?: 0
|
||||
val isAtTop = firstVisible == 0 && (lm?.findViewByPosition(0)?.top ?: 0) >= 0
|
||||
val firstVisibleGuid = if (!isAtTop) oldData.getOrNull(firstVisible)?.guid else null
|
||||
|
||||
val diffResult =
|
||||
androidx.recyclerview.widget.DiffUtil.calculateDiff(
|
||||
object : androidx.recyclerview.widget.DiffUtil.Callback() {
|
||||
override fun getOldListSize() = oldData.size
|
||||
override fun getNewListSize() = parsedNewData.size
|
||||
|
||||
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
|
||||
return oldData[oldPos].guid == parsedNewData[newPos].guid
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
|
||||
val oldProfile = oldData[oldPos].profile
|
||||
val newProfile = parsedNewData[newPos].profile
|
||||
return oldProfile == newProfile &&
|
||||
oldProfile.isFavorite == newProfile.isFavorite &&
|
||||
MmkvManager.decodeServerAffiliationInfo(
|
||||
oldData[oldPos].guid
|
||||
)
|
||||
?.testDelayMillis ==
|
||||
MmkvManager.decodeServerAffiliationInfo(
|
||||
parsedNewData[newPos].guid
|
||||
)
|
||||
?.testDelayMillis
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
|
||||
if (oldData[oldPos].profile.isFavorite != parsedNewData[newPos].profile.isFavorite) {
|
||||
return PAYLOAD_FAVORITE
|
||||
}
|
||||
return super.getChangePayload(oldPos, newPos)
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
data = parsedNewData.toMutableList()
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
|
||||
if (isAtTop) {
|
||||
lm?.scrollToPositionWithOffset(0, 0)
|
||||
} else if (firstVisibleGuid != null) {
|
||||
val newPos = parsedNewData.indexOfFirst { it.guid == firstVisibleGuid }
|
||||
if (newPos >= 0) lm?.scrollToPosition(newPos)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size + 1
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isNotEmpty() && holder is MainViewHolder) {
|
||||
for (payload in payloads) {
|
||||
if (payload == PAYLOAD_FAVORITE) {
|
||||
val isFav = data[position].profile.isFavorite
|
||||
animateFavorite(holder.itemMainBinding.ivFavorite, isFav)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onBindViewHolder(holder, position, payloads)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateFavorite(view: android.widget.ImageView, isFavorite: Boolean) {
|
||||
view.animate()
|
||||
.scaleX(1.3f)
|
||||
.scaleY(1.3f)
|
||||
.setDuration(150)
|
||||
.withEndAction {
|
||||
view.setImageResource(
|
||||
if (isFavorite) R.drawable.ic_star_filled
|
||||
else R.drawable.ic_star_empty
|
||||
)
|
||||
view.animate()
|
||||
.scaleX(1.0f)
|
||||
.scaleY(1.0f)
|
||||
.setDuration(150)
|
||||
.start()
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||
if (holder is MainViewHolder) {
|
||||
val context = holder.itemMainBinding.root.context
|
||||
@@ -55,41 +161,64 @@ class MainRecyclerAdapter(
|
||||
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
//Name address
|
||||
// Name address
|
||||
holder.itemMainBinding.tvName.text = profile.remarks
|
||||
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
||||
|
||||
//TestResult
|
||||
// TestResult
|
||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(context, R.color.colorPingRed))
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(
|
||||
ContextCompat.getColor(context, R.color.colorPingRed)
|
||||
)
|
||||
} else {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(context, R.color.colorPing))
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(
|
||||
ContextCompat.getColor(context, R.color.colorPing)
|
||||
)
|
||||
}
|
||||
|
||||
//layoutIndicator
|
||||
// layoutIndicator
|
||||
if (guid == MmkvManager.getSelectServer()) {
|
||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorIndicator)
|
||||
} else {
|
||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
||||
}
|
||||
|
||||
//subscription remarks
|
||||
// subscription remarks
|
||||
val subRemarks = getSubscriptionRemarks(profile)
|
||||
holder.itemMainBinding.tvSubscription.text = subRemarks
|
||||
holder.itemMainBinding.layoutSubscription.visibility = if (subRemarks.isEmpty()) View.GONE else View.VISIBLE
|
||||
holder.itemMainBinding.layoutSubscription.visibility =
|
||||
if (subRemarks.isEmpty()) View.GONE else View.VISIBLE
|
||||
|
||||
val isFav = profile.isFavorite
|
||||
holder.itemMainBinding.ivFavorite.setImageResource(
|
||||
if (isFav) R.drawable.ic_star_filled else R.drawable.ic_star_empty
|
||||
)
|
||||
|
||||
holder.itemMainBinding.ivFavorite.setOnClickListener {
|
||||
profile.isFavorite = !profile.isFavorite
|
||||
MmkvManager.encodeServerConfig(guid, profile)
|
||||
mainViewModel.reloadServerList()
|
||||
}
|
||||
|
||||
holder.itemMainBinding.ivCopy.setOnClickListener {
|
||||
if (AngConfigManager.share2Clipboard(context, guid) == 0) {
|
||||
context.toastSuccess(R.string.toast_success)
|
||||
} else {
|
||||
context.toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||
adapterListener?.onSelectServer(guid)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server address information
|
||||
* Hides part of IP or domain information for privacy protection
|
||||
* Gets the server address information Hides part of IP or domain information for privacy
|
||||
* protection
|
||||
* @param profile The server configuration
|
||||
* @return Formatted address string
|
||||
*/
|
||||
@@ -104,10 +233,11 @@ class MainRecyclerAdapter(
|
||||
*/
|
||||
private fun getSubscriptionRemarks(profile: ProfileItem): String {
|
||||
val subRemarks =
|
||||
if (mainViewModel.subscriptionId.isEmpty())
|
||||
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull()
|
||||
else
|
||||
null
|
||||
if (mainViewModel.subscriptionId.isEmpty())
|
||||
MmkvManager.decodeSubscription(profile.subscriptionId)
|
||||
?.remarks
|
||||
?.firstOrNull()
|
||||
else null
|
||||
return subRemarks?.toString() ?: ""
|
||||
}
|
||||
|
||||
@@ -128,10 +258,21 @@ class MainRecyclerAdapter(
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_ITEM ->
|
||||
MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
|
||||
MainViewHolder(
|
||||
ItemRecyclerMainBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
else ->
|
||||
FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
FooterViewHolder(
|
||||
ItemRecyclerFooterBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,10 +295,10 @@ class MainRecyclerAdapter(
|
||||
}
|
||||
|
||||
class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) :
|
||||
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
|
||||
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
|
||||
|
||||
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
|
||||
BaseViewHolder(itemFooterBinding.root)
|
||||
BaseViewHolder(itemFooterBinding.root)
|
||||
|
||||
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
||||
mainViewModel.swapServer(fromPosition, toPosition)
|
||||
@@ -172,6 +313,5 @@ class MainRecyclerAdapter(
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun onItemDismiss(position: Int) {
|
||||
}
|
||||
}
|
||||
override fun onItemDismiss(position: Int) {}
|
||||
}
|
||||
|
||||
@@ -89,21 +89,29 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
data class ServerWithDelay(val guid: String, val delay: Long)
|
||||
data class ServerWithDelay(val guid: String, val delay: Long, val isFav: Boolean)
|
||||
val allServers = mutableListOf<ServerWithDelay>()
|
||||
|
||||
groupSubs.forEach { sub ->
|
||||
val subServers = MmkvManager.decodeServerList(sub.guid)
|
||||
subServers.forEach { guid ->
|
||||
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
||||
allServers.add(ServerWithDelay(guid, if (delay <= 0L) 999999 else delay))
|
||||
val isFav = MmkvManager.decodeServerConfig(guid)?.isFavorite ?: false
|
||||
val sortKey = when {
|
||||
delay > 0L -> delay
|
||||
delay == 0L -> Long.MAX_VALUE - 1
|
||||
else -> Long.MAX_VALUE
|
||||
}
|
||||
allServers.add(ServerWithDelay(guid, sortKey, isFav))
|
||||
}
|
||||
}
|
||||
|
||||
allServers.sortBy { it.delay }
|
||||
allServers.sortWith(compareBy({ !it.isFav }, { it.delay }))
|
||||
allServers.map { it.guid }.toMutableList()
|
||||
} else {
|
||||
MmkvManager.decodeServerList(subscriptionId)
|
||||
val list = MmkvManager.decodeServerList(subscriptionId)
|
||||
list.sortWith(compareBy { !(MmkvManager.decodeServerConfig(it)?.isFavorite ?: false) })
|
||||
list
|
||||
}
|
||||
|
||||
updateCache()
|
||||
@@ -461,7 +469,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
private fun sortByTestResultsForGroup(groupId: String) {
|
||||
data class ServerDelay(var guid: String, var testDelayMillis: Long, var subId: String)
|
||||
data class ServerDelay(var guid: String, var testDelayMillis: Long, var subId: String, var isFav: Boolean)
|
||||
|
||||
val allSubs = MmkvManager.decodeSubscriptions()
|
||||
val groupSubs = when (groupId) {
|
||||
@@ -482,11 +490,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val serverList = MmkvManager.decodeServerList(sub.guid)
|
||||
serverList.forEach { guid ->
|
||||
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
||||
allServerDelays.add(ServerDelay(guid, if (delay <= 0L) 999999 else delay, sub.guid))
|
||||
val isFav = MmkvManager.decodeServerConfig(guid)?.isFavorite ?: false
|
||||
val sortKey = when {
|
||||
delay > 0L -> delay
|
||||
delay == 0L -> Long.MAX_VALUE - 1
|
||||
else -> Long.MAX_VALUE
|
||||
}
|
||||
allServerDelays.add(ServerDelay(guid, sortKey, sub.guid, isFav))
|
||||
}
|
||||
}
|
||||
|
||||
allServerDelays.sortBy { it.testDelayMillis }
|
||||
allServerDelays.sortWith(compareBy({ !it.isFav }, { it.testDelayMillis }))
|
||||
|
||||
val serversBySubId = allServerDelays.groupBy { it.subId }
|
||||
serversBySubId.forEach { (subId, servers) ->
|
||||
@@ -500,16 +514,22 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
* @param subId The subscription ID to sort servers for.
|
||||
*/
|
||||
private fun sortByTestResultsForSub(subId: String) {
|
||||
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
||||
data class ServerDelay(var guid: String, var testDelayMillis: Long, var isFav: Boolean)
|
||||
|
||||
val serverDelays = mutableListOf<ServerDelay>()
|
||||
val serverListToSort = MmkvManager.decodeServerList(subId)
|
||||
|
||||
serverListToSort.forEach { key ->
|
||||
val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
|
||||
serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
|
||||
val isFav = MmkvManager.decodeServerConfig(key)?.isFavorite ?: false
|
||||
val sortKey = when {
|
||||
delay > 0L -> delay
|
||||
delay == 0L -> Long.MAX_VALUE - 1
|
||||
else -> Long.MAX_VALUE
|
||||
}
|
||||
serverDelays.add(ServerDelay(key, sortKey, isFav))
|
||||
}
|
||||
serverDelays.sortBy { it.testDelayMillis }
|
||||
serverDelays.sortWith(compareBy({ !it.isFav }, { it.testDelayMillis }))
|
||||
|
||||
val sortedServerList = serverDelays.map { it.guid }.toMutableList()
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zm-6,0C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?attr/colorAccent">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28l-3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?attr/colorAccent">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
|
||||
</vector>
|
||||
@@ -99,7 +99,28 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_favorite"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/padding_spacing_dp8"
|
||||
android:src="@drawable/ic_star_empty" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_copy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/padding_spacing_dp8"
|
||||
android:src="@drawable/ic_copy"
|
||||
app:tint="?attr/colorAccent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/search_view"
|
||||
android:icon="@drawable/ic_search_24dp"
|
||||
android:title="@string/menu_item_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="always|collapseActionView" />
|
||||
<item
|
||||
android:id="@+id/real_ping_all"
|
||||
android:icon="@drawable/ic_outline_filter_alt_24"
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import com.google.gson.Gson
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.net.URL
|
||||
import java.util.UUID
|
||||
|
||||
data class SubscriptionItem(
|
||||
var remarks: String = "",
|
||||
var url: String = "",
|
||||
var enabled: Boolean = true,
|
||||
val addedTime: Long = System.currentTimeMillis(),
|
||||
var lastUpdated: Long = -1,
|
||||
var autoUpdate: Boolean = false,
|
||||
val updateInterval: Int? = null,
|
||||
var prevProfile: String? = null,
|
||||
var nextProfile: String? = null,
|
||||
var filter: String? = null,
|
||||
var allowInsecureUrl: Boolean = false,
|
||||
var userAgent: String? = null
|
||||
)
|
||||
|
||||
class MMKVWriter(private val filePath: String) {
|
||||
private val gson = Gson()
|
||||
|
||||
fun writeEntry(key: String, json: String) {
|
||||
val file = RandomAccessFile(filePath, "rw")
|
||||
file.seek(file.length())
|
||||
|
||||
val keyBytes = key.toByteArray(Charsets.UTF_8)
|
||||
val jsonBytes = json.toByteArray(Charsets.UTF_8)
|
||||
|
||||
file.writeInt(keyBytes.size)
|
||||
file.write(keyBytes)
|
||||
file.writeInt(jsonBytes.size)
|
||||
file.write(jsonBytes)
|
||||
|
||||
file.close()
|
||||
}
|
||||
}
|
||||
|
||||
class SubscriptionManager(private val mmkvPath: String) {
|
||||
private val gson = Gson()
|
||||
private val writer = MMKVWriter(mmkvPath)
|
||||
|
||||
fun addSubscription(remarks: String, url: String, autoUpdate: Boolean = true): String {
|
||||
val guid = UUID.randomUUID().toString().replace("-", "")
|
||||
|
||||
val subItem = SubscriptionItem(
|
||||
remarks = remarks,
|
||||
url = url,
|
||||
enabled = true,
|
||||
addedTime = System.currentTimeMillis(),
|
||||
lastUpdated = -1,
|
||||
autoUpdate = autoUpdate,
|
||||
filter = "",
|
||||
allowInsecureUrl = false,
|
||||
userAgent = ""
|
||||
)
|
||||
|
||||
val json = gson.toJson(subItem)
|
||||
writer.writeEntry(guid, json)
|
||||
|
||||
return guid
|
||||
}
|
||||
|
||||
fun updateSubscription(guid: String, url: String): Boolean {
|
||||
return try {
|
||||
println("Обновление подписки $guid...")
|
||||
val content = URL(url).readText()
|
||||
val lines = content.lines().filter { it.isNotBlank() }
|
||||
println("Загружено ${lines.size} конфигураций")
|
||||
|
||||
val subItem = readSubscription(guid) ?: return false
|
||||
subItem.lastUpdated = System.currentTimeMillis()
|
||||
|
||||
val json = gson.toJson(subItem)
|
||||
writer.writeEntry(guid, json)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
println("Ошибка обновления: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun readSubscription(guid: String): SubscriptionItem? {
|
||||
val file = File(mmkvPath)
|
||||
if (!file.exists()) return null
|
||||
|
||||
val data = file.readBytes()
|
||||
var pos = 0
|
||||
|
||||
while (pos < data.size) {
|
||||
if (pos + 4 > data.size) break
|
||||
|
||||
val keyLen = java.nio.ByteBuffer.wrap(data, pos, 4).int
|
||||
pos += 4
|
||||
if (pos + keyLen > data.size) break
|
||||
|
||||
val key = String(data, pos, keyLen, Charsets.UTF_8)
|
||||
pos += keyLen
|
||||
|
||||
if (pos + 4 > data.size) break
|
||||
val jsonLen = java.nio.ByteBuffer.wrap(data, pos, 4).int
|
||||
pos += 4
|
||||
if (pos + jsonLen > data.size) break
|
||||
|
||||
val json = String(data, pos, jsonLen, Charsets.UTF_8)
|
||||
pos += jsonLen
|
||||
|
||||
if (key == guid) {
|
||||
return gson.fromJson(json, SubscriptionItem::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val mmkvPath = "/home/zarazaex/Projects/olcng/V2rayNG/app/src/main/assets/mmkv/SUB"
|
||||
|
||||
val manager = SubscriptionManager(mmkvPath)
|
||||
|
||||
val guid1 = manager.addSubscription(
|
||||
remarks = "БЕЛЫЕ Z",
|
||||
url = "https://raw.githubusercontent.com/zieng2/wl/refs/heads/main/vless_universal.txt",
|
||||
autoUpdate = true
|
||||
)
|
||||
println("Добавлена подписка БЕЛЫЕ Z: $guid1")
|
||||
|
||||
val guid2 = manager.addSubscription(
|
||||
remarks = "БЕЛЫЕ W",
|
||||
url = "https://raw.githubusercontent.com/whoahaow/rjsxrd/refs/heads/main/githubmirror/bypass/bypass-all.txt",
|
||||
autoUpdate = true
|
||||
)
|
||||
println("Добавлена подписка БЕЛЫЕ W: $guid2")
|
||||
|
||||
val guid3 = manager.addSubscription(
|
||||
remarks = "KEY",
|
||||
url = "https://key.zarazaex.xyz/sub",
|
||||
autoUpdate = true
|
||||
)
|
||||
println("Добавлена подписка KEY: $guid3")
|
||||
|
||||
println("\nОбновление подписок...")
|
||||
manager.updateSubscription(guid1, "https://raw.githubusercontent.com/zieng2/wl/refs/heads/main/vless_universal.txt")
|
||||
manager.updateSubscription(guid2, "https://raw.githubusercontent.com/whoahaow/rjsxrd/refs/heads/main/githubmirror/bypass/bypass-all.txt")
|
||||
manager.updateSubscription(guid3, "https://key.zarazaex.xyz/sub")
|
||||
|
||||
println("\nПодписки успешно добавлены и обновлены в $mmkvPath")
|
||||
}
|
||||
Reference in New Issue
Block a user