mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a04c53a045 | |||
| 278095015b | |||
| 3051309138 | |||
| 48373cfa0d | |||
| fb00560c72 | |||
| f81fb78702 | |||
| 340112272e | |||
| e220c380e2 | |||
| 611ac81228 | |||
| db511c9ab1 | |||
| 2623b3110b | |||
| 4bf125b4f5 | |||
| 50297d88cc | |||
| 2975b0cd1a | |||
| ea1f584422 | |||
| 73c81d02e7 | |||
| 8d803b8ff9 | |||
| 81d979546c | |||
| 9362d1ef01 | |||
| ac9760aedd | |||
| 1178b87fb7 | |||
| 4fea86944f | |||
| ff2fea8c03 | |||
| 415f2230e6 | |||
| fb827c1fb1 | |||
| cb4e3aab54 | |||
| 732d7248af | |||
| 0c32ddc642 | |||
| 153c545400 | |||
| 9af6472f8b | |||
| 346be42e37 | |||
| dd9e076ab1 | |||
| 903b0d4414 | |||
| f58a4f8f6f | |||
| fdce3ea2c1 | |||
| f0d620676d |
+21
-13
@@ -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
|
||||||
@@ -138,7 +147,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
chmod 755 gradlew
|
chmod 755 gradlew
|
||||||
./gradlew licenseFdroidReleaseReport
|
|
||||||
./gradlew assembleRelease --info 2>&1 | grep -i "signing\|keystore" || true
|
./gradlew assembleRelease --info 2>&1 | grep -i "signing\|keystore" || true
|
||||||
|
|
||||||
- name: Upload arm64-v8a APK
|
- name: Upload arm64-v8a APK
|
||||||
@@ -168,5 +176,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*.apk
|
files: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*.apk
|
||||||
tag_name: ${{ github.event.inputs.release_tag || github.ref_name }}
|
tag_name: ${{ github.event.inputs.release_tag || github.ref_name }}
|
||||||
prerelease: true
|
prerelease: false
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
|
||||||
id("com.jaredsburrows.license")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -19,8 +17,6 @@ android {
|
|||||||
versionCode = envVersionCode?.toIntOrNull() ?: 717
|
versionCode = envVersionCode?.toIntOrNull() ?: 717
|
||||||
versionName = envVersionName ?: "2.0.17"
|
versionName = envVersionName ?: "2.0.17"
|
||||||
|
|
||||||
multiDexEnabled = true
|
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
@@ -90,7 +86,7 @@ android {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("main") {
|
getByName("main") {
|
||||||
jniLibs.srcDirs("libs")
|
jniLibs.directories.add("libs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,50 +102,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationVariants.all {
|
|
||||||
val variant = this
|
|
||||||
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
|
|
||||||
if (isFdroid) {
|
|
||||||
val versionCodes =
|
|
||||||
mapOf(
|
|
||||||
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
|
|
||||||
)
|
|
||||||
|
|
||||||
variant.outputs
|
|
||||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
|
||||||
.forEach { output ->
|
|
||||||
val abi = output.getFilter("ABI") ?: "universal"
|
|
||||||
output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk"
|
|
||||||
if (versionCodes.containsKey(abi)) {
|
|
||||||
output.versionCodeOverride =
|
|
||||||
(100 * variant.versionCode + versionCodes[abi]!!).plus(5000000)
|
|
||||||
} else {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val versionCodes =
|
|
||||||
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
|
|
||||||
|
|
||||||
variant.outputs
|
|
||||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
|
||||||
.forEach { output ->
|
|
||||||
val abi = if (output.getFilter("ABI") != null)
|
|
||||||
output.getFilter("ABI")
|
|
||||||
else
|
|
||||||
"universal"
|
|
||||||
|
|
||||||
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
|
||||||
if (versionCodes.containsKey(abi)) {
|
|
||||||
output.versionCodeOverride =
|
|
||||||
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
|
||||||
} else {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
@@ -163,6 +115,28 @@ android {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants { variant ->
|
||||||
|
val isFdroid = variant.productFlavors.any { it.second == "fdroid" }
|
||||||
|
variant.outputs.forEach { output ->
|
||||||
|
val abi = output.filters.find {
|
||||||
|
it.filterType == com.android.build.api.variant.FilterConfiguration.FilterType.ABI
|
||||||
|
}?.identifier ?: "universal"
|
||||||
|
|
||||||
|
if (isFdroid) {
|
||||||
|
val versionCodes = mapOf(
|
||||||
|
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
|
||||||
|
)
|
||||||
|
versionCodes[abi]?.let { code ->
|
||||||
|
output.versionCode.set((100 * (output.versionCode.get() ?: 0) + code) + 5000000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.versionCode.set(1000000 * 4 + (output.versionCode.get() ?: 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Core Libraries
|
// Core Libraries
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
||||||
@@ -210,9 +184,6 @@ dependencies {
|
|||||||
implementation(libs.work.runtime.ktx)
|
implementation(libs.work.runtime.ktx)
|
||||||
implementation(libs.work.multiprocess)
|
implementation(libs.work.multiprocess)
|
||||||
|
|
||||||
// Multidex Support
|
|
||||||
implementation(libs.multidex)
|
|
||||||
|
|
||||||
// Testing Libraries
|
// Testing Libraries
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
@@ -210,6 +211,16 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.ServiceControlReceiver"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="${applicationId}.action.service.stop" />
|
||||||
|
<action android:name="${applicationId}.action.service.start" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.QSTileService"
|
android:name=".service.QSTileService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package xyz.zarazaex.olc
|
package xyz.zarazaex.olc
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.multidex.MultiDexApplication
|
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
@@ -9,7 +9,7 @@ import com.tencent.mmkv.MMKV
|
|||||||
import xyz.zarazaex.olc.AppConfig.ANG_PACKAGE
|
import xyz.zarazaex.olc.AppConfig.ANG_PACKAGE
|
||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
|
|
||||||
class AngApplication : MultiDexApplication() {
|
class AngApplication : Application() {
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var application: AngApplication
|
lateinit var application: AngApplication
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ object AppConfig {
|
|||||||
const val PREF_USE_HEV_TUNNEL = "pref_use_hev_tunnel_v2"
|
const val PREF_USE_HEV_TUNNEL = "pref_use_hev_tunnel_v2"
|
||||||
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
|
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
|
||||||
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
|
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
|
||||||
const val PREF_AUTO_REMOVE_INVALID_AFTER_TEST = "pref_auto_remove_invalid_after_test"
|
|
||||||
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
|
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
|
||||||
|
|
||||||
/** Cache keys. */
|
/** Cache keys. */
|
||||||
@@ -85,6 +84,8 @@ object AppConfig {
|
|||||||
const val BROADCAST_ACTION_SERVICE = "$ANG_PACKAGE.action.service"
|
const val BROADCAST_ACTION_SERVICE = "$ANG_PACKAGE.action.service"
|
||||||
const val BROADCAST_ACTION_ACTIVITY = "$ANG_PACKAGE.action.activity"
|
const val BROADCAST_ACTION_ACTIVITY = "$ANG_PACKAGE.action.activity"
|
||||||
const val BROADCAST_ACTION_WIDGET_CLICK = "$ANG_PACKAGE.action.widget.click"
|
const val BROADCAST_ACTION_WIDGET_CLICK = "$ANG_PACKAGE.action.widget.click"
|
||||||
|
const val BROADCAST_ACTION_SERVICE_STOP = "$ANG_PACKAGE.action.service.stop"
|
||||||
|
const val BROADCAST_ACTION_SERVICE_START = "$ANG_PACKAGE.action.service.start"
|
||||||
|
|
||||||
/** Tasker extras. */
|
/** Tasker extras. */
|
||||||
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
|
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
|
||||||
@@ -118,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"
|
||||||
|
|
||||||
@@ -163,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,
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package xyz.zarazaex.olc.dto
|
|||||||
|
|
||||||
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
||||||
fun getTestDelayString(): String {
|
fun getTestDelayString(): String {
|
||||||
if (testDelayMillis == 0L) {
|
return when {
|
||||||
return ""
|
testDelayMillis == 0L -> ""
|
||||||
|
testDelayMillis < 0L -> "Error"
|
||||||
|
else -> "${testDelayMillis}ms"
|
||||||
}
|
}
|
||||||
return testDelayMillis.toString() + "ms"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isReachable(): Boolean = testDelayMillis > 0L
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package xyz.zarazaex.olc.dto
|
||||||
|
|
||||||
|
enum class SubscriptionUpdateStatus {
|
||||||
|
IDLE,
|
||||||
|
LOADING,
|
||||||
|
SUCCESS,
|
||||||
|
FAILED,
|
||||||
|
SKIPPED
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SubscriptionStatus(
|
||||||
|
val guid: String,
|
||||||
|
val status: SubscriptionUpdateStatus = SubscriptionUpdateStatus.IDLE,
|
||||||
|
val configCount: Int = 0
|
||||||
|
)
|
||||||
@@ -29,6 +29,14 @@ import java.net.URI
|
|||||||
|
|
||||||
object AngConfigManager {
|
object AngConfigManager {
|
||||||
|
|
||||||
|
private val subscriptionLocks = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
private fun getSubscriptionLock(subid: String): Any {
|
||||||
|
return synchronized(subscriptionLocks) {
|
||||||
|
subscriptionLocks.getOrPut(subid) { Any() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shares the configuration to the clipboard.
|
* Shares the configuration to the clipboard.
|
||||||
@@ -217,11 +225,11 @@ object AngConfigManager {
|
|||||||
* @return The number of configurations parsed.
|
* @return The number of configurations parsed.
|
||||||
*/
|
*/
|
||||||
private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
|
private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
|
||||||
|
return synchronized(getSubscriptionLock(subid)) {
|
||||||
try {
|
try {
|
||||||
if (servers == null) {
|
if (servers == null) {
|
||||||
return 0
|
return@synchronized 0
|
||||||
}
|
}
|
||||||
// Find the currently selected server that matches the subscription ID
|
|
||||||
val removedSelected = if (subid.isNotBlank() && !append) {
|
val removedSelected = if (subid.isNotBlank() && !append) {
|
||||||
MmkvManager.getSelectServer()
|
MmkvManager.getSelectServer()
|
||||||
.takeIf { it?.isNotBlank() == true }
|
.takeIf { it?.isNotBlank() == true }
|
||||||
@@ -233,7 +241,12 @@ object AngConfigManager {
|
|||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
|
|
||||||
// Parse all configs first (no I/O during parsing)
|
val oldPingData = if (!append) {
|
||||||
|
saveOldPingData(subid)
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
val configs = mutableListOf<ProfileItem>()
|
val configs = mutableListOf<ProfileItem>()
|
||||||
servers.lines()
|
servers.lines()
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -245,21 +258,22 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch save all parsed configs (only one serverList read/write)
|
|
||||||
if (configs.isNotEmpty()) {
|
if (configs.isNotEmpty()) {
|
||||||
if (!append) {
|
if (!append) {
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
val keyToProfile = batchSaveConfigs(configs, subid)
|
val keyToProfile = batchSaveConfigs(configs, subid, append)
|
||||||
|
restoreOldPingData(keyToProfile, oldPingData)
|
||||||
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||||
matchKey?.let { MmkvManager.setSelectServer(it) }
|
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs.size
|
return@synchronized configs.size
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
||||||
}
|
}
|
||||||
return 0
|
return@synchronized 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,15 +284,23 @@ object AngConfigManager {
|
|||||||
* @param subid The subscription ID.
|
* @param subid The subscription ID.
|
||||||
* @return Map of generated keys to their corresponding ProfileItem.
|
* @return Map of generated keys to their corresponding ProfileItem.
|
||||||
*/
|
*/
|
||||||
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): Map<String, ProfileItem> {
|
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String, append: Boolean): Map<String, ProfileItem> {
|
||||||
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
||||||
|
|
||||||
val serverList = MmkvManager.decodeServerList(subid)
|
val serverList = if (append) {
|
||||||
|
MmkvManager.decodeServerList(subid)
|
||||||
|
} else {
|
||||||
|
mutableListOf()
|
||||||
|
}
|
||||||
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
||||||
|
|
||||||
val existingProfiles = serverList.mapNotNull { guid ->
|
val existingProfiles = if (append) {
|
||||||
|
serverList.mapNotNull { guid ->
|
||||||
MmkvManager.decodeServerConfig(guid)?.let { guid to it }
|
MmkvManager.decodeServerConfig(guid)?.let { guid to it }
|
||||||
}.toMap()
|
}.toMap()
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
configs.forEach { config ->
|
configs.forEach { config ->
|
||||||
val existingKey = existingProfiles.entries.firstOrNull { (_, existing) ->
|
val existingKey = existingProfiles.entries.firstOrNull { (_, existing) ->
|
||||||
@@ -286,6 +308,8 @@ object AngConfigManager {
|
|||||||
}?.key
|
}?.key
|
||||||
|
|
||||||
if (existingKey != null) {
|
if (existingKey != null) {
|
||||||
|
config.isFavorite = existingProfiles[existingKey]?.isFavorite ?: false
|
||||||
|
MmkvManager.encodeProfileDirect(existingKey, JsonUtil.toJson(config))
|
||||||
keyToProfile[existingKey] = config
|
keyToProfile[existingKey] = config
|
||||||
} else {
|
} else {
|
||||||
val key = Utils.getUuid()
|
val key = Utils.getUuid()
|
||||||
@@ -306,6 +330,35 @@ object AngConfigManager {
|
|||||||
return keyToProfile
|
return keyToProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveOldPingData(subid: String): Map<ProfileItem, Long> {
|
||||||
|
val pingData = mutableMapOf<ProfileItem, Long>()
|
||||||
|
val serverList = MmkvManager.decodeServerList(subid)
|
||||||
|
|
||||||
|
serverList.forEach { guid ->
|
||||||
|
val profile = MmkvManager.decodeServerConfig(guid)
|
||||||
|
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||||
|
if (profile != null && aff != null && aff.testDelayMillis > 0) {
|
||||||
|
pingData[profile] = aff.testDelayMillis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pingData
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreOldPingData(keyToProfile: Map<String, ProfileItem>, oldPingData: Map<ProfileItem, Long>) {
|
||||||
|
if (oldPingData.isEmpty()) return
|
||||||
|
|
||||||
|
keyToProfile.forEach { (key, newProfile) ->
|
||||||
|
val oldPing = oldPingData.entries.firstOrNull { (oldProfile, _) ->
|
||||||
|
oldProfile == newProfile
|
||||||
|
}?.value
|
||||||
|
|
||||||
|
if (oldPing != null && oldPing > 0) {
|
||||||
|
MmkvManager.encodeServerTestDelayMillis(key, oldPing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a matched profile key from the given key-profile map using multi-level matching.
|
* Finds a matched profile key from the given key-profile map using multi-level matching.
|
||||||
* Matching priority (from highest to lowest):
|
* Matching priority (from highest to lowest):
|
||||||
@@ -370,8 +423,9 @@ object AngConfigManager {
|
|||||||
* @return The number of configurations parsed.
|
* @return The number of configurations parsed.
|
||||||
*/
|
*/
|
||||||
private fun parseCustomConfigServer(server: String?, subid: String, append: Boolean): Int {
|
private fun parseCustomConfigServer(server: String?, subid: String, append: Boolean): Int {
|
||||||
|
return synchronized(getSubscriptionLock(subid)) {
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
return 0
|
return@synchronized 0
|
||||||
}
|
}
|
||||||
if (server.contains("inbounds")
|
if (server.contains("inbounds")
|
||||||
&& server.contains("outbounds")
|
&& server.contains("outbounds")
|
||||||
@@ -394,15 +448,14 @@ object AngConfigManager {
|
|||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
return count
|
return@synchronized count
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For compatibility
|
val config = CustomFmt.parse(server) ?: return@synchronized 0
|
||||||
val config = CustomFmt.parse(server) ?: return 0
|
|
||||||
config.subscriptionId = subid
|
config.subscriptionId = subid
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
@@ -410,27 +463,28 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
return 1
|
return@synchronized 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
||||||
}
|
}
|
||||||
return 0
|
return@synchronized 0
|
||||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
||||||
try {
|
try {
|
||||||
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
val config = WireguardFmt.parseWireguardConfFile(server) ?: return@synchronized R.string.toast_incorrect_protocol
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
return 1
|
return@synchronized 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
||||||
}
|
}
|
||||||
return 0
|
return@synchronized 0
|
||||||
} else {
|
} else {
|
||||||
return 0
|
return@synchronized 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,16 +596,18 @@ object AngConfigManager {
|
|||||||
Log.i(AppConfig.TAG, url)
|
Log.i(AppConfig.TAG, url)
|
||||||
val userAgent = it.subscription.userAgent
|
val userAgent = it.subscription.userAgent
|
||||||
|
|
||||||
|
val timeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 15000
|
||||||
|
|
||||||
var configText = try {
|
var configText = try {
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort)
|
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout, httpPort)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
if (configText.isEmpty()) {
|
if (configText.isEmpty()) {
|
||||||
configText = try {
|
configText = try {
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent)
|
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -292,34 +292,6 @@ object MmkvManager {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes invalid server configurations.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @return The number of server configurations removed.
|
|
||||||
*/
|
|
||||||
fun removeInvalidServer(guid: String): Int {
|
|
||||||
var count = 0
|
|
||||||
if (guid.isNotEmpty()) {
|
|
||||||
decodeServerAffiliationInfo(guid)?.let { aff ->
|
|
||||||
if (aff.testDelayMillis < 0L) {
|
|
||||||
removeServer(guid)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
serverAffStorage.allKeys()?.forEach { key ->
|
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
|
||||||
if (aff.testDelayMillis < 0L) {
|
|
||||||
removeServer(key)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes the raw server configuration.
|
* Encodes the raw server configuration.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import java.io.FileOutputStream
|
|||||||
|
|
||||||
object UpdateCheckerManager {
|
object UpdateCheckerManager {
|
||||||
suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
|
suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
val url = if (includePreRelease) {
|
val url = if (includePreRelease) {
|
||||||
AppConfig.APP_API_URL
|
AppConfig.APP_API_URL
|
||||||
} else {
|
} else {
|
||||||
@@ -59,6 +60,13 @@ object UpdateCheckerManager {
|
|||||||
} else {
|
} else {
|
||||||
CheckUpdateResult(hasUpdate = false)
|
CheckUpdateResult(hasUpdate = false)
|
||||||
}
|
}
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to parse version: ${e.message}")
|
||||||
|
return@withContext CheckUpdateResult(hasUpdate = false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to check for update: ${e.message}")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
|
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
|
||||||
@@ -95,12 +103,15 @@ object UpdateCheckerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compareVersions(version1: String, version2: String): Int {
|
private fun compareVersions(version1: String, version2: String): Int {
|
||||||
val v1 = version1.split(".")
|
val cleanVersion1 = version1.split("-")[0]
|
||||||
val v2 = version2.split(".")
|
val cleanVersion2 = version2.split("-")[0]
|
||||||
|
|
||||||
|
val v1 = cleanVersion1.split(".")
|
||||||
|
val v2 = cleanVersion2.split(".")
|
||||||
|
|
||||||
for (i in 0 until maxOf(v1.size, v2.size)) {
|
for (i in 0 until maxOf(v1.size, v2.size)) {
|
||||||
val num1 = if (i < v1.size) v1[i].toInt() else 0
|
val num1 = if (i < v1.size) v1[i].toIntOrNull() ?: 0 else 0
|
||||||
val num2 = if (i < v2.size) v2[i].toInt() else 0
|
val num2 = if (i < v2.size) v2[i].toIntOrNull() ?: 0 else 0
|
||||||
if (num1 != num2) return num1 - num2
|
if (num1 != num2) return num1 - num2
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ object V2RayServiceManager {
|
|||||||
private val coreController: CoreController = V2RayNativeManager.newCoreController(CoreCallback())
|
private val coreController: CoreController = V2RayNativeManager.newCoreController(CoreCallback())
|
||||||
private val mMsgReceive = ReceiveMessageHandler()
|
private val mMsgReceive = ReceiveMessageHandler()
|
||||||
private var currentConfig: ProfileItem? = null
|
private var currentConfig: ProfileItem? = null
|
||||||
|
private val operationLock = Any()
|
||||||
|
@Volatile private var isOperationInProgress = false
|
||||||
|
@Volatile var isIntentionalStop = false
|
||||||
|
|
||||||
var serviceControl: SoftReference<ServiceControl>? = null
|
var serviceControl: SoftReference<ServiceControl>? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -47,6 +50,7 @@ object V2RayServiceManager {
|
|||||||
context.toast(R.string.app_tile_first_use)
|
context.toast(R.string.app_tile_first_use)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
isIntentionalStop = false
|
||||||
startContextService(context)
|
startContextService(context)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -57,13 +61,28 @@ object V2RayServiceManager {
|
|||||||
* @param guid The GUID of the server configuration to use (optional).
|
* @param guid The GUID of the server configuration to use (optional).
|
||||||
*/
|
*/
|
||||||
fun startVService(context: Context, guid: String? = null) {
|
fun startVService(context: Context, guid: String? = null) {
|
||||||
|
synchronized(operationLock) {
|
||||||
|
if (isOperationInProgress) {
|
||||||
|
Log.w(AppConfig.TAG, "StartCore-Manager: Operation already in progress")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isOperationInProgress = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
|
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
|
||||||
|
|
||||||
if (guid != null) {
|
if (guid != null) {
|
||||||
MmkvManager.setSelectServer(guid)
|
MmkvManager.setSelectServer(guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isIntentionalStop = false
|
||||||
startContextService(context)
|
startContextService(context)
|
||||||
|
} finally {
|
||||||
|
synchronized(operationLock) {
|
||||||
|
isOperationInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,8 +90,16 @@ object V2RayServiceManager {
|
|||||||
* @param context The context from which the service is stopped.
|
* @param context The context from which the service is stopped.
|
||||||
*/
|
*/
|
||||||
fun stopVService(context: Context) {
|
fun stopVService(context: Context) {
|
||||||
//context.toast(R.string.toast_services_stop)
|
Log.i(AppConfig.TAG, "StartCore-Manager: stopVService called")
|
||||||
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
isIntentionalStop = true
|
||||||
|
val svc = serviceControl?.get()
|
||||||
|
if (svc != null) {
|
||||||
|
svc.stopService()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val intent = Intent(AppConfig.BROADCAST_ACTION_SERVICE_STOP)
|
||||||
|
intent.setPackage(AppConfig.ANG_PACKAGE)
|
||||||
|
context.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -228,23 +255,26 @@ object V2RayServiceManager {
|
|||||||
fun stopCoreLoop(): Boolean {
|
fun stopCoreLoop(): Boolean {
|
||||||
val service = getService() ?: return false
|
val service = getService() ?: return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.unregisterReceiver(mMsgReceive)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManager.cancelNotification()
|
||||||
|
|
||||||
if (coreController.isRunning) {
|
if (coreController.isRunning) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
coreController.stopLoop()
|
coreController.stopLoop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
|
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
|
||||||
}
|
} finally {
|
||||||
}
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||||
NotificationManager.cancelNotification()
|
|
||||||
|
|
||||||
try {
|
|
||||||
service.unregisterReceiver(mMsgReceive)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -334,10 +364,24 @@ object V2RayServiceManager {
|
|||||||
override fun shutdown(): Long {
|
override fun shutdown(): Long {
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
val serviceControl = serviceControl?.get() ?: return -1
|
||||||
return try {
|
return try {
|
||||||
serviceControl.stopService()
|
Log.w(AppConfig.TAG, "StartCore-Manager: Core shutdown callback, attempting restart")
|
||||||
|
val service = serviceControl.getService()
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||||
|
if (isIntentionalStop) {
|
||||||
|
Log.i(AppConfig.TAG, "StartCore-Manager: Intentional stop, skipping restart")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
kotlinx.coroutines.delay(1000L)
|
||||||
|
val ctx = service.applicationContext
|
||||||
|
if (coreController.isRunning == false) {
|
||||||
|
Log.i(AppConfig.TAG, "StartCore-Manager: Restarting service after core shutdown")
|
||||||
|
startVService(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
0
|
0
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
|
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to handle core shutdown", e)
|
||||||
-1
|
-1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,34 +409,46 @@ object V2RayServiceManager {
|
|||||||
* @param intent The intent being received.
|
* @param intent The intent being received.
|
||||||
*/
|
*/
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||||
val serviceControl = serviceControl?.get() ?: return
|
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
when (intent?.getIntExtra("key", 0)) {
|
||||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
AppConfig.MSG_REGISTER_CLIENT -> {
|
||||||
|
val svc = serviceControl?.get() ?: return
|
||||||
if (coreController.isRunning) {
|
if (coreController.isRunning) {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
MessageUtil.sendMsg2UI(svc.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
||||||
} else {
|
} else {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
MessageUtil.sendMsg2UI(svc.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
||||||
// nothing to do
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START -> {
|
AppConfig.MSG_STATE_START -> {
|
||||||
// nothing to do
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
AppConfig.MSG_STATE_STOP -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
||||||
serviceControl.stopService()
|
synchronized(operationLock) {
|
||||||
|
isOperationInProgress = false
|
||||||
|
}
|
||||||
|
val svc = serviceControl?.get()
|
||||||
|
if (svc != null) {
|
||||||
|
svc.stopService()
|
||||||
|
} else if (ctx != null) {
|
||||||
|
Log.w(AppConfig.TAG, "StartCore-Manager: serviceControl null on stop, stopping core directly")
|
||||||
|
stopCoreLoop()
|
||||||
|
ctx.stopService(Intent(ctx, V2RayVpnService::class.java))
|
||||||
|
ctx.stopService(Intent(ctx, V2RayProxyOnlyService::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
AppConfig.MSG_STATE_RESTART -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
||||||
serviceControl.stopService()
|
synchronized(operationLock) {
|
||||||
|
isOperationInProgress = false
|
||||||
|
}
|
||||||
|
serviceControl?.get()?.stopService()
|
||||||
Thread.sleep(500L)
|
Thread.sleep(500L)
|
||||||
startVService(serviceControl.getService())
|
if (ctx != null) startVService(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_DELAY -> {
|
AppConfig.MSG_MEASURE_DELAY -> {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package xyz.zarazaex.olc.receiver
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import xyz.zarazaex.olc.AppConfig
|
||||||
|
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
||||||
|
|
||||||
|
class ServiceControlReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
AppConfig.BROADCAST_ACTION_SERVICE_STOP -> {
|
||||||
|
V2RayServiceManager.isIntentionalStop = true
|
||||||
|
V2RayServiceManager.stopVService(context)
|
||||||
|
}
|
||||||
|
AppConfig.BROADCAST_ACTION_SERVICE_START -> V2RayServiceManager.startVServiceFromToggle(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +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 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,
|
||||||
@@ -26,62 +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 fun startRealPing(guid: String): Long {
|
|
||||||
val retFailure = -1L
|
|
||||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
|
||||||
if (!configResult.status) {
|
|
||||||
return retFailure
|
|
||||||
}
|
|
||||||
return V2RayNativeManager.measureOutboundDelay(configResult.content, SettingsManager.getDelayTestUrl())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Proxy: Service command received")
|
Log.i(AppConfig.TAG, "StartCore-Proxy: Service command received")
|
||||||
V2RayServiceManager.startCoreLoop(null)
|
V2RayServiceManager.startCoreLoop(null)
|
||||||
return START_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.net.ProxyInfo
|
|||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.PowerManager
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -25,6 +26,7 @@ import xyz.zarazaex.olc.handler.MmkvManager
|
|||||||
import xyz.zarazaex.olc.handler.NotificationManager
|
import xyz.zarazaex.olc.handler.NotificationManager
|
||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
||||||
|
import xyz.zarazaex.olc.util.MessageUtil
|
||||||
import xyz.zarazaex.olc.util.MyContextWrapper
|
import xyz.zarazaex.olc.util.MyContextWrapper
|
||||||
import xyz.zarazaex.olc.util.Utils
|
import xyz.zarazaex.olc.util.Utils
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
@@ -34,6 +36,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
private lateinit var mInterface: ParcelFileDescriptor
|
private lateinit var mInterface: ParcelFileDescriptor
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
private var tun2SocksService: Tun2SocksControl? = null
|
private var tun2SocksService: Tun2SocksControl? = null
|
||||||
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
|
||||||
/**destroy
|
/**destroy
|
||||||
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||||
@@ -78,6 +81,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
||||||
StrictMode.setThreadPolicy(policy)
|
StrictMode.setThreadPolicy(policy)
|
||||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||||
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||||
|
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "v2rayng:vpn")
|
||||||
|
.also { it.acquire() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRevoke() {
|
override fun onRevoke() {
|
||||||
@@ -93,15 +99,16 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
|
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
|
||||||
NotificationManager.cancelNotification()
|
stopAllService(false)
|
||||||
|
wakeLock?.let { if (it.isHeld) it.release() }
|
||||||
|
wakeLock = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.i(AppConfig.TAG, "StartCore-VPN: Service command received")
|
Log.i(AppConfig.TAG, "StartCore-VPN: Service command received")
|
||||||
setupVpnService()
|
setupVpnService()
|
||||||
startService()
|
startService()
|
||||||
return START_STICKY
|
return START_NOT_STICKY
|
||||||
//return super.onStartCommand(intent, flags, startId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getService(): Service {
|
override fun getService(): Service {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import xyz.zarazaex.olc.handler.SettingsChangeManager
|
|||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
import xyz.zarazaex.olc.handler.UpdateCheckerManager
|
import xyz.zarazaex.olc.handler.UpdateCheckerManager
|
||||||
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
||||||
|
import xyz.zarazaex.olc.util.MessageUtil
|
||||||
import xyz.zarazaex.olc.util.Utils
|
import xyz.zarazaex.olc.util.Utils
|
||||||
import xyz.zarazaex.olc.viewmodel.MainViewModel
|
import xyz.zarazaex.olc.viewmodel.MainViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -42,9 +43,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||||
private val binding by lazy {
|
private val binding by lazy {ActivityMainBinding.inflate(layoutInflater)}
|
||||||
ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
}
|
|
||||||
private var isLiteTesting = false
|
private var isLiteTesting = false
|
||||||
private var easterEggClickCount = 0
|
private var easterEggClickCount = 0
|
||||||
private var isEasterEggActive = false
|
private var isEasterEggActive = false
|
||||||
@@ -52,6 +51,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
val mainViewModel: MainViewModel by viewModels()
|
val mainViewModel: MainViewModel by viewModels()
|
||||||
private lateinit var groupPagerAdapter: GroupPagerAdapter
|
private lateinit var groupPagerAdapter: GroupPagerAdapter
|
||||||
private var tabMediator: TabLayoutMediator? = null
|
private var tabMediator: TabLayoutMediator? = null
|
||||||
|
@Volatile private var isFabOperationInProgress = false
|
||||||
|
|
||||||
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == RESULT_OK) {
|
if (it.resultCode == RESULT_OK) {
|
||||||
@@ -148,7 +148,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
setupGroupTab()
|
setupGroupTab()
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
importConfigViaSub()
|
importAllSubsOnStartup()
|
||||||
|
|
||||||
checkAndRequestPermission(PermissionType.POST_NOTIFICATIONS) {
|
checkAndRequestPermission(PermissionType.POST_NOTIFICATIONS) {
|
||||||
}
|
}
|
||||||
@@ -165,13 +165,15 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
mainViewModel.sortByTestResults()
|
mainViewModel.sortByTestResults()
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
|
|
||||||
val firstServer = mainViewModel.serversCache.firstOrNull()
|
val firstReachable = mainViewModel.serversCache.firstOrNull { cache ->
|
||||||
if (firstServer != null) {
|
(MmkvManager.decodeServerAffiliationInfo(cache.guid)?.testDelayMillis ?: 0L) > 0L
|
||||||
MmkvManager.setSelectServer(firstServer.guid)
|
}
|
||||||
|
if (firstReachable != null) {
|
||||||
|
MmkvManager.setSelectServer(firstReachable.guid)
|
||||||
showStatus("Подключаемся к быстрейшему серверу")
|
showStatus("Подключаемся к быстрейшему серверу")
|
||||||
startV2RayWithPermission()
|
startV2RayWithPermission()
|
||||||
} else {
|
} else {
|
||||||
showStatus("Серверы не найдены!")
|
showStatus("Нет доступных серверов!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,13 +203,31 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFabAction() {
|
private fun handleFabAction() {
|
||||||
|
if (isFabOperationInProgress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
|
val isRunning = mainViewModel.isRunning.value == true
|
||||||
|
|
||||||
applyRunningState(isLoading = true, isRunning = false)
|
applyRunningState(isLoading = true, isRunning = false)
|
||||||
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
lifecycleScope.launch {
|
||||||
V2RayServiceManager.stopVService(this)
|
try {
|
||||||
|
if (isRunning) {
|
||||||
|
Log.d(AppConfig.TAG, "FAB: stopping service")
|
||||||
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(AppConfig.TAG, "FAB: starting service")
|
||||||
startV2RayWithPermission()
|
startV2RayWithPermission()
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "FAB: error", e)
|
||||||
|
applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value == true)
|
||||||
|
} finally {
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLayoutTestClick() {
|
private fun handleLayoutTestClick() {
|
||||||
@@ -220,15 +240,23 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLiteAction() {
|
private fun handleLiteAction() {
|
||||||
|
if (isFabOperationInProgress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
V2RayServiceManager.stopVService(this)
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
|
delay(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
showStatus("Обновление профилей...")
|
showStatus("Обновление профилей...")
|
||||||
showLoading()
|
showLoading()
|
||||||
isLiteTesting = true
|
isLiteTesting = true
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val result = mainViewModel.updateConfigViaSubAll()
|
val result = mainViewModel.updateConfigViaSubAll()
|
||||||
delay(500L)
|
delay(500L)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
@@ -245,6 +273,13 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
mainViewModel.testAllRealPing()
|
mainViewModel.testAllRealPing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
delay(1500)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error in handleLiteAction", e)
|
||||||
|
} finally {
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startV2RayWithPermission() {
|
private fun startV2RayWithPermission() {
|
||||||
@@ -269,12 +304,24 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun restartV2Ray() {
|
fun restartV2Ray() {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (isFabOperationInProgress) {
|
||||||
V2RayServiceManager.stopVService(this)
|
return
|
||||||
}
|
}
|
||||||
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
delay(500)
|
try {
|
||||||
|
if (mainViewModel.isRunning.value == true) {
|
||||||
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
startV2Ray()
|
startV2Ray()
|
||||||
|
delay(1000)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error in restartV2Ray", e)
|
||||||
|
} finally {
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +372,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -333,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +514,20 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun importAllSubsOnStartup() {
|
||||||
|
showLoading()
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val result = AngConfigManager.updateConfigViaSubAll()
|
||||||
|
delay(500L)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
if (result.configCount > 0) {
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
|
showStatus(getString(R.string.title_update_config_count, result.configCount))
|
||||||
|
}
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* import config from sub
|
* import config from sub
|
||||||
*/
|
*/
|
||||||
@@ -523,25 +611,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun delInvalidConfig() {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
showLoading()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.removeInvalidServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
showStatus(getString(R.string.title_del_config_count, ret))
|
|
||||||
hideLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sortByTestResults() {
|
private fun sortByTestResults() {
|
||||||
showLoading()
|
showLoading()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -55,41 +159,56 @@ class MainRecyclerAdapter(
|
|||||||
|
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
//Name address
|
// Name address
|
||||||
holder.itemMainBinding.tvName.text = profile.remarks
|
holder.itemMainBinding.tvName.text = profile.remarks
|
||||||
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
||||||
|
|
||||||
//TestResult
|
// TestResult
|
||||||
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
|
||||||
if (guid == MmkvManager.getSelectServer()) {
|
if (guid == MmkvManager.getSelectServer()) {
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorIndicator)
|
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorIndicator)
|
||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
//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) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,6 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
//setContentView(binding.root)
|
|
||||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_sub_setting))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_sub_setting))
|
||||||
|
|
||||||
adapter = SubSettingRecyclerAdapter(viewModel, ActivityAdapterListener())
|
adapter = SubSettingRecyclerAdapter(viewModel, ActivityAdapterListener())
|
||||||
@@ -53,6 +52,14 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
|
|
||||||
|
viewModel.isUpdating.observe(this) { isUpdating ->
|
||||||
|
adapter.setUpdating(isUpdating)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.subscriptionStatuses.observe(this) { statuses ->
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -62,44 +69,105 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.action_sub_setting, menu)
|
menuInflater.inflate(R.menu.action_sub_setting, menu)
|
||||||
|
viewModel.isUpdating.observe(this) { isUpdating ->
|
||||||
|
menu.findItem(R.id.sub_update)?.isEnabled = !isUpdating
|
||||||
|
menu.findItem(R.id.add_config)?.isEnabled = !isUpdating
|
||||||
|
}
|
||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
R.id.add_config -> {
|
R.id.add_config -> {
|
||||||
startActivity(Intent(this, SubEditActivity::class.java))
|
startActivity(Intent(this, SubEditActivity::class.java))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.sub_update -> {
|
R.id.sub_update -> {
|
||||||
showLoading()
|
if (viewModel.isUpdating.value == true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading()
|
||||||
|
viewModel.isUpdating.value = true
|
||||||
|
viewModel.subscriptionStatuses.value = emptyMap()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val subscriptions = viewModel.getAll()
|
||||||
|
var totalConfigCount = 0
|
||||||
|
var successCount = 0
|
||||||
|
var failureCount = 0
|
||||||
|
var skipCount = 0
|
||||||
|
|
||||||
|
val jobs = subscriptions.map { subscription ->
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val subId = subscription.guid
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = AngConfigManager.updateConfigViaSubAll()
|
|
||||||
delay(500L)
|
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (result.successCount + result.failureCount + result.skipCount == 0) {
|
viewModel.updateSubscriptionStatus(subId, xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.LOADING)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = AngConfigManager.updateConfigViaSub(subscription)
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
when {
|
||||||
|
result.successCount > 0 -> {
|
||||||
|
viewModel.updateSubscriptionStatus(
|
||||||
|
subId,
|
||||||
|
xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.SUCCESS,
|
||||||
|
result.configCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result.skipCount > 0 -> {
|
||||||
|
viewModel.updateSubscriptionStatus(
|
||||||
|
subId,
|
||||||
|
xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.SKIPPED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.updateSubscriptionStatus(
|
||||||
|
subId,
|
||||||
|
xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.FAILED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(this@SubSettingActivity) {
|
||||||
|
totalConfigCount += result.configCount
|
||||||
|
successCount += result.successCount
|
||||||
|
failureCount += result.failureCount
|
||||||
|
skipCount += result.skipCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs.forEach { it.join() }
|
||||||
|
|
||||||
|
delay(500L)
|
||||||
|
viewModel.isUpdating.value = false
|
||||||
|
|
||||||
|
if (successCount + failureCount + skipCount == 0) {
|
||||||
toast(R.string.title_update_subscription_no_subscription)
|
toast(R.string.title_update_subscription_no_subscription)
|
||||||
} else if (result.successCount > 0 && result.failureCount + result.skipCount == 0) {
|
} else if (successCount > 0 && failureCount + skipCount == 0) {
|
||||||
toast(getString(R.string.title_update_config_count, result.configCount))
|
toast(getString(R.string.title_update_config_count, totalConfigCount))
|
||||||
} else {
|
} else {
|
||||||
toast(
|
toast(
|
||||||
getString(
|
getString(
|
||||||
R.string.title_update_subscription_result,
|
R.string.title_update_subscription_result,
|
||||||
result.configCount, result.successCount, result.failureCount, result.skipCount
|
totalConfigCount, successCount, failureCount, skipCount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
hideLoading()
|
hideLoading()
|
||||||
refreshData()
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
|
import xyz.zarazaex.olc.R
|
||||||
import xyz.zarazaex.olc.contracts.BaseAdapterListener
|
import xyz.zarazaex.olc.contracts.BaseAdapterListener
|
||||||
import xyz.zarazaex.olc.databinding.ItemRecyclerSubSettingBinding
|
import xyz.zarazaex.olc.databinding.ItemRecyclerSubSettingBinding
|
||||||
|
import xyz.zarazaex.olc.dto.SubscriptionUpdateStatus
|
||||||
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.util.Utils
|
import xyz.zarazaex.olc.util.Utils
|
||||||
@@ -20,6 +22,15 @@ class SubSettingRecyclerAdapter(
|
|||||||
private val adapterListener: BaseAdapterListener?
|
private val adapterListener: BaseAdapterListener?
|
||||||
) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
||||||
|
|
||||||
|
private var isUpdating = false
|
||||||
|
|
||||||
|
fun setUpdating(updating: Boolean) {
|
||||||
|
if (isUpdating != updating) {
|
||||||
|
isUpdating = updating
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount() = viewModel.getAll().size
|
override fun getItemCount() = viewModel.getAll().size
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||||
@@ -32,16 +43,60 @@ class SubSettingRecyclerAdapter(
|
|||||||
holder.itemSubSettingBinding.tvLastUpdated.text = Utils.formatTimestamp(subItem.lastUpdated)
|
holder.itemSubSettingBinding.tvLastUpdated.text = Utils.formatTimestamp(subItem.lastUpdated)
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
|
val subStatus = viewModel.getSubscriptionStatus(subId)
|
||||||
|
when (subStatus?.status) {
|
||||||
|
SubscriptionUpdateStatus.LOADING -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.text = holder.itemView.context.getString(R.string.title_updating)
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.GRAY)
|
||||||
|
}
|
||||||
|
SubscriptionUpdateStatus.SUCCESS -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.GONE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.text = "✓ ${subStatus.configCount}"
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.parseColor("#4CAF50"))
|
||||||
|
}
|
||||||
|
SubscriptionUpdateStatus.FAILED -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.GONE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.text = "✗"
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.parseColor("#F44336"))
|
||||||
|
}
|
||||||
|
SubscriptionUpdateStatus.SKIPPED -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.GONE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.text = "—"
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.GRAY)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.GONE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isEnabled = !isUpdating
|
||||||
|
|
||||||
|
holder.itemSubSettingBinding.layoutEdit.isClickable = isEnabled
|
||||||
|
holder.itemSubSettingBinding.layoutEdit.alpha = if (isEnabled) 1.0f else 0.5f
|
||||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||||
|
if (isEnabled) {
|
||||||
adapterListener?.onEdit(subId, position)
|
adapterListener?.onEdit(subId, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
|
||||||
adapterListener?.onRemove(subId, position)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.isClickable = isEnabled
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.alpha = if (isEnabled) 1.0f else 0.5f
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||||
|
if (isEnabled) {
|
||||||
|
adapterListener?.onRemove(subId, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemSubSettingBinding.chkEnable.isEnabled = isEnabled
|
||||||
|
holder.itemSubSettingBinding.chkEnable.alpha = if (isEnabled) 1.0f else 0.5f
|
||||||
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||||
if (!it.isPressed) return@setOnCheckedChangeListener
|
if (!it.isPressed || !isEnabled) return@setOnCheckedChangeListener
|
||||||
subItem.enabled = isChecked
|
subItem.enabled = isChecked
|
||||||
viewModel.update(subId, subItem)
|
viewModel.update(subId, subItem)
|
||||||
}
|
}
|
||||||
@@ -56,11 +111,15 @@ class SubSettingRecyclerAdapter(
|
|||||||
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
|
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
|
||||||
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
||||||
holder.itemSubSettingBinding.layoutLastUpdated.visibility = View.VISIBLE
|
holder.itemSubSettingBinding.layoutLastUpdated.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.layoutShare.isClickable = isEnabled
|
||||||
|
holder.itemSubSettingBinding.layoutShare.alpha = if (isEnabled) 1.0f else 0.5f
|
||||||
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||||
|
if (isEnabled) {
|
||||||
adapterListener?.onShare(subItem.url)
|
adapterListener?.onShare(subItem.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||||
return MainViewHolder(
|
return MainViewHolder(
|
||||||
|
|||||||
@@ -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,21 +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
|
||||||
allServers.add(ServerWithDelay(guid, if (delay <= 0L) 999999 else delay))
|
val isFav = MmkvManager.decodeServerConfig(guid)?.isFavorite ?: false
|
||||||
|
val sortKey = when {
|
||||||
|
delay > 0L -> delay
|
||||||
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
|
else -> Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
allServers.add(ServerWithDelay(guid, sortKey, isFav))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allServers.sortBy { it.delay }
|
allServers.sortWith(compareBy({ !it.isFav }, { it.delay }))
|
||||||
allServers.map { it.guid }.toMutableList()
|
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()
|
||||||
@@ -266,6 +275,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
updateListAction.value = -1
|
updateListAction.value = -1
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
if (serversCache.isEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) { reloadServerList() }
|
||||||
|
}
|
||||||
if (serversCache.isEmpty()) {
|
if (serversCache.isEmpty()) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -442,23 +454,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes invalid servers.
|
|
||||||
* @return The number of removed servers.
|
|
||||||
*/
|
|
||||||
fun removeInvalidServer(): Int {
|
|
||||||
var count = 0
|
|
||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
|
||||||
count += MmkvManager.removeInvalidServer("")
|
|
||||||
} else {
|
|
||||||
val serversCopy = serversCache.toList()
|
|
||||||
for (item in serversCopy) {
|
|
||||||
count += MmkvManager.removeInvalidServer(item.guid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts servers by their test results.
|
* Sorts servers by their test results.
|
||||||
*/
|
*/
|
||||||
@@ -475,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) {
|
||||||
@@ -496,11 +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
|
||||||
allServerDelays.add(ServerDelay(guid, if (delay <= 0L) 999999 else delay, sub.guid))
|
val isFav = MmkvManager.decodeServerConfig(guid)?.isFavorite ?: false
|
||||||
|
val sortKey = when {
|
||||||
|
delay > 0L -> delay
|
||||||
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
|
else -> Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
allServerDelays.add(ServerDelay(guid, sortKey, sub.guid, isFav))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allServerDelays.sortBy { it.testDelayMillis }
|
allServerDelays.sortWith(compareBy({ !it.isFav }, { it.testDelayMillis }))
|
||||||
|
|
||||||
val serversBySubId = allServerDelays.groupBy { it.subId }
|
val serversBySubId = allServerDelays.groupBy { it.subId }
|
||||||
serversBySubId.forEach { (subId, servers) ->
|
serversBySubId.forEach { (subId, servers) ->
|
||||||
@@ -514,16 +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
|
||||||
serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
|
val isFav = MmkvManager.decodeServerConfig(key)?.isFavorite ?: false
|
||||||
|
val sortKey = when {
|
||||||
|
delay > 0L -> delay
|
||||||
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
|
else -> Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
serverDelays.sortBy { it.testDelayMillis }
|
serverDelays.add(ServerDelay(key, sortKey, isFav))
|
||||||
|
}
|
||||||
|
serverDelays.sortWith(compareBy({ !it.isFav }, { it.testDelayMillis }))
|
||||||
|
|
||||||
val sortedServerList = serverDelays.map { it.guid }.toMutableList()
|
val sortedServerList = serverDelays.map { it.guid }.toMutableList()
|
||||||
|
|
||||||
@@ -577,10 +584,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
fun onTestsFinished() {
|
fun onTestsFinished() {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_REMOVE_INVALID_AFTER_TEST)) {
|
|
||||||
removeInvalidServer()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_SORT_AFTER_TEST, true)) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_SORT_AFTER_TEST, true)) {
|
||||||
sortByTestResults()
|
sortByTestResults()
|
||||||
}
|
}
|
||||||
@@ -626,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 =
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package xyz.zarazaex.olc.viewmodel
|
package xyz.zarazaex.olc.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import xyz.zarazaex.olc.dto.SubscriptionCache
|
import xyz.zarazaex.olc.dto.SubscriptionCache
|
||||||
import xyz.zarazaex.olc.dto.SubscriptionItem
|
import xyz.zarazaex.olc.dto.SubscriptionItem
|
||||||
|
import xyz.zarazaex.olc.dto.SubscriptionStatus
|
||||||
|
import xyz.zarazaex.olc.dto.SubscriptionUpdateStatus
|
||||||
import xyz.zarazaex.olc.handler.MmkvManager
|
import xyz.zarazaex.olc.handler.MmkvManager
|
||||||
import xyz.zarazaex.olc.handler.SettingsChangeManager
|
import xyz.zarazaex.olc.handler.SettingsChangeManager
|
||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
@@ -11,11 +14,25 @@ class SubscriptionsViewModel : ViewModel() {
|
|||||||
private val subscriptions: MutableList<SubscriptionCache> =
|
private val subscriptions: MutableList<SubscriptionCache> =
|
||||||
MmkvManager.decodeSubscriptions().toMutableList()
|
MmkvManager.decodeSubscriptions().toMutableList()
|
||||||
|
|
||||||
|
val isUpdating = MutableLiveData<Boolean>(false)
|
||||||
|
val subscriptionStatuses = MutableLiveData<Map<String, SubscriptionStatus>>(emptyMap())
|
||||||
|
|
||||||
fun getAll(): List<SubscriptionCache> = subscriptions.toList()
|
fun getAll(): List<SubscriptionCache> = subscriptions.toList()
|
||||||
|
|
||||||
fun reload() {
|
fun reload() {
|
||||||
subscriptions.clear()
|
subscriptions.clear()
|
||||||
subscriptions.addAll(MmkvManager.decodeSubscriptions())
|
subscriptions.addAll(MmkvManager.decodeSubscriptions())
|
||||||
|
subscriptionStatuses.value = emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSubscriptionStatus(guid: String, status: SubscriptionUpdateStatus, configCount: Int = 0) {
|
||||||
|
val currentStatuses = subscriptionStatuses.value?.toMutableMap() ?: mutableMapOf()
|
||||||
|
currentStatuses[guid] = SubscriptionStatus(guid, status, configCount)
|
||||||
|
subscriptionStatuses.postValue(currentStatuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSubscriptionStatus(guid: String): SubscriptionStatus? {
|
||||||
|
return subscriptionStatuses.value?.get(guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(subId: String): Boolean {
|
fun remove(subId: String): Boolean {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -168,9 +168,26 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_last_updated"
|
android:id="@+id/tv_last_updated"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
style="?android:attr/progressBarStyleSmall"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginStart="@dimen/padding_spacing_dp8"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_update_status"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
|
android:layout_marginStart="@dimen/padding_spacing_dp8"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
</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"
|
||||||
|
|||||||
@@ -156,8 +156,6 @@
|
|||||||
<string name="title_pref_is_booted">Auto connect at startup</string>
|
<string name="title_pref_is_booted">Auto connect at startup</string>
|
||||||
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -417,4 +415,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">جارٍ التحديث…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -154,8 +154,6 @@
|
|||||||
<string name="title_pref_is_booted">Auto connect at startup</string>
|
<string name="title_pref_is_booted">Auto connect at startup</string>
|
||||||
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -423,4 +421,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">আপডেট হচ্ছে…</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -155,8 +155,6 @@
|
|||||||
<string name="title_pref_is_booted">منپیز خوتکار مجال ره ونی</string>
|
<string name="title_pref_is_booted">منپیز خوتکار مجال ره ونی</string>
|
||||||
<string name="summary_pref_is_booted">مجال ره وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ.</string>
|
<string name="summary_pref_is_booted">مجال ره وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ.</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">پاک کردن خوتکار کانفیگ نا موئتبر بئڌ آزمایش</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">نتیجه یل آزمایش گاشڌ دییق نبۊن؛ کانفیگ پاک وابیڌه ن نتری وورگنی.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">ترتیب خوتکار بئڌ آزمایش</string>
|
<string name="title_pref_auto_sort_after_test">ترتیب خوتکار بئڌ آزمایش</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">نتیجه یل آزمایش گاشڌ دییق نبۊن؛</string>
|
<string name="summary_pref_auto_sort_after_test">نتیجه یل آزمایش گاشڌ دییق نبۊن؛</string>
|
||||||
|
|
||||||
@@ -433,4 +431,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">در حال بهروزرسانی…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -152,8 +152,6 @@
|
|||||||
<string name="title_pref_is_booted">اتصال خودکار هنگام راه اندازی</string>
|
<string name="title_pref_is_booted">اتصال خودکار هنگام راه اندازی</string>
|
||||||
<string name="summary_pref_is_booted">هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد.</string>
|
<string name="summary_pref_is_booted">هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد.</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -432,4 +430,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">در حال بهروزرسانی…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="color_fab_active">#88AC8A</color>
|
<color name="color_fab_active">#90CAF9</color>
|
||||||
<color name="color_fab_inactive">#646464</color>
|
<color name="color_fab_inactive">#646464</color>
|
||||||
<color name="divider_color_light">#424242</color>
|
<color name="divider_color_light">#424242</color>
|
||||||
|
|
||||||
<!-- Primary colors - main tone: gray -->
|
|
||||||
<color name="md_theme_primary">#C0C0C0</color>
|
<color name="md_theme_primary">#C0C0C0</color>
|
||||||
<color name="md_theme_onPrimary">#303030</color>
|
<color name="md_theme_onPrimary">#303030</color>
|
||||||
<color name="md_theme_primaryContainer">#474747</color>
|
<color name="md_theme_primaryContainer">#474747</color>
|
||||||
<color name="md_theme_onPrimaryContainer">#E0E0E0</color>
|
<color name="md_theme_onPrimaryContainer">#E0E0E0</color>
|
||||||
|
|
||||||
<!-- Secondary colors - accent color: green -->
|
<color name="md_theme_secondary">#90CAF9</color>
|
||||||
<color name="md_theme_secondary">#88AC8A</color>
|
|
||||||
<color name="md_theme_onSecondary">#FFFFFF</color>
|
<color name="md_theme_onSecondary">#FFFFFF</color>
|
||||||
<color name="md_theme_secondaryContainer">#6F3800</color>
|
<color name="md_theme_secondaryContainer">#6F3800</color>
|
||||||
<color name="md_theme_onSecondaryContainer">#FFE8D6</color>
|
<color name="md_theme_onSecondaryContainer">#FFE8D6</color>
|
||||||
|
|
||||||
<!-- Tertiary colors - tertiary color: green -->
|
<color name="md_theme_tertiary">#64B5F6</color>
|
||||||
<color name="md_theme_tertiary">#83D6B5</color>
|
|
||||||
<color name="md_theme_onTertiary">#00382E</color>
|
<color name="md_theme_onTertiary">#00382E</color>
|
||||||
<color name="md_theme_tertiaryContainer">#005143</color>
|
<color name="md_theme_tertiaryContainer">#005143</color>
|
||||||
<color name="md_theme_onTertiaryContainer">#A0F2D0</color>
|
<color name="md_theme_onTertiaryContainer">#BBDEFB</color>
|
||||||
|
|
||||||
<!-- Error colors -->
|
<!-- Error colors -->
|
||||||
<color name="md_theme_error">#FFB4AB</color>
|
<color name="md_theme_error">#FFB4AB</color>
|
||||||
|
|||||||
@@ -156,8 +156,6 @@
|
|||||||
<string name="title_pref_is_booted">Автоподключение при запуске</string>
|
<string name="title_pref_is_booted">Автоподключение при запуске</string>
|
||||||
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
|
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Автоудаление нерабочих профилей</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Автоматическое удаление нерабочих профилей после проверки (результаты проверки могут быть неточными; восстановить удалённые профили невозможно)</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Автосортировка профилей</string>
|
<string name="title_pref_auto_sort_after_test">Автосортировка профилей</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
|
<string name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
|
||||||
|
|
||||||
@@ -298,6 +296,7 @@
|
|||||||
<string name="title_import_config_count">Импортировано профилей: %d</string>
|
<string name="title_import_config_count">Импортировано профилей: %d</string>
|
||||||
<string name="title_export_config_count">Экспортировано профилей: %d</string>
|
<string name="title_export_config_count">Экспортировано профилей: %d</string>
|
||||||
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
||||||
|
<string name="title_updating">Обновление…</string>
|
||||||
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
|
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
|
||||||
<string name="title_update_subscription_no_subscription">Нет подписок</string>
|
<string name="title_update_subscription_no_subscription">Нет подписок</string>
|
||||||
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
|
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
|
||||||
|
|||||||
@@ -153,8 +153,6 @@
|
|||||||
<string name="title_pref_is_booted">Auto connect at startup</string>
|
<string name="title_pref_is_booted">Auto connect at startup</string>
|
||||||
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -419,4 +417,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">Đang cập nhật…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -152,8 +152,6 @@
|
|||||||
<string name="title_pref_is_booted">开机时自动连接</string>
|
<string name="title_pref_is_booted">开机时自动连接</string>
|
||||||
<string name="summary_pref_is_booted">开机时自动连接选择的服务器,可能会不成功</string>
|
<string name="summary_pref_is_booted">开机时自动连接选择的服务器,可能会不成功</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">测试后自动删除无效配置</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">测试结果可能不准确;已删除的配置无法恢复。</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">测试后自动排序</string>
|
<string name="title_pref_auto_sort_after_test">测试后自动排序</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">测试结果可能不准确;</string>
|
<string name="summary_pref_auto_sort_after_test">测试结果可能不准确;</string>
|
||||||
|
|
||||||
@@ -425,4 +423,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">更新中…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -153,8 +153,6 @@
|
|||||||
<string name="title_pref_is_booted">開機時自動連線</string>
|
<string name="title_pref_is_booted">開機時自動連線</string>
|
||||||
<string name="summary_pref_is_booted">開機時自動連線選擇的伺服器,可能會不成功</string>
|
<string name="summary_pref_is_booted">開機時自動連線選擇的伺服器,可能會不成功</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">測試後自動刪除無效配置</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">測試結果可能不準確;已刪除的配置無法復原。 </string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">測試後自動排序</string>
|
<string name="title_pref_auto_sort_after_test">測試後自動排序</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">測試結果可能不準確;</string>
|
<string name="summary_pref_auto_sort_after_test">測試結果可能不準確;</string>
|
||||||
|
|
||||||
@@ -425,4 +423,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">更新中…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPing">#009966</color>
|
<color name="colorPing">#1565C0</color>
|
||||||
<color name="colorPingRed">#FF0099</color>
|
<color name="colorPingRed">#FF0099</color>
|
||||||
<color name="colorConfigType">#88AC8A</color>
|
<color name="colorConfigType">#1976D2</color>
|
||||||
<color name="colorWhite">#FFFFFF</color>
|
<color name="colorWhite">#FFFFFF</color>
|
||||||
<color name="color_fab_active">#88AC8A</color>
|
<color name="color_fab_active">#1976D2</color>
|
||||||
<color name="color_fab_inactive">#9C9C9C</color>
|
<color name="color_fab_inactive">#9C9C9C</color>
|
||||||
<color name="divider_color_light">#E0E0E0</color>
|
<color name="divider_color_light">#E0E0E0</color>
|
||||||
<color name="colorIndicator">@color/md_theme_primary</color>
|
<color name="colorIndicator">@color/md_theme_primary</color>
|
||||||
|
|
||||||
<!-- Primary colors - main tone: black -->
|
|
||||||
<color name="md_theme_primary">#000000</color>
|
<color name="md_theme_primary">#000000</color>
|
||||||
<color name="md_theme_onPrimary">#FFFFFF</color>
|
<color name="md_theme_onPrimary">#FFFFFF</color>
|
||||||
<color name="md_theme_primaryContainer">#E0E0E0</color>
|
<color name="md_theme_primaryContainer">#E0E0E0</color>
|
||||||
<color name="md_theme_onPrimaryContainer">#000000</color>
|
<color name="md_theme_onPrimaryContainer">#000000</color>
|
||||||
|
|
||||||
<!-- Secondary colors - accent color: green -->
|
<color name="md_theme_secondary">#1976D2</color>
|
||||||
<color name="md_theme_secondary">#88AC8A</color>
|
|
||||||
<color name="md_theme_onSecondary">#FFFFFF</color>
|
<color name="md_theme_onSecondary">#FFFFFF</color>
|
||||||
<color name="md_theme_secondaryContainer">#FFE8D6</color>
|
<color name="md_theme_secondaryContainer">#FFE8D6</color>
|
||||||
<color name="md_theme_onSecondaryContainer">#2B1700</color>
|
<color name="md_theme_onSecondaryContainer">#2B1700</color>
|
||||||
|
|
||||||
<!-- Tertiary colors - accent color: green -->
|
<color name="md_theme_tertiary">#1565C0</color>
|
||||||
<color name="md_theme_tertiary">#009966</color>
|
|
||||||
<color name="md_theme_onTertiary">#FFFFFF</color>
|
<color name="md_theme_onTertiary">#FFFFFF</color>
|
||||||
<color name="md_theme_tertiaryContainer">#A0F2D0</color>
|
<color name="md_theme_tertiaryContainer">#BBDEFB</color>
|
||||||
<color name="md_theme_onTertiaryContainer">#00201A</color>
|
<color name="md_theme_onTertiaryContainer">#00201A</color>
|
||||||
|
|
||||||
<!-- Error colors -->
|
<!-- Error colors -->
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<string name="navigation_drawer_open">Open navigation drawer</string>
|
<string name="navigation_drawer_open">Open navigation drawer</string>
|
||||||
<string name="navigation_drawer_close">Close navigation drawer</string>
|
<string name="navigation_drawer_close">Close navigation drawer</string>
|
||||||
<string name="migration_success">Data migration success!</string>
|
<string name="migration_success">Data migration success!</string>
|
||||||
<string name="drawer_forked_text">forked from <a href="https://github.com/2dust/v2rayng">V2RayNG</a></string>
|
<string name="drawer_forked_text" translatable="false">forked from <a href="https://github.com/2dust/v2rayng">V2RayNG</a></string>
|
||||||
<string name="drawer_developed_text">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string>
|
<string name="drawer_developed_text" translatable="false">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string>
|
||||||
<string name="action_stop_service">Stop service</string>
|
<string name="action_stop_service">Stop service</string>
|
||||||
<string name="migration_fail">Data migration failed!</string>
|
<string name="migration_fail">Data migration failed!</string>
|
||||||
<string name="pull_down_to_refresh">Please pull down to refresh!</string>
|
<string name="pull_down_to_refresh">Please pull down to refresh!</string>
|
||||||
@@ -157,8 +157,6 @@
|
|||||||
<string name="title_pref_is_booted">Auto connect at startup</string>
|
<string name="title_pref_is_booted">Auto connect at startup</string>
|
||||||
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -249,7 +247,7 @@
|
|||||||
<string name="summary_pref_tg_group">Join Telegram Group</string>
|
<string name="summary_pref_tg_group">Join Telegram Group</string>
|
||||||
<string name="toast_tg_app_not_found">Telegram app not found</string>
|
<string name="toast_tg_app_not_found">Telegram app not found</string>
|
||||||
<string name="title_privacy_policy">Privacy policy</string>
|
<string name="title_privacy_policy">Privacy policy</string>
|
||||||
<string name="title_qr_code">QR code</string>
|
<string name="title_qr_code" translatable="false">QR code</string>
|
||||||
<string name="title_about">About</string>
|
<string name="title_about">About</string>
|
||||||
<string name="title_source_code">Source code</string>
|
<string name="title_source_code">Source code</string>
|
||||||
<string name="title_oss_license">Open Source licenses</string>
|
<string name="title_oss_license">Open Source licenses</string>
|
||||||
@@ -304,6 +302,7 @@
|
|||||||
<string name="title_import_config_count">Import %d configs</string>
|
<string name="title_import_config_count">Import %d configs</string>
|
||||||
<string name="title_export_config_count">Export %d configs</string>
|
<string name="title_export_config_count">Export %d configs</string>
|
||||||
<string name="title_update_config_count">Update %d configs</string>
|
<string name="title_update_config_count">Update %d configs</string>
|
||||||
|
<string name="title_updating">Updating…</string>
|
||||||
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||||
<string name="title_update_subscription_no_subscription">No subscriptions</string>
|
<string name="title_update_subscription_no_subscription">No subscriptions</string>
|
||||||
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||||
|
|||||||
@@ -8,13 +8,11 @@
|
|||||||
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
|
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
|
||||||
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
|
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
|
||||||
|
|
||||||
<!-- Secondary colors - accent color: orange -->
|
|
||||||
<item name="colorSecondary">@color/md_theme_secondary</item>
|
<item name="colorSecondary">@color/md_theme_secondary</item>
|
||||||
<item name="colorOnSecondary">@color/md_theme_onSecondary</item>
|
<item name="colorOnSecondary">@color/md_theme_onSecondary</item>
|
||||||
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
|
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
|
||||||
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
|
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
|
||||||
|
|
||||||
<!-- Tertiary colors - tertiary color: green -->
|
|
||||||
<item name="colorTertiary">@color/md_theme_tertiary</item>
|
<item name="colorTertiary">@color/md_theme_tertiary</item>
|
||||||
<item name="colorOnTertiary">@color/md_theme_onTertiary</item>
|
<item name="colorOnTertiary">@color/md_theme_onTertiary</item>
|
||||||
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
|
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
|
||||||
|
|||||||
@@ -266,12 +266,6 @@
|
|||||||
android:summary="@string/summary_pref_is_booted"
|
android:summary="@string/summary_pref_is_booted"
|
||||||
android:title="@string/title_pref_is_booted" />
|
android:title="@string/title_pref_is_booted" />
|
||||||
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:key="pref_auto_remove_invalid_after_test"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:summary="@string/summary_pref_auto_remove_invalid_after_test"
|
|
||||||
android:title="@string/title_pref_auto_remove_invalid_after_test" />
|
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:key="pref_auto_sort_after_test"
|
android:key="pref_auto_sort_after_test"
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
|
|||||||
@@ -2,12 +2,4 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.android.library) apply false
|
alias(libs.plugins.android.library) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
|
||||||
dependencies {
|
|
||||||
classpath(libs.gradle.license.plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,20 +15,7 @@ org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
|
|||||||
# Android operating system, and which are packaged with your app's APK
|
# Android operating system, and which are packaged with your app's APK
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
|
||||||
# thereby reducing the size of the R class for that library
|
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
kotlin.incremental=true
|
kotlin.incremental=true
|
||||||
android.defaults.buildfeatures.resvalues=true
|
android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false
|
||||||
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
|
||||||
android.enableAppCompileTimeRClass=false
|
|
||||||
android.usesSdkInManifest.disallowed=false
|
|
||||||
android.uniquePackageNames=false
|
|
||||||
android.dependency.useConstraints=true
|
|
||||||
android.r8.strictFullModeForKeepRules=false
|
|
||||||
android.r8.optimizedResourceShrinking=false
|
|
||||||
android.builtInKotlin=false
|
|
||||||
android.newDsl=false
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
agp = "9.1.0"
|
agp = "9.1.0"
|
||||||
desugarJdkLibs = "2.1.5"
|
desugarJdkLibs = "2.1.5"
|
||||||
gradleLicensePlugin = "0.9.8"
|
gradleLicensePlugin = "0.9.8"
|
||||||
kotlin = "2.3.10"
|
kotlin = "2.1.0"
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.3.0"
|
||||||
|
|||||||
@@ -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