mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a04c53a045 | |||
| 278095015b | |||
| 3051309138 | |||
| 48373cfa0d | |||
| fb00560c72 | |||
| f81fb78702 | |||
| 340112272e | |||
| e220c380e2 | |||
| 611ac81228 |
+20
-11
@@ -66,21 +66,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
|
cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
|
||||||
|
|
||||||
- name: Fetch AndroidLibXrayLite tag
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
cache-dependency-path: AndroidLibXrayLite/go.sum
|
||||||
|
|
||||||
|
- name: Build libv2ray
|
||||||
run: |
|
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
|
pushd AndroidLibXrayLite
|
||||||
CURRENT_TAG=$(git describe --tags --abbrev=0)
|
mkdir -p assets data
|
||||||
echo "Current tag in this repo: $CURRENT_TAG"
|
# Download geo assets if needed
|
||||||
echo "CURRENT_TAG=$CURRENT_TAG" >> $GITHUB_ENV
|
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
|
popd
|
||||||
|
|
||||||
- name: Download libv2ray
|
mkdir -p V2rayNG/app/libs/
|
||||||
uses: robinraju/release-downloader@v1.12
|
cp AndroidLibXrayLite/libv2ray.aar V2rayNG/app/libs/
|
||||||
with:
|
|
||||||
repository: '2dust/AndroidLibXrayLite'
|
|
||||||
tag: ${{ env.CURRENT_TAG }}
|
|
||||||
fileName: 'libv2ray.aar'
|
|
||||||
out-file-path: V2rayNG/app/libs/
|
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
[submodule "AndroidLibXrayLite"]
|
|
||||||
path = AndroidLibXrayLite
|
|
||||||
url = https://github.com/2dust/AndroidLibXrayLite
|
|
||||||
[submodule "hev-socks5-tunnel"]
|
[submodule "hev-socks5-tunnel"]
|
||||||
path = hev-socks5-tunnel
|
path = hev-socks5-tunnel
|
||||||
url = https://github.com/heiher/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,398 @@
|
|||||||
|
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) {
|
||||||
|
return measureOutboundDelayInternal(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semaphore to limit concurrency (max 128 concurrent tests)
|
||||||
|
sem := make(chan struct{}, 128)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(it PingItem) {
|
||||||
|
defer wg.Done()
|
||||||
|
sem <- struct{}{}
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
delay, _ := measureOutboundDelayInternal(it.Config, url)
|
||||||
|
if callback != nil {
|
||||||
|
callback.OnResult(it.Guid, delay)
|
||||||
|
}
|
||||||
|
}(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func measureOutboundDelayInternal(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)
|
||||||
|
}
|
||||||
|
defer inst.Close()
|
||||||
|
return measureInstDelay(context.Background(), inst, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: false,
|
||||||
|
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: 5 * 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 as requested by user
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read body and close resp immediately
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict IP check: the body must contain a valid IP address
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,8 +119,8 @@ object AppConfig {
|
|||||||
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
||||||
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
||||||
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
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_URL = "https://icanhazip.com"
|
||||||
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
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://speed.cloudflare.com/meta"
|
||||||
const val IP_API_URL = "https://api.ip.sb/geoip"
|
const val IP_API_URL = "https://api.ip.sb/geoip"
|
||||||
|
|
||||||
@@ -164,6 +164,7 @@ object AppConfig {
|
|||||||
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
||||||
const val MSG_MEASURE_CONFIG_NOTIFY = 73
|
const val MSG_MEASURE_CONFIG_NOTIFY = 73
|
||||||
const val MSG_MEASURE_CONFIG_FINISH = 74
|
const val MSG_MEASURE_CONFIG_FINISH = 74
|
||||||
|
const val MSG_MEASURE_CONFIG_BATCH = 75
|
||||||
|
|
||||||
/** Notification channel IDs and names. */
|
/** Notification channel IDs and names. */
|
||||||
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package xyz.zarazaex.olc.dto
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class PingResultItem(
|
||||||
|
val guid: String,
|
||||||
|
val delay: Long
|
||||||
|
) : Serializable
|
||||||
|
|
||||||
|
data class PingProgressUpdate(
|
||||||
|
val results: ArrayList<PingResultItem>,
|
||||||
|
val finished: Int,
|
||||||
|
val total: Int
|
||||||
|
) : Serializable
|
||||||
@@ -13,6 +13,7 @@ data class ProfileItem(
|
|||||||
val configType: EConfigType,
|
val configType: EConfigType,
|
||||||
var subscriptionId: String = "",
|
var subscriptionId: String = "",
|
||||||
var addedTime: Long = System.currentTimeMillis(),
|
var addedTime: Long = System.currentTimeMillis(),
|
||||||
|
var isFavorite: Boolean = false,
|
||||||
|
|
||||||
var remarks: String = "",
|
var remarks: String = "",
|
||||||
var description: String? = null,
|
var description: String? = null,
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ object AngConfigManager {
|
|||||||
}?.key
|
}?.key
|
||||||
|
|
||||||
if (existingKey != null) {
|
if (existingKey != null) {
|
||||||
|
config.isFavorite = existingProfiles[existingKey]?.isFavorite ?: false
|
||||||
MmkvManager.encodeProfileDirect(existingKey, JsonUtil.toJson(config))
|
MmkvManager.encodeProfileDirect(existingKey, JsonUtil.toJson(config))
|
||||||
keyToProfile[existingKey] = config
|
keyToProfile[existingKey] = config
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ object SettingsManager {
|
|||||||
migrateServerListToSubscriptions()
|
migrateServerListToSubscriptions()
|
||||||
migrateHysteria2PinSHA256()
|
migrateHysteria2PinSHA256()
|
||||||
migrateAutoSort()
|
migrateAutoSort()
|
||||||
|
migrateDelayTestUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -573,4 +574,16 @@ 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.
|
* Create a new core controller instance.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
package xyz.zarazaex.olc.service
|
package xyz.zarazaex.olc.service
|
||||||
|
|
||||||
import android.content.Context
|
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.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
|
import xyz.zarazaex.olc.dto.PingProgressUpdate
|
||||||
|
import xyz.zarazaex.olc.dto.PingResultItem
|
||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
||||||
import xyz.zarazaex.olc.handler.V2rayConfigManager
|
import xyz.zarazaex.olc.handler.V2rayConfigManager
|
||||||
|
import xyz.zarazaex.olc.util.JsonUtil
|
||||||
import xyz.zarazaex.olc.util.MessageUtil
|
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.
|
* Worker that runs a batch of real-ping tests independently. Optimized to use Go-level concurrency
|
||||||
* Each batch owns its own CoroutineScope/dispatcher and can be cancelled separately.
|
* for improved performance.
|
||||||
*/
|
*/
|
||||||
class RealPingWorkerService(
|
class RealPingWorkerService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -27,75 +28,115 @@ class RealPingWorkerService(
|
|||||||
private val onFinish: (status: String) -> Unit = {}
|
private val onFinish: (status: String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
private val job = SupervisorJob()
|
private val job = SupervisorJob()
|
||||||
private val cpu = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
|
private val scope =
|
||||||
private val dispatcher = Executors.newFixedThreadPool(cpu * 16).asCoroutineDispatcher()
|
CoroutineScope(job + Dispatchers.Default + CoroutineName("RealPingBatchWorker"))
|
||||||
private val scope = CoroutineScope(job + dispatcher + CoroutineName("RealPingBatchWorker"))
|
|
||||||
|
|
||||||
private val runningCount = AtomicInteger(0)
|
private val totalCount = AtomicInteger(guids.size)
|
||||||
private val totalCount = AtomicInteger(0)
|
private val finishedCount = AtomicInteger(0)
|
||||||
|
private val pendingResults = ArrayList<PingResultItem>()
|
||||||
|
private val pendingLock = Any()
|
||||||
|
|
||||||
|
private val delayTestUrl = SettingsManager.getDelayTestUrl()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val RESULT_BATCH_SIZE = 32
|
||||||
|
private const val FLUSH_INTERVAL_MS = 1000L
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PingItem(val guid: String, val config: String)
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
val jobs = guids.map { guid ->
|
scope.launch(Dispatchers.IO) {
|
||||||
totalCount.incrementAndGet()
|
while (isActive) {
|
||||||
scope.launch {
|
delay(FLUSH_INTERVAL_MS)
|
||||||
runningCount.incrementAndGet()
|
flushPendingResults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val result = startRealPing(guid)
|
// Prepare configurations for batch test and shuffle for better async feel
|
||||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
|
val items =
|
||||||
|
guids.shuffled().mapNotNull { guid ->
|
||||||
|
val configResult =
|
||||||
|
V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||||
|
if (configResult.status) {
|
||||||
|
PingItem(guid, configResult.content)
|
||||||
|
} else {
|
||||||
|
// Notify failure immediately for invalid configs
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
flushPendingResults()
|
||||||
|
onFinish("0")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, -1L))
|
flushPendingResults()
|
||||||
|
onFinish("-1")
|
||||||
} finally {
|
} finally {
|
||||||
val count = totalCount.decrementAndGet()
|
cancel()
|
||||||
val left = runningCount.decrementAndGet()
|
|
||||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $count")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch {
|
private fun reportResult(guid: String, delay: Long) {
|
||||||
try {
|
val finished = finishedCount.incrementAndGet()
|
||||||
joinAll(*jobs.toTypedArray())
|
var readyBatch: PingProgressUpdate? = null
|
||||||
onFinish("0")
|
synchronized(pendingLock) {
|
||||||
} catch (_: CancellationException) {
|
pendingResults.add(PingResultItem(guid, delay))
|
||||||
onFinish("-1")
|
if (pendingResults.size >= RESULT_BATCH_SIZE || finished >= totalCount.get()) {
|
||||||
} finally {
|
readyBatch = createProgressUpdateLocked(finished)
|
||||||
close()
|
pendingResults.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
readyBatch?.let(::sendBatchUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flushPendingResults() {
|
||||||
|
val finished = finishedCount.get()
|
||||||
|
val update =
|
||||||
|
synchronized(pendingLock) {
|
||||||
|
if (pendingResults.isEmpty()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
createProgressUpdateLocked(finished).also { pendingResults.clear() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update?.let(::sendBatchUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createProgressUpdateLocked(finished: Int): PingProgressUpdate {
|
||||||
|
return PingProgressUpdate(
|
||||||
|
results = ArrayList(pendingResults),
|
||||||
|
finished = finished,
|
||||||
|
total = totalCount.get()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendBatchUpdate(update: PingProgressUpdate) {
|
||||||
|
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_BATCH, update)
|
||||||
|
val left = (update.total - update.finished).coerceAtLeast(0)
|
||||||
|
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / ${update.total}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun close() {
|
|
||||||
try {
|
|
||||||
dispatcher.close()
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startRealPing(guid: String): Long {
|
|
||||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
|
||||||
if (!configResult.status) return -1L
|
|
||||||
|
|
||||||
val urls = listOf(
|
|
||||||
SettingsManager.getDelayTestUrl(),
|
|
||||||
SettingsManager.getDelayTestUrl(true)
|
|
||||||
)
|
|
||||||
|
|
||||||
for (url in urls) {
|
|
||||||
try {
|
|
||||||
val delay = withTimeout(10000L) {
|
|
||||||
V2RayNativeManager.measureOutboundDelay(configResult.content, url)
|
|
||||||
}
|
|
||||||
if (delay > 0) return delay
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -381,6 +381,32 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_main, menu)
|
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)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import java.util.Collections
|
||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
import xyz.zarazaex.olc.R
|
import xyz.zarazaex.olc.R
|
||||||
import xyz.zarazaex.olc.contracts.MainAdapterListener
|
import xyz.zarazaex.olc.contracts.MainAdapterListener
|
||||||
@@ -14,13 +15,11 @@ import xyz.zarazaex.olc.databinding.ItemRecyclerFooterBinding
|
|||||||
import xyz.zarazaex.olc.databinding.ItemRecyclerMainBinding
|
import xyz.zarazaex.olc.databinding.ItemRecyclerMainBinding
|
||||||
import xyz.zarazaex.olc.dto.ProfileItem
|
import xyz.zarazaex.olc.dto.ProfileItem
|
||||||
import xyz.zarazaex.olc.dto.ServersCache
|
import xyz.zarazaex.olc.dto.ServersCache
|
||||||
import xyz.zarazaex.olc.extension.nullIfBlank
|
|
||||||
import xyz.zarazaex.olc.handler.AngConfigManager
|
import xyz.zarazaex.olc.handler.AngConfigManager
|
||||||
import xyz.zarazaex.olc.handler.MmkvManager
|
import xyz.zarazaex.olc.handler.MmkvManager
|
||||||
import xyz.zarazaex.olc.helper.ItemTouchHelperAdapter
|
import xyz.zarazaex.olc.helper.ItemTouchHelperAdapter
|
||||||
import xyz.zarazaex.olc.helper.ItemTouchHelperViewHolder
|
import xyz.zarazaex.olc.helper.ItemTouchHelperViewHolder
|
||||||
import xyz.zarazaex.olc.viewmodel.MainViewModel
|
import xyz.zarazaex.olc.viewmodel.MainViewModel
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
class MainRecyclerAdapter(
|
class MainRecyclerAdapter(
|
||||||
private val mainViewModel: MainViewModel,
|
private val mainViewModel: MainViewModel,
|
||||||
@@ -29,24 +28,129 @@ class MainRecyclerAdapter(
|
|||||||
companion object {
|
companion object {
|
||||||
private const val VIEW_TYPE_ITEM = 1
|
private const val VIEW_TYPE_ITEM = 1
|
||||||
private const val VIEW_TYPE_FOOTER = 2
|
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 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")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) {
|
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) {
|
||||||
data = newData?.toMutableList() ?: mutableListOf()
|
val parsedNewData = newData?.toList() ?: emptyList()
|
||||||
|
|
||||||
|
if (data.isEmpty() || parsedNewData.isEmpty() || position >= 0) {
|
||||||
|
data = parsedNewData.toMutableList()
|
||||||
if (position >= 0 && position in data.indices) {
|
if (position >= 0 && position in data.indices) {
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
} else {
|
} else {
|
||||||
notifyDataSetChanged()
|
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 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) {
|
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||||
if (holder is MainViewHolder) {
|
if (holder is MainViewHolder) {
|
||||||
val context = holder.itemMainBinding.root.context
|
val context = holder.itemMainBinding.root.context
|
||||||
@@ -63,9 +167,13 @@ class MainRecyclerAdapter(
|
|||||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
||||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
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 {
|
} else {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(context, R.color.colorPing))
|
holder.itemMainBinding.tvTestResult.setTextColor(
|
||||||
|
ContextCompat.getColor(context, R.color.colorPing)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// layoutIndicator
|
// layoutIndicator
|
||||||
@@ -78,18 +186,29 @@ class MainRecyclerAdapter(
|
|||||||
// subscription remarks
|
// subscription remarks
|
||||||
val subRemarks = getSubscriptionRemarks(profile)
|
val subRemarks = getSubscriptionRemarks(profile)
|
||||||
holder.itemMainBinding.tvSubscription.text = subRemarks
|
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.infoContainer.setOnClickListener {
|
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||||
adapterListener?.onSelectServer(guid)
|
adapterListener?.onSelectServer(guid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the server address information
|
* Gets the server address information Hides part of IP or domain information for privacy
|
||||||
* Hides part of IP or domain information for privacy protection
|
* protection
|
||||||
* @param profile The server configuration
|
* @param profile The server configuration
|
||||||
* @return Formatted address string
|
* @return Formatted address string
|
||||||
*/
|
*/
|
||||||
@@ -105,9 +224,10 @@ class MainRecyclerAdapter(
|
|||||||
private fun getSubscriptionRemarks(profile: ProfileItem): String {
|
private fun getSubscriptionRemarks(profile: ProfileItem): String {
|
||||||
val subRemarks =
|
val subRemarks =
|
||||||
if (mainViewModel.subscriptionId.isEmpty())
|
if (mainViewModel.subscriptionId.isEmpty())
|
||||||
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull()
|
MmkvManager.decodeSubscription(profile.subscriptionId)
|
||||||
else
|
?.remarks
|
||||||
null
|
?.firstOrNull()
|
||||||
|
else null
|
||||||
return subRemarks?.toString() ?: ""
|
return subRemarks?.toString() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,10 +248,21 @@ class MainRecyclerAdapter(
|
|||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
VIEW_TYPE_ITEM ->
|
VIEW_TYPE_ITEM ->
|
||||||
MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
MainViewHolder(
|
||||||
|
ItemRecyclerMainBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
else ->
|
else ->
|
||||||
FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
FooterViewHolder(
|
||||||
|
ItemRecyclerFooterBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +303,5 @@ class MainRecyclerAdapter(
|
|||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ import xyz.zarazaex.olc.AngApplication
|
|||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
import xyz.zarazaex.olc.R
|
import xyz.zarazaex.olc.R
|
||||||
import xyz.zarazaex.olc.dto.GroupMapItem
|
import xyz.zarazaex.olc.dto.GroupMapItem
|
||||||
|
import xyz.zarazaex.olc.dto.PingProgressUpdate
|
||||||
import xyz.zarazaex.olc.dto.ServersCache
|
import xyz.zarazaex.olc.dto.ServersCache
|
||||||
import xyz.zarazaex.olc.dto.SubscriptionCache
|
import xyz.zarazaex.olc.dto.SubscriptionCache
|
||||||
import xyz.zarazaex.olc.dto.SubscriptionUpdateResult
|
import xyz.zarazaex.olc.dto.SubscriptionUpdateResult
|
||||||
@@ -89,26 +90,29 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
else -> emptyList()
|
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>()
|
val allServers = mutableListOf<ServerWithDelay>()
|
||||||
|
|
||||||
groupSubs.forEach { sub ->
|
groupSubs.forEach { sub ->
|
||||||
val subServers = MmkvManager.decodeServerList(sub.guid)
|
val subServers = MmkvManager.decodeServerList(sub.guid)
|
||||||
subServers.forEach { guid ->
|
subServers.forEach { guid ->
|
||||||
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
||||||
|
val isFav = MmkvManager.decodeServerConfig(guid)?.isFavorite ?: false
|
||||||
val sortKey = when {
|
val sortKey = when {
|
||||||
delay > 0L -> delay
|
delay > 0L -> delay
|
||||||
delay == 0L -> Long.MAX_VALUE - 1
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
else -> Long.MAX_VALUE
|
else -> Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
allServers.add(ServerWithDelay(guid, sortKey))
|
allServers.add(ServerWithDelay(guid, sortKey, isFav))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allServers.sortBy { it.delay }
|
allServers.sortWith(compareBy({ !it.isFav }, { it.delay }))
|
||||||
allServers.map { it.guid }.toMutableList()
|
allServers.map { it.guid }.toMutableList()
|
||||||
} else {
|
} else {
|
||||||
MmkvManager.decodeServerList(subscriptionId)
|
val list = MmkvManager.decodeServerList(subscriptionId)
|
||||||
|
list.sortWith(compareBy { !(MmkvManager.decodeServerConfig(it)?.isFavorite ?: false) })
|
||||||
|
list
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCache()
|
updateCache()
|
||||||
@@ -466,7 +470,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sortByTestResultsForGroup(groupId: String) {
|
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 allSubs = MmkvManager.decodeSubscriptions()
|
||||||
val groupSubs = when (groupId) {
|
val groupSubs = when (groupId) {
|
||||||
@@ -487,16 +491,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val serverList = MmkvManager.decodeServerList(sub.guid)
|
val serverList = MmkvManager.decodeServerList(sub.guid)
|
||||||
serverList.forEach { guid ->
|
serverList.forEach { guid ->
|
||||||
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
||||||
|
val isFav = MmkvManager.decodeServerConfig(guid)?.isFavorite ?: false
|
||||||
val sortKey = when {
|
val sortKey = when {
|
||||||
delay > 0L -> delay
|
delay > 0L -> delay
|
||||||
delay == 0L -> Long.MAX_VALUE - 1
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
else -> Long.MAX_VALUE
|
else -> Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
allServerDelays.add(ServerDelay(guid, sortKey, sub.guid))
|
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 }
|
val serversBySubId = allServerDelays.groupBy { it.subId }
|
||||||
serversBySubId.forEach { (subId, servers) ->
|
serversBySubId.forEach { (subId, servers) ->
|
||||||
@@ -510,21 +515,22 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* @param subId The subscription ID to sort servers for.
|
* @param subId The subscription ID to sort servers for.
|
||||||
*/
|
*/
|
||||||
private fun sortByTestResultsForSub(subId: String) {
|
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 serverDelays = mutableListOf<ServerDelay>()
|
||||||
val serverListToSort = MmkvManager.decodeServerList(subId)
|
val serverListToSort = MmkvManager.decodeServerList(subId)
|
||||||
|
|
||||||
serverListToSort.forEach { key ->
|
serverListToSort.forEach { key ->
|
||||||
val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
|
val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
|
||||||
|
val isFav = MmkvManager.decodeServerConfig(key)?.isFavorite ?: false
|
||||||
val sortKey = when {
|
val sortKey = when {
|
||||||
delay > 0L -> delay
|
delay > 0L -> delay
|
||||||
delay == 0L -> Long.MAX_VALUE - 1
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
else -> Long.MAX_VALUE
|
else -> Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
serverDelays.add(ServerDelay(key, sortKey))
|
serverDelays.add(ServerDelay(key, sortKey, isFav))
|
||||||
}
|
}
|
||||||
serverDelays.sortBy { it.testDelayMillis }
|
serverDelays.sortWith(compareBy({ !it.isFav }, { it.testDelayMillis }))
|
||||||
|
|
||||||
val sortedServerList = serverDelays.map { it.guid }.toMutableList()
|
val sortedServerList = serverDelays.map { it.guid }.toMutableList()
|
||||||
|
|
||||||
@@ -623,6 +629,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
updateListAction.value = getPosition(resultPair.first)
|
updateListAction.value = getPosition(resultPair.first)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppConfig.MSG_MEASURE_CONFIG_BATCH -> {
|
||||||
|
val update = intent.serializable<PingProgressUpdate>("content") ?: return
|
||||||
|
update.results.forEach { result ->
|
||||||
|
MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay)
|
||||||
|
}
|
||||||
|
updateListAction.value = -1
|
||||||
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
|
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
|
||||||
val content = intent.getStringExtra("content")
|
val content = intent.getStringExtra("content")
|
||||||
updateTestResultAction.value =
|
updateTestResultAction.value =
|
||||||
|
|||||||
@@ -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,16 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</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" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
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
|
<item
|
||||||
android:id="@+id/real_ping_all"
|
android:id="@+id/real_ping_all"
|
||||||
android:icon="@drawable/ic_outline_filter_alt_24"
|
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