Compare commits
24 Commits
v0.8.88-pr
...
v0.8.58
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80f8aa22ee | ||
|
|
97714e8b25 | ||
|
|
50bf4170d9 | ||
|
|
79efa67df3 | ||
|
|
ac397393a0 | ||
|
|
b685165230 | ||
|
|
402221aaa2 | ||
|
|
f6d9ed11d9 | ||
|
|
c38a671d57 | ||
|
|
75af47aead | ||
|
|
8dafe3b0ec | ||
|
|
813198a21d | ||
|
|
68dd262fef | ||
|
|
5ef020db73 | ||
|
|
e3c9035903 | ||
|
|
7fc54c5295 | ||
|
|
00a78b5fb4 | ||
|
|
8cdaf30de0 | ||
|
|
f39b9cf933 | ||
|
|
9df1ff46c2 | ||
|
|
fcbbbdc698 | ||
|
|
3ba8355772 | ||
|
|
f6b97f82ae | ||
|
|
13ac20f273 |
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: 问题反馈 / Bug report
|
||||
title: "[BUG] "
|
||||
description: 反馈你遇到的问题 / Report the issue you are experiencing
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
1. 请务必给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请务必按照模板规范详细描述问题,否则issue将会被直接关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述 / Describe the bug
|
||||
description: 详细清晰地描述你遇到的问题,并配合截图 / Describe the problem you encountered in detail and clearly, and provide screenshots
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 软件版本 / Version
|
||||
description: 请提供FlClash的具体版本 / Please provide the specific version of FlClash.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 复现步骤 / To Reproduce
|
||||
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 操作系统 / OS
|
||||
options:
|
||||
- Android
|
||||
- Windows
|
||||
- MacOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: 操作系统版本 / OS Version
|
||||
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug / Please provide a complete or relevant Debug log (please adjust it to debug in the left menu of software-> Settings-> Log Level)
|
||||
validations:
|
||||
required: true
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +0,0 @@
|
||||
contact_links:
|
||||
- name: 讨论交流 / Communication
|
||||
url: https://t.me/+G-veVtwBOl4wODc1
|
||||
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group
|
||||
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: 功能请求 / Feature request
|
||||
title: "[Feature] "
|
||||
description: 提出你的功能请求 / Propose your feature request
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
1. 请务必给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请务必按照模板规范详细描述问题,否则issue将会被直接关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 功能描述 / Feature description
|
||||
description: 详细清晰地描述你的功能请求 / A clear and concise description of what the feature is
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 使用场景 / Use case
|
||||
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: os-labels
|
||||
attributes:
|
||||
label: 适用系统 / Target OS
|
||||
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
|
||||
options:
|
||||
- label: Android
|
||||
- label: Windows
|
||||
- label: MacOS
|
||||
- label: Linux
|
||||
validations:
|
||||
required: true
|
||||
58
.github/release_template.md
vendored
58
.github/release_template.md
vendored
@@ -1,58 +0,0 @@
|
||||
<div align=center>
|
||||
|
||||
[](https://img.shields.io/github/downloads/chen08209/FlClash/vVERSION/)
|
||||
|
||||
</div>
|
||||
|
||||
**Download based on your OS:**
|
||||
|
||||
<div align=left>
|
||||
<table>
|
||||
<thead align=left>
|
||||
<tr>
|
||||
<th>OS</th>
|
||||
<th>Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody align=left>
|
||||
<tr>
|
||||
<td>Android</td>
|
||||
<td>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-android-arm64-v8a.apk"><img src="https://img.shields.io/badge/APK-ARMv8-168039.svg?logo=android"></a><br>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-android-armeabi-v7a.apk"><img src="https://img.shields.io/badge/APK-ARMv7-45bf55.svg?logo=android"></a><br>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-android-x86_64.apk"><img src="https://img.shields.io/badge/APK-x64-96ed89.svg?logo=android"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows</td>
|
||||
<td>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-windows-amd64-setup.exe"><img src="https://img.shields.io/badge/Setup-x64-2d7d9a.svg?logo=windows"></a><br>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-windows-amd64.zip"><img src="https://img.shields.io/badge/Portable-x64-67b7d1.svg?logo=windows"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>macOS</td>
|
||||
<td>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-arm64.dmg"><img src="https://img.shields.io/badge/DMG-Apple%20Silicon-%23000000.svg?logo=apple"></a><br>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-amd64.dmg"><img src="https://img.shields.io/badge/DMG-Intel%20X64-%2300A9E0.svg?logo=apple"></a><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux</td>
|
||||
<td>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-linux-amd64.AppImage"><img src="https://img.shields.io/badge/AppImage-x64-f84e29.svg?logo=linux"> </a><br>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-linux-amd64.deb"><img src="https://img.shields.io/badge/DebPackage-x64-FF9966.svg?logo=debian"> </a><br>
|
||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-linux-amd64.deb"><img src="https://img.shields.io/badge/RpmPackage-x64-F1B42F.svg?logo=redhat"> </a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
**List of all changes:** [ChangeLog](https://github.com/chen08209/FlClash/blob/main/CHANGELOG.md)
|
||||
|
||||
</div>
|
||||
259
.github/workflows/build.yaml
vendored
259
.github/workflows/build.yaml
vendored
@@ -1,259 +0,0 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
env:
|
||||
IS_STABLE: ${{ !contains(github.ref, '-') }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: android
|
||||
os: ubuntu-latest
|
||||
- platform: windows
|
||||
os: windows-latest
|
||||
arch: amd64
|
||||
- platform: linux
|
||||
os: ubuntu-22.04
|
||||
arch: amd64
|
||||
- platform: macos
|
||||
os: macos-13
|
||||
arch: amd64
|
||||
- platform: macos
|
||||
os: macos-latest
|
||||
arch: arm64
|
||||
- platform: windows
|
||||
os: windows-11-arm
|
||||
arch: arm64
|
||||
- platform: linux
|
||||
os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Setup rust
|
||||
if: startsWith(matrix.os, 'windows-11-arm')
|
||||
run: |
|
||||
Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile rustup-init.exe
|
||||
.\rustup-init.exe -y --default-toolchain stable
|
||||
$cargoPath = "$env:USERPROFILE\.cargo\bin"
|
||||
Add-Content $env:GITHUB_PATH $cargoPath
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Android Signing
|
||||
if: startsWith(matrix.platform,'android')
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
|
||||
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
|
||||
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
|
||||
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24.0'
|
||||
cache-dependency-path: |
|
||||
core/go.sum
|
||||
|
||||
- name: Setup Flutter Master
|
||||
if: startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'master'
|
||||
cache: true
|
||||
- name: Setup Flutter
|
||||
if: ${{ !(startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) }}
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
# flutter-version: 3.29.3
|
||||
|
||||
- name: Get Flutter Dependency
|
||||
run: flutter pub get
|
||||
|
||||
- name: Setup
|
||||
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }}
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
|
||||
path: ./dist
|
||||
overwrite: true
|
||||
|
||||
changelog:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: refs/heads/main
|
||||
- name: Generate
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
|
||||
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
|
||||
currentTag=""
|
||||
for ((i = 0; i <= ${#tags[@]}; i++)); do
|
||||
if (( i < ${#tags[@]} )); then
|
||||
tag=${tags[$i]}
|
||||
else
|
||||
tag=""
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
echo "## $currentTag" >> NEW_CHANGELOG.md
|
||||
echo "" >> NEW_CHANGELOG.md
|
||||
if [ -n "$tag" ]; then
|
||||
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
|
||||
else
|
||||
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
|
||||
fi
|
||||
echo "" >> NEW_CHANGELOG.md
|
||||
fi
|
||||
currentTag=$tag
|
||||
done
|
||||
cat CHANGELOG.md >> NEW_CHANGELOG.md
|
||||
cat NEW_CHANGELOG.md > CHANGELOG.md
|
||||
|
||||
- name: Commit
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
git add CHANGELOG.md
|
||||
if ! git diff --cached --quiet; then
|
||||
echo "Commit pushing"
|
||||
git config --local user.email "chen08209@gmail.com"
|
||||
git config --local user.name "chen08209"
|
||||
git commit -m "Update changelog"
|
||||
git push
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Push succeeded"
|
||||
else
|
||||
echo "Push failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload:
|
||||
permissions: write-all
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
telegram-bot-api:
|
||||
image: aiogram/telegram-bot-api:latest
|
||||
env:
|
||||
TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }}
|
||||
TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
|
||||
ports:
|
||||
- 8081:8081
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./dist/
|
||||
pattern: artifact-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate release.md
|
||||
run: |
|
||||
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
|
||||
preTag=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
|
||||
currentTag=""
|
||||
for ((i = 0; i <= ${#tags[@]}; i++)); do
|
||||
if (( i < ${#tags[@]} )); then
|
||||
tag=${tags[$i]}
|
||||
else
|
||||
tag=""
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
if [ -n "$tag" ]; then
|
||||
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md
|
||||
else
|
||||
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md
|
||||
fi
|
||||
echo "" >> release.md
|
||||
fi
|
||||
currentTag=$tag
|
||||
done
|
||||
|
||||
- name: Push to telegram
|
||||
env:
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
python release_telegram.py
|
||||
|
||||
- name: Patch release.md
|
||||
run: |
|
||||
version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
|
||||
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
|
||||
|
||||
- name: Generate sha256
|
||||
if: env.IS_STABLE == 'true'
|
||||
run: |
|
||||
cd ./dist
|
||||
for file in $(find . -type f -not -name "*.sha256"); do
|
||||
sha256sum "$file" > "${file}.sha256"
|
||||
done
|
||||
|
||||
- name: Release
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./dist/*
|
||||
body_path: './release.md'
|
||||
|
||||
- name: Create Fdroid Source Dir
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
mkdir -p ./tmp
|
||||
cp ./dist/*android-arm64-v8a* ./tmp/ || true
|
||||
echo "Files copied successfully"
|
||||
|
||||
- name: Push to fdroid repo
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
uses: cpina/github-action-push-to-another-repository@v1.7.2
|
||||
env:
|
||||
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||
with:
|
||||
source-directory: ./tmp/
|
||||
destination-github-username: chen08209
|
||||
destination-repository-name: FlClash-fdroid-repo
|
||||
user-name: 'github-actions[bot]'
|
||||
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||
target-branch: main
|
||||
commit-message: Update from ${{ github.ref_name }}
|
||||
target-directory: /tmp/
|
||||
|
||||
164
.github/workflows/build.yml
vendored
Normal file
164
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: android
|
||||
os: ubuntu-latest
|
||||
- platform: windows
|
||||
os: windows-latest
|
||||
arch: amd64
|
||||
- platform: linux
|
||||
os: ubuntu-latest
|
||||
arch: amd64
|
||||
- platform: macos
|
||||
os: macos-13
|
||||
arch: amd64
|
||||
- platform: macos
|
||||
os: macos-latest
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Setup Mingw64
|
||||
if: startsWith(matrix.platform,'windows')
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: mingw64
|
||||
install: mingw-w64-x86_64-gcc
|
||||
update: true
|
||||
|
||||
|
||||
- name: Set Mingw64 Env
|
||||
if: startsWith(matrix.platform,'windows')
|
||||
run: |
|
||||
echo "${{ runner.temp }}\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
- name: Check Matrix
|
||||
run: |
|
||||
echo "Running on ${{ matrix.os }}"
|
||||
echo "Arch: ${{ runner.arch }}"
|
||||
gcc --version
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup JAVA
|
||||
if: startsWith(matrix.platform,'android')
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Setup NDK
|
||||
if: startsWith(matrix.platform,'android')
|
||||
uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r26b
|
||||
add-to-path: true
|
||||
link-to-sdk: true
|
||||
|
||||
- name: Setup Android Signing
|
||||
if: startsWith(matrix.platform,'android')
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
|
||||
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
|
||||
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
|
||||
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
|
||||
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'core/go.mod'
|
||||
cache-dependency-path: |
|
||||
core/go.sum
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: 3.22.x
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Get Flutter Dependency
|
||||
run: flutter pub get
|
||||
|
||||
- name: Setup
|
||||
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }}
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
|
||||
path: ./dist
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
|
||||
|
||||
upload-release:
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
permissions: write-all
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./dist/
|
||||
pattern: artifact-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Pre Release
|
||||
run: |
|
||||
pip install gitchangelog pystache mustache markdown
|
||||
pre=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
|
||||
if [ -z "pre" ]; then
|
||||
echo "init" > release.md
|
||||
else
|
||||
current="${{ github.ref_name }}"
|
||||
echo -e "\n\n<details markdown=1><summary>All changes from $current to the latest commit:</summary>\n\n" >> release.md
|
||||
gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog"
|
||||
echo -e "\n\n</details>" >> release.md
|
||||
fi
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./dist/*
|
||||
body_path: './release.md'
|
||||
|
||||
- name: Create Fdroid Source Dir
|
||||
run: |
|
||||
mkdir -p ./tmp
|
||||
cp ./dist/*android-arm64-v8a* ./tmp/ || true
|
||||
echo "Files copied successfully"
|
||||
|
||||
- name: Push to fdroid repo
|
||||
uses: cpina/github-action-push-to-another-repository@v1.7.2
|
||||
env:
|
||||
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||
with:
|
||||
source-directory: ./tmp/
|
||||
destination-github-username: chen08209
|
||||
destination-repository-name: FlClash-fdroid-repo
|
||||
user-name: 'github-actions[bot]'
|
||||
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||
target-branch: action-pr
|
||||
commit-message: Update from ${{ github.ref_name }}
|
||||
target-directory: /tmp/
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,11 +5,9 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -6,5 +6,3 @@
|
||||
path = plugins/flutter_distributor
|
||||
url = git@github.com:chen08209/flutter_distributor.git
|
||||
branch = FlClash
|
||||
|
||||
|
||||
|
||||
918
CHANGELOG.md
918
CHANGELOG.md
@@ -1,918 +0,0 @@
|
||||
## v0.8.87
|
||||
|
||||
- Optimize desktop view
|
||||
|
||||
- Optimize logs, requests, connection pages
|
||||
|
||||
- Optimize windows tray auto hide
|
||||
|
||||
- Optimize some details
|
||||
|
||||
- Update core
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.86
|
||||
|
||||
- Fix windows tun issues
|
||||
|
||||
- Optimize android get system dns
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.85
|
||||
|
||||
- Support override script
|
||||
|
||||
- Support proxies search
|
||||
|
||||
- Support svg display
|
||||
|
||||
- Optimize config persistence
|
||||
|
||||
- Add some scenes auto close connections
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize more details
|
||||
|
||||
## v0.8.84
|
||||
|
||||
- Fix windows service verify issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.83
|
||||
|
||||
- Add windows server mode start process verify
|
||||
|
||||
- Add linux deb dependencies
|
||||
|
||||
- Add backup recovery strategy select
|
||||
|
||||
- Support custom text scaling
|
||||
|
||||
- Optimize the display of different text scale
|
||||
|
||||
- Optimize windows setup experience
|
||||
|
||||
- Optimize startTun performance
|
||||
|
||||
- Optimize android tv experience
|
||||
|
||||
- Optimize default option
|
||||
|
||||
- Optimize computed text size
|
||||
|
||||
- Optimize hyperOS freeform window
|
||||
|
||||
- Add developer mode
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Add issues template
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.82
|
||||
|
||||
- Optimize android vpn performance
|
||||
|
||||
- Add custom primary color and color scheme
|
||||
|
||||
- Add linux nad windows arm release
|
||||
|
||||
- Optimize requests and logs page
|
||||
|
||||
- Fix map input page delete issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.81
|
||||
|
||||
- Add rule override
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.80
|
||||
|
||||
- Optimize dashboard performance
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Fix unselected proxy group delay issues
|
||||
|
||||
- Fix asn url issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.79
|
||||
|
||||
- Fix tab delay view issues
|
||||
|
||||
- Fix tray action issues
|
||||
|
||||
- Fix get profile redirect client ua issues
|
||||
|
||||
- Fix proxy card delay view issues
|
||||
|
||||
- Add Russian, Japanese adaptation
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.78
|
||||
|
||||
- Fix list form input view issues
|
||||
|
||||
- Fix traffic view issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.77
|
||||
|
||||
- Optimize performance
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize core stability
|
||||
|
||||
- Fix linux tun authority check error
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Fix scroll physics error
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.75
|
||||
|
||||
- Add windows storage corruption detection
|
||||
|
||||
- Fix core crash caused by windows resource manager restart
|
||||
|
||||
- Optimize logs, requests, access to pages
|
||||
|
||||
- Fix macos bypass domain issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.74
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.73
|
||||
|
||||
- Update popup menu
|
||||
|
||||
- Add file editor
|
||||
|
||||
- Fix android service issues
|
||||
|
||||
- Optimize desktop background performance
|
||||
|
||||
- Optimize android main process performance
|
||||
|
||||
- Optimize delay test
|
||||
|
||||
- Optimize vpn protect
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.72
|
||||
|
||||
- Update core
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.71
|
||||
|
||||
- Remake dashboard
|
||||
|
||||
- Optimize theme
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.70
|
||||
|
||||
- Support better window position memory
|
||||
|
||||
- Add windows arm64 and linux arm64 build script
|
||||
|
||||
- Optimize some details
|
||||
|
||||
## v0.8.69
|
||||
|
||||
- Remake desktop
|
||||
|
||||
- Optimize change proxy
|
||||
|
||||
- Optimize network check
|
||||
|
||||
- Fix fallback issues
|
||||
|
||||
- Optimize lots of details
|
||||
|
||||
- Update change.yaml
|
||||
|
||||
- Fix android tile issues
|
||||
|
||||
- Fix windows tray issues
|
||||
|
||||
- Support setting bypassDomain
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Fix android service issues
|
||||
|
||||
- Fix macos dock exit button issues
|
||||
|
||||
- Add route address setting
|
||||
|
||||
- Optimize provider view
|
||||
|
||||
- Update changelog
|
||||
|
||||
- Update CHANGELOG.md
|
||||
|
||||
## v0.8.67
|
||||
|
||||
- Add android shortcuts
|
||||
|
||||
- Fix init params issues
|
||||
|
||||
- Fix dynamic color issues
|
||||
|
||||
- Optimize navigator animate
|
||||
|
||||
- Optimize window init
|
||||
|
||||
- Optimize fab
|
||||
|
||||
- Optimize save
|
||||
|
||||
## v0.8.66
|
||||
|
||||
- Fix the collapse issues
|
||||
|
||||
- Add fontFamily options
|
||||
|
||||
## v0.8.65
|
||||
|
||||
- Update core version
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Optimize ip check
|
||||
|
||||
- Optimize url-test
|
||||
|
||||
## v0.8.64
|
||||
|
||||
- Update release message
|
||||
|
||||
- Init auto gen changelog
|
||||
|
||||
- Fix windows tray issues
|
||||
|
||||
- Fix urltest issues
|
||||
|
||||
- Add auto changelog
|
||||
|
||||
- Fix windows admin auto launch issues
|
||||
|
||||
- Add android vpn options
|
||||
|
||||
- Support proxies icon configuration
|
||||
|
||||
- Optimize android immersion display
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Optimize ip detection
|
||||
|
||||
- Support android vpn ipv6 inbound switch
|
||||
|
||||
- Support log export
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Fix android system dns issues
|
||||
|
||||
- Optimize dns default option
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update readme
|
||||
|
||||
## v0.8.60
|
||||
|
||||
- Fix build error2
|
||||
|
||||
- Fix build error
|
||||
|
||||
- Support desktop hotkey
|
||||
|
||||
- Support android ipv6 inbound
|
||||
|
||||
- Support android system dns
|
||||
|
||||
- fix some bugs
|
||||
|
||||
## v0.8.59
|
||||
|
||||
- Fix delete profile error
|
||||
|
||||
## v0.8.58
|
||||
|
||||
- Fix submit error 2
|
||||
|
||||
- Fix submit error
|
||||
|
||||
- Optimize DNS strategy
|
||||
|
||||
- Fix the problem that the tray is not displayed in some cases
|
||||
|
||||
- Optimize tray
|
||||
|
||||
- Update core
|
||||
|
||||
- Fix some error
|
||||
|
||||
## v0.8.57
|
||||
|
||||
- Fix tun update issues
|
||||
|
||||
- Add DNS override
|
||||
- Fixed some bugs
|
||||
- Optimize more detail
|
||||
|
||||
- Add Hosts override
|
||||
|
||||
## v0.8.56
|
||||
|
||||
- fix android tip error
|
||||
- fix windows auto launch error
|
||||
|
||||
## v0.8.55
|
||||
|
||||
- Fix windows tray issues
|
||||
|
||||
- Optimize windows logic
|
||||
|
||||
- Optimize app logic
|
||||
|
||||
- Support windows administrator auto launch
|
||||
|
||||
- Support android close vpn
|
||||
|
||||
## v0.8.53
|
||||
|
||||
- Change flutter version
|
||||
|
||||
- Support profiles sort
|
||||
|
||||
- Support windows country flags display
|
||||
|
||||
- Optimize proxies page and profiles page columns
|
||||
|
||||
## v0.8.52
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Update version
|
||||
|
||||
- Update timeout time
|
||||
|
||||
- Update access control page
|
||||
|
||||
- Fix bug
|
||||
|
||||
## v0.8.51
|
||||
|
||||
- Optimize provider page
|
||||
|
||||
- Optimize delay test
|
||||
|
||||
- Support local backup and recovery
|
||||
|
||||
- Fix android tile service issues
|
||||
|
||||
## v0.8.49
|
||||
|
||||
- Fix linux core build error
|
||||
|
||||
- Add proxy-only traffic statistics
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Merge pull request #140 from txyyh/main
|
||||
|
||||
- 添加自建 F-Droid 仓库相关 workflow
|
||||
- Rename readme fingerprint
|
||||
|
||||
- Rename workflow deploy repo name
|
||||
|
||||
- Add download guide to README
|
||||
|
||||
- Add push release files to fdroid-repo
|
||||
|
||||
## v0.8.48
|
||||
|
||||
- Optimize proxies page
|
||||
|
||||
- Fix ua issues
|
||||
|
||||
- Optimize more details
|
||||
|
||||
## v0.8.47
|
||||
|
||||
- Fix windows build error
|
||||
|
||||
## v0.8.46
|
||||
|
||||
- Update app icon
|
||||
|
||||
- Fix desktop backup error
|
||||
|
||||
- Optimize request ua
|
||||
|
||||
- Change android icon
|
||||
|
||||
- Optimize dashboard
|
||||
|
||||
## v0.8.44
|
||||
|
||||
- Remove request validate certificate
|
||||
|
||||
- Sync core
|
||||
|
||||
## v0.8.43
|
||||
|
||||
- Fix windows error
|
||||
|
||||
## v0.8.42
|
||||
|
||||
- Fix setup.dart error
|
||||
|
||||
- Fix android system proxy not effective
|
||||
|
||||
- Add macos arm64
|
||||
|
||||
## v0.8.41
|
||||
|
||||
- Optimize proxies page
|
||||
|
||||
- Support mouse drag scroll
|
||||
|
||||
- Adjust desktop ui
|
||||
|
||||
- Revert "Fix android vpn issues"
|
||||
|
||||
- This reverts commit 891977408e6938e2acd74e9b9adb959c48c79988.
|
||||
|
||||
## v0.8.40
|
||||
|
||||
- Fix android vpn issues
|
||||
|
||||
- Fix android vpn issues
|
||||
|
||||
- Rollback partial modification
|
||||
|
||||
## v0.8.39
|
||||
|
||||
- Fix the problem that ui can't be synchronized when android vpn is occupied by an external
|
||||
|
||||
- Override default socksPort,port
|
||||
|
||||
## v0.8.38
|
||||
|
||||
- Fix fab issues
|
||||
|
||||
## v0.8.37
|
||||
|
||||
- Update version
|
||||
|
||||
- Fix the problem that vpn cannot be started in some cases
|
||||
|
||||
- Fix the problem that geodata url does not take effect
|
||||
|
||||
## v0.8.36
|
||||
|
||||
- Update ua
|
||||
|
||||
- Fix change outbound mode without check ip issues
|
||||
|
||||
- Separate android ui and vpn
|
||||
|
||||
- Fix url validate issues 2
|
||||
|
||||
- Add android hidden from the recent task
|
||||
|
||||
- Add geoip file
|
||||
|
||||
- Support modify geoData URL
|
||||
|
||||
## v0.8.35
|
||||
|
||||
- Fix url validate issues
|
||||
|
||||
- Fix check ip performance problem
|
||||
|
||||
- Optimize resources page
|
||||
|
||||
## v0.8.34
|
||||
|
||||
- Add ua selector
|
||||
|
||||
- Support modify test url
|
||||
|
||||
- Optimize android proxy
|
||||
|
||||
- Fix the error that async proxy provider could not selected the proxy
|
||||
|
||||
## v0.8.33
|
||||
|
||||
- Fix android proxy error
|
||||
|
||||
- Fix submit error
|
||||
|
||||
- Add windows tun
|
||||
|
||||
- Optimize android proxy
|
||||
|
||||
- Optimize change profile
|
||||
|
||||
- Update application ua
|
||||
|
||||
- Optimize delay test
|
||||
|
||||
## v0.8.32
|
||||
|
||||
- Fix android repeated request notification issues
|
||||
|
||||
## v0.8.31
|
||||
|
||||
- Fix memory overflow issues
|
||||
|
||||
## v0.8.30
|
||||
|
||||
- Optimize proxies expansion panel 2
|
||||
|
||||
- Fix android scan qrcode error
|
||||
|
||||
## v0.8.29
|
||||
|
||||
- Optimize proxies expansion panel
|
||||
|
||||
- Fix text error
|
||||
|
||||
## v0.8.28
|
||||
|
||||
- Optimize proxy
|
||||
|
||||
- Optimize delayed sorting performance
|
||||
|
||||
- Add expansion panel proxies page
|
||||
|
||||
- Support to adjust the proxy card size
|
||||
|
||||
- Support to adjust proxies columns number
|
||||
|
||||
- Fix autoRun show issues
|
||||
|
||||
- Fix Android 10 issues
|
||||
|
||||
- Optimize ip show
|
||||
|
||||
## v0.8.26
|
||||
|
||||
- Add intranet IP display
|
||||
|
||||
- Add connections page
|
||||
|
||||
- Add search in connections, requests
|
||||
|
||||
- Add keyword search in connections, requests, logs
|
||||
|
||||
- Add basic viewing editing capabilities
|
||||
|
||||
- Optimize update profile
|
||||
|
||||
## v0.8.25
|
||||
|
||||
- Update version
|
||||
|
||||
- Fix the problem of excessive memory usage in traffic usage.
|
||||
|
||||
- Add lightBlue theme color
|
||||
|
||||
- Fix start unable to update profile issues
|
||||
|
||||
- Fix flashback caused by process
|
||||
|
||||
## v0.8.23
|
||||
|
||||
- Add build version
|
||||
|
||||
- Optimize quick start
|
||||
|
||||
- Update system default option
|
||||
|
||||
## v0.8.22
|
||||
|
||||
- Update build.yml
|
||||
|
||||
- Fix android vpn close issues
|
||||
|
||||
- Add requests page
|
||||
|
||||
- Fix checkUpdate dark mode style error
|
||||
|
||||
- Fix quickStart error open app
|
||||
|
||||
- Add memory proxies tab index
|
||||
|
||||
- Support hidden group
|
||||
|
||||
- Optimize logs
|
||||
|
||||
- Fix externalController hot load error
|
||||
|
||||
## v0.8.21
|
||||
|
||||
- Add tcp concurrent switch
|
||||
|
||||
- Add system proxy switch
|
||||
|
||||
- Add geodata loader switch
|
||||
|
||||
- Add external controller switch
|
||||
|
||||
- Add auto gc on trim memory
|
||||
|
||||
- Fix android notification error
|
||||
|
||||
## v0.8.20
|
||||
|
||||
- Fix ipv6 error
|
||||
|
||||
- Fix android udp direct error
|
||||
|
||||
- Add ipv6 switch
|
||||
|
||||
- Add access all selected button
|
||||
|
||||
- Remove android low version splash
|
||||
|
||||
## v0.8.19
|
||||
|
||||
- Update version
|
||||
|
||||
- Add allowBypass
|
||||
|
||||
- Fix Android only pick .text file issues
|
||||
|
||||
## v0.8.18
|
||||
|
||||
- Fix search issues
|
||||
|
||||
## v0.8.17
|
||||
|
||||
- Fix LoadBalance, Relay load error
|
||||
|
||||
- Fix build.yml4
|
||||
|
||||
- Fix build.yml3
|
||||
|
||||
- Fix build.yml2
|
||||
|
||||
- Fix build.yml
|
||||
|
||||
- Add search function at access control
|
||||
|
||||
- Fix the issues with the profile add button to cover the edit button
|
||||
|
||||
- Adapt LoadBalance and Relay
|
||||
|
||||
- Add arm
|
||||
|
||||
- Fix android notification icon error
|
||||
|
||||
## v0.8.16
|
||||
|
||||
- Add one-click update all profiles
|
||||
- Add expire show
|
||||
|
||||
## v0.8.15
|
||||
|
||||
- Temp remove tun mode
|
||||
|
||||
- Remove macos in workflow
|
||||
|
||||
- Change go version
|
||||
|
||||
## v0.8.14
|
||||
|
||||
- Update Version
|
||||
|
||||
- Fix tun unable to open
|
||||
|
||||
## v0.8.13
|
||||
|
||||
- Optimize delay test2
|
||||
|
||||
- Optimize delay test
|
||||
|
||||
- Add check ip
|
||||
|
||||
- add check ip request
|
||||
|
||||
## v0.8.12
|
||||
|
||||
- Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the
|
||||
application to flash back.
|
||||
|
||||
- Fix edit profile error
|
||||
|
||||
- Fix quickStart change proxy error
|
||||
|
||||
- Fix core version
|
||||
|
||||
## v0.8.10
|
||||
|
||||
- Fix core version
|
||||
|
||||
## v0.8.9
|
||||
|
||||
- Update file_picker
|
||||
|
||||
- Add resources page
|
||||
|
||||
- Optimize more detail
|
||||
|
||||
- Add access selected sorted
|
||||
|
||||
- Fix notification duplicate creation issue
|
||||
|
||||
- Fix AccessControl click issue
|
||||
|
||||
## v0.8.7
|
||||
|
||||
- Fix Workflow
|
||||
|
||||
- Fix Linux unable to open
|
||||
|
||||
- Update README.md 3
|
||||
|
||||
- Create LICENSE
|
||||
- Update README.md 2
|
||||
|
||||
- Update README.md
|
||||
|
||||
- Optimize workFlow
|
||||
|
||||
## v0.8.6
|
||||
|
||||
- optimize checkUpdate
|
||||
|
||||
## v0.8.5
|
||||
|
||||
- Fix submit error
|
||||
|
||||
## v0.8.4
|
||||
|
||||
- add WebDAV
|
||||
|
||||
- add Auto check updates
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- optimize delayTest
|
||||
|
||||
## v0.8.2
|
||||
|
||||
- upgrade flutter version
|
||||
|
||||
## v0.8.1
|
||||
|
||||
- Update kernel
|
||||
- Add import profile via QR code image
|
||||
|
||||
## v0.8.0
|
||||
|
||||
- Add compatibility mode and adapt clash scheme.
|
||||
|
||||
## v0.7.14
|
||||
|
||||
- update Version
|
||||
|
||||
- Reconstruction application proxy logic
|
||||
|
||||
## v0.7.13
|
||||
|
||||
- Fix Tab destroy error
|
||||
|
||||
## v0.7.12
|
||||
|
||||
- Optimize repeat healthcheck
|
||||
|
||||
## v0.7.11
|
||||
|
||||
- Optimize Direct mode ui
|
||||
|
||||
## v0.7.10
|
||||
|
||||
- Optimize Healthcheck
|
||||
|
||||
- Remove proxies position animation, improve performance
|
||||
- Add Telegram Link
|
||||
|
||||
- Update healthcheck policy
|
||||
|
||||
- New Check URLTest
|
||||
|
||||
- Fix the problem of invalid auto-selection
|
||||
|
||||
## v0.7.8
|
||||
|
||||
- New Async UpdateConfig
|
||||
|
||||
- add changeProfileDebounce
|
||||
|
||||
- Update Workflow
|
||||
|
||||
- Fix ChangeProfile block
|
||||
|
||||
- Fix Release Message Error
|
||||
|
||||
## v0.7.7
|
||||
|
||||
- Update Selector 2
|
||||
|
||||
## v0.7.6
|
||||
|
||||
- Update Version
|
||||
|
||||
- Fix Proxies Select Error
|
||||
|
||||
## v0.7.5
|
||||
|
||||
- Fix the problem that the proxy group is empty in global mode.
|
||||
|
||||
- Fix the problem that the proxy group is empty in global mode.
|
||||
|
||||
## v0.7.4
|
||||
|
||||
- Add ProxyProvider2
|
||||
|
||||
## v0.7.3
|
||||
|
||||
- Add ProxyProvider
|
||||
|
||||
- Update Version
|
||||
|
||||
- Update ProxyGroup Sort
|
||||
|
||||
- Fix Android quickStart VpnService some problems
|
||||
|
||||
## v0.7.1
|
||||
|
||||
- Update version
|
||||
|
||||
- Set Android notification low importance
|
||||
|
||||
- Fix the issue that VpnService can't be closed correctly in special cases
|
||||
|
||||
- Fix the problem that TileService is not destroyed correctly in some cases
|
||||
|
||||
- Adjust tab animation defaults
|
||||
|
||||
- Add Telegram in README_zh_CN.md
|
||||
|
||||
- Add Telegram
|
||||
|
||||
## v0.7.0
|
||||
|
||||
- update mobile_scanner
|
||||
|
||||
- Initial commit
|
||||
10
Makefile
10
Makefile
@@ -1,10 +0,0 @@
|
||||
android_arm64:
|
||||
dart ./setup.dart android --arch arm64
|
||||
macos_arm64:
|
||||
dart ./setup.dart macos --arch arm64
|
||||
android_app:
|
||||
dart ./setup.dart android
|
||||
android_arm64_core:
|
||||
dart ./setup.dart android --arch arm64 --out core
|
||||
macos_arm64_core:
|
||||
dart ./setup.dart macos --arch arm64 --out core
|
||||
46
README.md
46
README.md
@@ -6,9 +6,13 @@
|
||||
|
||||
## FlClash
|
||||
|
||||
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||
|
||||
[](https://t.me/FlClash)
|
||||
<p style="text-align: left;">
|
||||
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
|
||||
<img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
|
||||
<a href="LICENSE">
|
||||
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||
|
||||
@@ -34,33 +38,14 @@ on Mobile:
|
||||
|
||||
✨ Support subscription link, Dark mode
|
||||
|
||||
## Use
|
||||
|
||||
### Linux
|
||||
|
||||
⚠️ Make sure to install the following dependencies before using them
|
||||
|
||||
```bash
|
||||
sudo apt-get install libayatana-appindicator3-dev
|
||||
sudo apt-get install libkeybinder-3.0-dev
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
Support the following actions
|
||||
|
||||
```bash
|
||||
com.follow.clash.action.START
|
||||
|
||||
com.follow.clash.action.STOP
|
||||
|
||||
com.follow.clash.action.CHANGE
|
||||
```
|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
||||
|
||||
## Contact
|
||||
|
||||
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
|
||||
|
||||
## Build
|
||||
|
||||
1. Update submodules
|
||||
@@ -93,7 +78,7 @@ Support the following actions
|
||||
3. Run build script
|
||||
|
||||
```bash
|
||||
dart .\setup.dart windows --arch <arm64 | amd64>
|
||||
dart .\setup.dart
|
||||
```
|
||||
|
||||
- linux
|
||||
@@ -103,7 +88,7 @@ Support the following actions
|
||||
2. Run build script
|
||||
|
||||
```bash
|
||||
dart .\setup.dart linux --arch <arm64 | amd64>
|
||||
dart .\setup.dart
|
||||
```
|
||||
|
||||
- macOS
|
||||
@@ -113,8 +98,11 @@ Support the following actions
|
||||
2. Run build script
|
||||
|
||||
```bash
|
||||
dart .\setup.dart macos --arch <arm64 | amd64>
|
||||
dart .\setup.dart
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Star
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
|
||||
## FlClash
|
||||
|
||||
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||
|
||||
[](https://t.me/FlClash)
|
||||
<p style="text-align: left;">
|
||||
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
|
||||
<img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
|
||||
<a href="LICENSE">
|
||||
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
|
||||
|
||||
@@ -34,33 +38,15 @@ on Mobile:
|
||||
|
||||
✨ 支持一键导入订阅, 深色模式
|
||||
|
||||
## Use
|
||||
|
||||
### Linux
|
||||
|
||||
⚠️ 使用前请确保安装以下依赖
|
||||
|
||||
```bash
|
||||
sudo apt-get install libayatana-appindicator3-dev
|
||||
sudo apt-get install libkeybinder-3.0-dev
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
支持下列操作
|
||||
|
||||
```bash
|
||||
com.follow.clash.action.START
|
||||
|
||||
com.follow.clash.action.STOP
|
||||
|
||||
com.follow.clash.action.CHANGE
|
||||
```
|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
|
||||
|
||||
## Build
|
||||
|
||||
1. 更新 submodules
|
||||
@@ -93,7 +79,7 @@ on Mobile:
|
||||
3. 运行构建脚本
|
||||
|
||||
```bash
|
||||
dart .\setup.dart windows --arch <arm64 | amd64>
|
||||
dart .\setup.dart
|
||||
```
|
||||
|
||||
- linux
|
||||
@@ -103,7 +89,7 @@ on Mobile:
|
||||
2. 运行构建脚本
|
||||
|
||||
```bash
|
||||
dart .\setup.dart linux --arch <arm64 | amd64>
|
||||
dart .\setup.dart
|
||||
```
|
||||
|
||||
- macOS
|
||||
@@ -113,7 +99,7 @@ on Mobile:
|
||||
2. 运行构建脚本
|
||||
|
||||
```bash
|
||||
dart .\setup.dart macos --arch <arm64 | amd64>
|
||||
dart .\setup.dart
|
||||
```
|
||||
|
||||
## Star History
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
plugins:
|
||||
- custom_lint
|
||||
exclude:
|
||||
- lib/l10n/intl/**
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
prefer_single_quotes: true
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
115
android/app/build.gradle
Normal file
115
android/app/build.gradle
Normal file
@@ -0,0 +1,115 @@
|
||||
import com.android.build.gradle.tasks.MergeSourceSetFolders
|
||||
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
def defStoreFile = file("keystore.jks")
|
||||
def defStorePassword = localProperties.getProperty('storePassword')
|
||||
def defKeyAlias = localProperties.getProperty('keyAlias')
|
||||
def defKeyPassword = localProperties.getProperty('keyPassword')
|
||||
def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias != null && defKeyPassword != null
|
||||
|
||||
android {
|
||||
namespace "com.follow.clash"
|
||||
compileSdkVersion 34
|
||||
ndkVersion "25.1.8937393"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
signingConfigs {
|
||||
if (isRelease){
|
||||
release {
|
||||
storeFile defStoreFile
|
||||
storePassword defStorePassword
|
||||
keyAlias defKeyAlias
|
||||
keyPassword defKeyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.follow.clash"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
if(isRelease){
|
||||
signingConfig signingConfigs.release
|
||||
}else{
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('copyNativeLibs', Copy) {
|
||||
delete('src/main/jniLibs')
|
||||
from('../../libclash/android')
|
||||
into('src/main/jniLibs')
|
||||
}
|
||||
|
||||
tasks.withType(MergeSourceSetFolders).configureEach {
|
||||
dependsOn copyNativeLibs
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
|
||||
exclude group: "com.google.guava", module: "guava"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
afterEvaluate {
|
||||
assembleDebug.dependsOn copyNativeLibs
|
||||
|
||||
assembleRelease.dependsOn copyNativeLibs
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val localPropertiesFile = rootProject.file("local.properties")
|
||||
val localProperties = Properties().apply {
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val mStoreFile: File = file("keystore.jks")
|
||||
val mStorePassword: String? = localProperties.getProperty("storePassword")
|
||||
val mKeyAlias: String? = localProperties.getProperty("keyAlias")
|
||||
val mKeyPassword: String? = localProperties.getProperty("keyPassword")
|
||||
val isRelease = mStoreFile.exists()
|
||||
&& mStorePassword != null
|
||||
&& mKeyAlias != null
|
||||
&& mKeyPassword != null
|
||||
|
||||
android {
|
||||
namespace = "com.follow.clash"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
ndkVersion = libs.versions.ndkVersion.get()
|
||||
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.follow.clash"
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (isRelease) {
|
||||
create("release") {
|
||||
storeFile = mStoreFile
|
||||
storePassword = mStorePassword
|
||||
keyAlias = mKeyAlias
|
||||
keyPassword = mKeyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
signingConfig = if (isRelease) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":service"))
|
||||
implementation(project(":common"))
|
||||
implementation(libs.core.splashscreen)
|
||||
implementation(libs.gson)
|
||||
implementation(libs.smali.dexlib2) {
|
||||
exclude(group = "com.google.guava", module = "guava")
|
||||
}
|
||||
}
|
||||
4
android/app/proguard-rules.pro
vendored
4
android/app/proguard-rules.pro
vendored
@@ -1,4 +1,2 @@
|
||||
|
||||
-keep class com.follow.clash.models.**{ *; }
|
||||
|
||||
-keep class com.follow.clash.service.models.**{ *; }
|
||||
-keep class com.follow.clash.models.**{ *; }
|
||||
@@ -1,17 +1,13 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label">
|
||||
<application android:label="FlClash Debug" tools:replace="android:label">
|
||||
<service
|
||||
android:name=".TileService"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label"
|
||||
tools:targetApi="24" />
|
||||
android:name=".services.FlClashTileService"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label">
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.RECEIVE_BROADCASTS"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:name="${applicationName}"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash">
|
||||
android:label="FlClash"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name="com.follow.clash.MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
@@ -52,9 +47,7 @@
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
@@ -73,29 +66,19 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
|
||||
<!-- android:value="true" />-->
|
||||
|
||||
<activity
|
||||
android:name=".TempActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@style/TransparentTheme">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="${applicationId}.action.START" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="${applicationId}.action.STOP" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="${applicationId}.action.TOGGLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
android:theme="@style/TransparentTheme" />
|
||||
|
||||
<service
|
||||
android:name=".TileService"
|
||||
android:name=".services.FlClashTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:label="FlClash"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
@@ -106,19 +89,6 @@
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".BroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="${applicationId}.permission.RECEIVE_BROADCASTS">
|
||||
<intent-filter>
|
||||
<action android:name="${applicationId}.intent.action.START" />
|
||||
<action android:name="${applicationId}.intent.action.STOP" />
|
||||
<action android:name="${applicationId}.intent.action.TOGGLE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
||||
<provider
|
||||
android:name=".FilesProvider"
|
||||
android:authorities="${applicationId}.files"
|
||||
@@ -141,6 +111,21 @@
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashVpnService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse" />
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.follow.clash.common.GlobalState
|
||||
|
||||
class Application : Application() {
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
GlobalState.init(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.follow.clash
|
||||
|
||||
import com.follow.clash.models.Props
|
||||
|
||||
interface BaseServiceInterface {
|
||||
fun start(port: Int, props: Props?): Int?
|
||||
fun stop()
|
||||
fun startForeground(title: String, content: String)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.follow.clash.common.BroadcastAction
|
||||
import com.follow.clash.common.action
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BroadcastReceiver : BroadcastReceiver(),
|
||||
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
BroadcastAction.START.action -> {
|
||||
launch {
|
||||
State.handleStartServiceAction()
|
||||
}
|
||||
}
|
||||
|
||||
BroadcastAction.STOP.action -> {
|
||||
State.handleStopServiceAction()
|
||||
}
|
||||
|
||||
BroadcastAction.TOGGLE.action -> {
|
||||
launch {
|
||||
State.handleToggleAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Base64
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
suspend fun Drawable.getBase64(): String {
|
||||
val drawable = this
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bitmap = drawable.toBitmap()
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> MethodChannel.awaitResult(
|
||||
method: String, arguments: Any? = null
|
||||
): T? = withContext(Dispatchers.Main) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
invokeMethod(method, arguments, object : MethodChannel.Result {
|
||||
override fun success(result: Any?) {
|
||||
@Suppress("UNCHECKED_CAST") continuation.resume(result as T?)
|
||||
}
|
||||
|
||||
override fun error(code: String, message: String?, details: Any?) {
|
||||
continuation.resume(null)
|
||||
}
|
||||
|
||||
override fun notImplemented() {
|
||||
continuation.resume(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : FlutterPlugin> FlutterEngine.plugin(): T? {
|
||||
return plugins.get(T::class.java) as T?
|
||||
}
|
||||
|
||||
|
||||
fun <T> MethodChannel.invokeMethodOnMainThread(
|
||||
method: String,
|
||||
arguments: Any? = null,
|
||||
callback: ((Result<T>) -> Unit)? = null
|
||||
) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invokeMethod(method, arguments, object : MethodChannel.Result {
|
||||
override fun success(result: Any?) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callback?.invoke(Result.success(result as T))
|
||||
}
|
||||
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
val exception = Exception("MethodChannel error: $errorCode - $errorMessage")
|
||||
callback?.invoke(Result.failure(exception))
|
||||
}
|
||||
|
||||
override fun notImplemented() {
|
||||
val exception = NotImplementedError("Method not implemented: $method")
|
||||
callback?.invoke(Result.failure(exception))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class FilesProvider : DocumentsProvider() {
|
||||
add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID)
|
||||
add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY)
|
||||
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
|
||||
add(Root.COLUMN_TITLE, "FlClash")
|
||||
add(Root.COLUMN_TITLE, context!!.getString(R.string.fl_clash))
|
||||
add(Root.COLUMN_SUMMARY, "Data")
|
||||
add(Root.COLUMN_DOCUMENT_ID, "/")
|
||||
}
|
||||
|
||||
71
android/app/src/main/kotlin/com/follow/clash/GlobalState.kt
Normal file
71
android/app/src/main/kotlin/com/follow/clash/GlobalState.kt
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import io.flutter.FlutterInjector
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.dart.DartExecutor
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
enum class RunState {
|
||||
START,
|
||||
PENDING,
|
||||
STOP
|
||||
}
|
||||
|
||||
|
||||
object GlobalState {
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
val runLock = ReentrantLock()
|
||||
|
||||
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
|
||||
var flutterEngine: FlutterEngine? = null
|
||||
private var serviceEngine: FlutterEngine? = null
|
||||
|
||||
fun getCurrentAppPlugin(): AppPlugin? {
|
||||
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
|
||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||
}
|
||||
|
||||
fun getCurrentTitlePlugin(): TilePlugin? {
|
||||
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
|
||||
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
|
||||
}
|
||||
|
||||
fun getCurrentVPNPlugin(): VpnPlugin? {
|
||||
val currentEngine = if (serviceEngine != null) serviceEngine else flutterEngine
|
||||
return currentEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||
}
|
||||
|
||||
fun destroyServiceEngine() {
|
||||
serviceEngine?.destroy()
|
||||
serviceEngine = null
|
||||
}
|
||||
|
||||
fun initServiceEngine(context: Context) {
|
||||
if (serviceEngine != null) return
|
||||
lock.withLock {
|
||||
destroyServiceEngine()
|
||||
serviceEngine = FlutterEngine(context)
|
||||
serviceEngine?.plugins?.add(VpnPlugin())
|
||||
serviceEngine?.plugins?.add(AppPlugin())
|
||||
serviceEngine?.plugins?.add(TilePlugin())
|
||||
serviceEngine?.plugins?.add(ServicePlugin())
|
||||
val vpnService = DartExecutor.DartEntrypoint(
|
||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
|
||||
"vpnService"
|
||||
)
|
||||
serviceEngine?.dartExecutor?.executeDartEntrypoint(
|
||||
vpnService,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : FlutterActivity(),
|
||||
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
State.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(AppPlugin())
|
||||
flutterEngine.plugins.add(VpnPlugin())
|
||||
flutterEngine.plugins.add(ServicePlugin())
|
||||
flutterEngine.plugins.add(TilePlugin())
|
||||
State.flutterEngine = flutterEngine
|
||||
GlobalState.flutterEngine = flutterEngine
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
State.flutterEngine = null
|
||||
GlobalState.flutterEngine = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.follow.clash
|
||||
|
||||
import com.follow.clash.common.ServiceDelegate
|
||||
import com.follow.clash.common.intent
|
||||
import com.follow.clash.service.ICallbackInterface
|
||||
import com.follow.clash.service.IRemoteInterface
|
||||
import com.follow.clash.service.RemoteService
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
object Service {
|
||||
private val delegate by lazy {
|
||||
ServiceDelegate<IRemoteInterface>(
|
||||
RemoteService::class.intent, ::handleOnServiceCrash
|
||||
) {
|
||||
IRemoteInterface.Stub.asInterface(it)
|
||||
}
|
||||
}
|
||||
|
||||
var onServiceCrash: (() -> Unit)? = null
|
||||
|
||||
private fun handleOnServiceCrash() {
|
||||
bindingState.set(false)
|
||||
onServiceCrash?.let {
|
||||
it()
|
||||
}
|
||||
}
|
||||
|
||||
private val bindingState = AtomicBoolean(false)
|
||||
|
||||
fun bind() {
|
||||
if (bindingState.compareAndSet(false, true)) {
|
||||
delegate.bind()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun invokeAction(
|
||||
data: String, cb: (result: String?) -> Unit
|
||||
) {
|
||||
delegate.useService {
|
||||
it.invokeAction(data, object : ICallbackInterface.Stub() {
|
||||
override fun onResult(result: String?) {
|
||||
cb(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateNotificationParams(
|
||||
params: NotificationParams
|
||||
) {
|
||||
delegate.useService {
|
||||
it.updateNotificationParams(params)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setMessageCallback(
|
||||
cb: (result: String?) -> Unit
|
||||
) {
|
||||
delegate.useService {
|
||||
it.setMessageCallback(object : ICallbackInterface.Stub() {
|
||||
override fun onResult(result: String?) {
|
||||
cb(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startService(options: VpnOptions, inApp: Boolean) {
|
||||
delegate.useService { it.startService(options, inApp) }
|
||||
}
|
||||
|
||||
suspend fun stopService() {
|
||||
delegate.useService { it.stopService() }
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package com.follow.clash
|
||||
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import io.flutter.FlutterInjector
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.dart.DartExecutor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
enum class RunState {
|
||||
START, PENDING, STOP
|
||||
}
|
||||
|
||||
|
||||
object State {
|
||||
|
||||
val runLock = Mutex()
|
||||
|
||||
var runTime: Long = 0
|
||||
|
||||
val runStateFlow: MutableStateFlow<RunState> = MutableStateFlow(RunState.STOP)
|
||||
var flutterEngine: FlutterEngine? = null
|
||||
var serviceFlutterEngine: FlutterEngine? = null
|
||||
|
||||
val appPlugin: AppPlugin?
|
||||
get() = flutterEngine?.plugin<AppPlugin>() ?: serviceFlutterEngine?.plugin<AppPlugin>()
|
||||
|
||||
val servicePlugin: ServicePlugin?
|
||||
get() = flutterEngine?.plugin<ServicePlugin>()
|
||||
?: serviceFlutterEngine?.plugin<ServicePlugin>()
|
||||
|
||||
val tilePlugin: TilePlugin?
|
||||
get() = flutterEngine?.plugin<TilePlugin>() ?: serviceFlutterEngine?.plugin<TilePlugin>()
|
||||
|
||||
suspend fun handleToggleAction() {
|
||||
var action: (suspend () -> Unit)?
|
||||
runLock.withLock {
|
||||
action = when (runStateFlow.value) {
|
||||
RunState.PENDING -> null
|
||||
RunState.START -> ::handleStopServiceAction
|
||||
RunState.STOP -> ::handleStartServiceAction
|
||||
}
|
||||
}
|
||||
action?.invoke()
|
||||
}
|
||||
|
||||
suspend fun handleStartServiceAction() {
|
||||
tilePlugin?.handleStart()
|
||||
if (flutterEngine != null) {
|
||||
return
|
||||
}
|
||||
startServiceWithEngine()
|
||||
}
|
||||
|
||||
fun handleStopServiceAction() {
|
||||
tilePlugin?.handleStop()
|
||||
if (flutterEngine != null || serviceFlutterEngine != null) {
|
||||
return
|
||||
}
|
||||
handleStopService()
|
||||
}
|
||||
|
||||
fun handleStartService() {
|
||||
if (appPlugin != null) {
|
||||
appPlugin?.requestNotificationsPermission {
|
||||
startService()
|
||||
}
|
||||
return
|
||||
}
|
||||
startService()
|
||||
}
|
||||
|
||||
suspend fun destroyServiceEngine() {
|
||||
runLock.withLock {
|
||||
withContext(Dispatchers.Main) {
|
||||
runCatching {
|
||||
serviceFlutterEngine?.destroy()
|
||||
serviceFlutterEngine = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startServiceWithEngine() {
|
||||
runLock.withLock {
|
||||
withContext(Dispatchers.Main) {
|
||||
serviceFlutterEngine = FlutterEngine(GlobalState.application)
|
||||
serviceFlutterEngine?.plugins?.add(ServicePlugin())
|
||||
serviceFlutterEngine?.plugins?.add(AppPlugin())
|
||||
serviceFlutterEngine?.plugins?.add(TilePlugin())
|
||||
val dartEntrypoint = DartExecutor.DartEntrypoint(
|
||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(), "_service"
|
||||
)
|
||||
serviceFlutterEngine?.dartExecutor?.executeDartEntrypoint(dartEntrypoint)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun startService() {
|
||||
GlobalState.launch {
|
||||
runLock.withLock {
|
||||
if (runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.START) {
|
||||
return@launch
|
||||
}
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
if (servicePlugin == null) {
|
||||
return@launch
|
||||
}
|
||||
val options = servicePlugin?.handleGetVpnOptions()
|
||||
if (options == null) {
|
||||
return@launch
|
||||
}
|
||||
appPlugin?.prepare(options.enable) {
|
||||
Service.startService(options, true)
|
||||
runStateFlow.tryEmit(RunState.START)
|
||||
runTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun handleStopService() {
|
||||
GlobalState.launch {
|
||||
runLock.withLock {
|
||||
if (runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.STOP) {
|
||||
return@launch
|
||||
}
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
Service.stopService()
|
||||
runStateFlow.tryEmit(RunState.STOP)
|
||||
runTime = 0
|
||||
}
|
||||
destroyServiceEngine()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,34 +2,10 @@ package com.follow.clash
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import com.follow.clash.common.QuickAction
|
||||
import com.follow.clash.common.action
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TempActivity : Activity(),
|
||||
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
class TempActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
when (intent.action) {
|
||||
QuickAction.START.action -> {
|
||||
launch {
|
||||
State.handleStartServiceAction()
|
||||
}
|
||||
}
|
||||
|
||||
QuickAction.STOP.action -> {
|
||||
State.handleStopServiceAction()
|
||||
}
|
||||
|
||||
QuickAction.TOGGLE.action -> {
|
||||
launch {
|
||||
State.handleToggleAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
finish()
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.follow.clash.common.QuickAction
|
||||
import com.follow.clash.common.quickIntent
|
||||
import com.follow.clash.common.toPendingIntent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TileService : TileService() {
|
||||
private var scope: CoroutineScope? = null
|
||||
private fun updateTile(runState: RunState) {
|
||||
if (qsTile != null) {
|
||||
qsTile.state = when (runState) {
|
||||
RunState.START -> Tile.STATE_ACTIVE
|
||||
RunState.PENDING -> Tile.STATE_UNAVAILABLE
|
||||
RunState.STOP -> Tile.STATE_INACTIVE
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
scope?.cancel()
|
||||
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
scope?.launch {
|
||||
State.runStateFlow.collect {
|
||||
updateTile(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
private fun handleToggle() {
|
||||
val intent = QuickAction.TOGGLE.quickIntent
|
||||
val pendingIntent = intent.toPendingIntent
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startActivityAndCollapse(pendingIntent)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
handleToggle()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
scope?.cancel()
|
||||
super.onStopListening()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.follow.clash.extensions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.system.OsConstants.IPPROTO_TCP
|
||||
import android.system.OsConstants.IPPROTO_UDP
|
||||
import android.util.Base64
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.models.Metadata
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
|
||||
suspend fun Drawable.getBase64(): String {
|
||||
val drawable = this
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bitmap = drawable.toBitmap()
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
fun Metadata.getProtocol(): Int? {
|
||||
if (network.startsWith("tcp")) return IPPROTO_TCP
|
||||
if (network.startsWith("udp")) return IPPROTO_UDP
|
||||
return null
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
import java.util.Date
|
||||
|
||||
data class Package(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
val system: Boolean,
|
||||
val internet: Boolean,
|
||||
val lastUpdateTime: Long,
|
||||
val isSystem: Boolean,
|
||||
val firstInstallTime: Long,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
data class Process(
|
||||
val id: Int,
|
||||
val metadata: Metadata,
|
||||
)
|
||||
|
||||
data class Metadata(
|
||||
val network: String,
|
||||
val sourceIP: String,
|
||||
val sourcePort: Int,
|
||||
val destinationIP: String,
|
||||
val destinationPort: Int,
|
||||
val host: String
|
||||
)
|
||||
19
android/app/src/main/kotlin/com/follow/clash/models/Props.kt
Normal file
19
android/app/src/main/kotlin/com/follow/clash/models/Props.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
enum class AccessControlMode {
|
||||
acceptSelected,
|
||||
rejectSelected,
|
||||
}
|
||||
|
||||
data class AccessControl(
|
||||
val mode: AccessControlMode,
|
||||
val acceptList: List<String>,
|
||||
val rejectList: List<String>,
|
||||
)
|
||||
|
||||
data class Props(
|
||||
val enable: Boolean?,
|
||||
val accessControl: AccessControl?,
|
||||
val allowBypass: Boolean?,
|
||||
val systemProxy: Boolean?,
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
|
||||
data class AppState(
|
||||
val currentProfileName: String,
|
||||
val stopText: String,
|
||||
val onlyStatisticsProxy: Boolean,
|
||||
)
|
||||
@@ -3,27 +3,26 @@ package com.follow.clash.plugins
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.common.Components
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.common.QuickAction
|
||||
import com.follow.clash.common.quickIntent
|
||||
import com.follow.clash.getBase64
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.extensions.getBase64
|
||||
import com.follow.clash.extensions.getProtocol
|
||||
import com.follow.clash.models.Package
|
||||
import com.follow.clash.models.Process
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
@@ -38,28 +37,26 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
companion object {
|
||||
const val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
||||
}
|
||||
private var activity: Activity? = null
|
||||
|
||||
private var activityRef: WeakReference<Activity>? = null
|
||||
private var toast: Toast? = null
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
private var vpnPrepareCallback: (suspend () -> Unit)? = null
|
||||
private var connectivity: ConnectivityManager? = null
|
||||
|
||||
private var requestNotificationCallback: (() -> Unit)? = null
|
||||
private var vpnCallBack: (() -> Unit)? = null
|
||||
|
||||
private val iconMap = mutableMapOf<String, String?>()
|
||||
|
||||
private val packages = mutableListOf<Package>()
|
||||
|
||||
private val skipPrefixList = listOf(
|
||||
@@ -117,24 +114,46 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
||||
}
|
||||
|
||||
|
||||
val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
|
||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
||||
|
||||
private var isBlockNotification: Boolean = false
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
context = flutterPluginBinding.applicationContext;
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
|
||||
channel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun tip(message: String?) {
|
||||
if (GlobalState.flutterEngine == null) {
|
||||
if (toast != null) {
|
||||
toast!!.cancel()
|
||||
}
|
||||
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
|
||||
toast!!.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
when (call.method) {
|
||||
"moveTaskToBack" -> {
|
||||
activityRef?.get()?.moveTaskToBack(true)
|
||||
result.success(true)
|
||||
activity?.moveTaskToBack(true)
|
||||
result.success(true);
|
||||
}
|
||||
|
||||
"updateExcludeFromRecents" -> {
|
||||
val value = call.argument<Boolean>("value")
|
||||
updateExcludeFromRecents(value)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"initShortcuts" -> {
|
||||
initShortcuts(call.arguments as String)
|
||||
result.success(true)
|
||||
result.success(true);
|
||||
}
|
||||
|
||||
"getPackages" -> {
|
||||
@@ -164,7 +183,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
if (iconMap["default"] == null) {
|
||||
iconMap["default"] =
|
||||
GlobalState.application.packageManager?.defaultActivityIcon?.getBase64()
|
||||
context.packageManager?.defaultActivityIcon?.getBase64()
|
||||
}
|
||||
result.success(iconMap["default"])
|
||||
return@launch
|
||||
@@ -172,47 +191,109 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
}
|
||||
|
||||
"resolverProcess" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val process =
|
||||
if (data != null) Gson().fromJson(
|
||||
data,
|
||||
Process::class.java
|
||||
) else null
|
||||
val metadata = process?.metadata
|
||||
val protocol = metadata?.getProtocol()
|
||||
if (protocol == null) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
if (connectivity == null) {
|
||||
connectivity = context.getSystemService<ConnectivityManager>()
|
||||
}
|
||||
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
|
||||
val dst = InetSocketAddress(
|
||||
metadata.destinationIP.ifEmpty { metadata.host },
|
||||
metadata.destinationPort
|
||||
)
|
||||
val uid = try {
|
||||
connectivity?.getConnectionOwnerUid(protocol, src, dst)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (uid == null || uid == -1) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val packages = context.packageManager?.getPackagesForUid(uid)
|
||||
result.success(packages?.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"tip" -> {
|
||||
val message = call.argument<String>("message")
|
||||
tip(message)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"openFile" -> {
|
||||
val path = call.argument<String>("path")!!
|
||||
openFile(path)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initShortcuts(label: String) {
|
||||
val shortcut = with(ShortcutInfoCompat.Builder(GlobalState.application, "toggle")) {
|
||||
setShortLabel(label)
|
||||
setIcon(
|
||||
IconCompat.createWithResource(
|
||||
GlobalState.application,
|
||||
R.mipmap.ic_launcher_round,
|
||||
)
|
||||
)
|
||||
setIntent(QuickAction.TOGGLE.quickIntent)
|
||||
build()
|
||||
}
|
||||
ShortcutManagerCompat.setDynamicShortcuts(
|
||||
GlobalState.application, listOf(shortcut)
|
||||
private fun openFile(path: String) {
|
||||
val file = File(path)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileProvider",
|
||||
file
|
||||
)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
|
||||
uri,
|
||||
"text/plain"
|
||||
)
|
||||
|
||||
val flags =
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
val resInfoList = context.packageManager.queryIntentActivities(
|
||||
intent, PackageManager.MATCH_DEFAULT_ONLY
|
||||
)
|
||||
|
||||
for (resolveInfo in resInfoList) {
|
||||
val packageName = resolveInfo.activityInfo.packageName
|
||||
context.grantUriPermission(
|
||||
packageName,
|
||||
uri,
|
||||
flags
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
activity?.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
println(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tip(message: String?) {
|
||||
Toast.makeText(GlobalState.application, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun updateExcludeFromRecents(value: Boolean?) {
|
||||
val am = getSystemService(GlobalState.application, ActivityManager::class.java)
|
||||
val am = getSystemService(context, ActivityManager::class.java)
|
||||
val task = am?.appTasks?.firstOrNull {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
it.taskInfo.taskId == activityRef?.get()?.taskId
|
||||
it.taskInfo.taskId == activity?.taskId
|
||||
} else {
|
||||
it.taskInfo.id == activityRef?.get()?.taskId
|
||||
it.taskInfo.id == activity?.taskId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +305,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private suspend fun getPackageIcon(packageName: String): String? {
|
||||
val packageManager = GlobalState.application.packageManager
|
||||
val packageManager = context.packageManager
|
||||
if (iconMap[packageName] == null) {
|
||||
iconMap[packageName] = try {
|
||||
packageManager?.getApplicationIcon(packageName)?.getBase64()
|
||||
@@ -237,22 +318,22 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private fun getPackages(): List<Package> {
|
||||
val packageManager = GlobalState.application.packageManager
|
||||
if (packages.isNotEmpty()) return packages
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
|
||||
?.filter {
|
||||
it.packageName != GlobalState.application.packageName || it.packageName == "android"
|
||||
val packageManager = context.packageManager
|
||||
if (packages.isNotEmpty()) return packages;
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||
it.packageName != context.packageName
|
||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo?.loadLabel(packageManager).toString(),
|
||||
system = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
|
||||
lastUpdateTime = it.lastUpdateTime,
|
||||
internet = it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
)
|
||||
}?.let { packages.addAll(it) }
|
||||
return packages
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
|
||||
firstInstallTime = it.firstInstallTime
|
||||
)
|
||||
}?.let { packages.addAll(it) }
|
||||
return packages;
|
||||
}
|
||||
|
||||
private suspend fun getPackagesToJson(): String {
|
||||
@@ -269,66 +350,45 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
}
|
||||
|
||||
fun requestNotificationsPermission(callBack: () -> Unit) {
|
||||
requestNotificationCallback = callBack
|
||||
fun requestVpnPermission(context: Context, callBack: () -> Unit) {
|
||||
vpnCallBack = callBack
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
return;
|
||||
}
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
|
||||
fun requestNotificationsPermission(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = ContextCompat.checkSelfPermission(
|
||||
GlobalState.application, Manifest.permission.POST_NOTIFICATIONS
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
if (permission == PackageManager.PERMISSION_GRANTED || isBlockNotification) {
|
||||
invokeRequestNotificationCallback()
|
||||
return
|
||||
}
|
||||
activityRef?.get()?.let {
|
||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
if (isBlockNotification) return
|
||||
if (activity == null) return
|
||||
ActivityCompat.requestPermissions(
|
||||
it,
|
||||
activity!!,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
return
|
||||
}
|
||||
return
|
||||
} else {
|
||||
invokeRequestNotificationCallback()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun invokeRequestNotificationCallback() {
|
||||
requestNotificationCallback?.invoke()
|
||||
requestNotificationCallback = null
|
||||
}
|
||||
|
||||
fun prepare(needPrepare: Boolean, callBack: (suspend () -> Unit)) {
|
||||
vpnPrepareCallback = callBack
|
||||
if (!needPrepare) {
|
||||
invokeVpnPrepareCallback()
|
||||
return
|
||||
}
|
||||
val intent = VpnService.prepare(GlobalState.application)
|
||||
if (intent != null) {
|
||||
activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
return
|
||||
}
|
||||
invokeVpnPrepareCallback()
|
||||
}
|
||||
|
||||
fun invokeVpnPrepareCallback() {
|
||||
GlobalState.launch {
|
||||
vpnPrepareCallback?.invoke()
|
||||
vpnPrepareCallback = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun isChinaPackage(packageName: String): Boolean {
|
||||
val packageManager = GlobalState.application.packageManager ?: return false
|
||||
val packageManager = context.packageManager ?: return false
|
||||
skipPrefixList.forEach {
|
||||
if (packageName == it || packageName.startsWith("$it.")) return false
|
||||
}
|
||||
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
}
|
||||
if (packageName.matches(chinaAppRegex)) {
|
||||
@@ -337,10 +397,11 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
try {
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackageInfo(
|
||||
packageName, PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
||||
packageName,
|
||||
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
||||
)
|
||||
} else {
|
||||
packageManager.getPackageInfo(
|
||||
@Suppress("DEPRECATION") packageManager.getPackageInfo(
|
||||
packageName, packageManagerFlags
|
||||
)
|
||||
}
|
||||
@@ -352,33 +413,31 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}.forEach {
|
||||
if (it.name.matches(chinaAppRegex)) return true
|
||||
}
|
||||
packageInfo.applicationInfo?.publicSourceDir?.let {
|
||||
ZipFile(File(it)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (packageEntry.name.startsWith("firebase-")) return false
|
||||
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (packageEntry.name.startsWith("firebase-")) return false
|
||||
}
|
||||
for (packageEntry in it.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
for (packageEntry in it.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (packageEntry.size > 15000000) {
|
||||
return true
|
||||
}
|
||||
val input = it.getInputStream(packageEntry).buffered()
|
||||
val dexFile = try {
|
||||
DexBackedDexFile.fromInputStream(null, input)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
for (clazz in dexFile.classes) {
|
||||
val clazzName =
|
||||
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
|
||||
.replace("$", ".")
|
||||
if (clazzName.matches(chinaAppRegex)) return true
|
||||
}
|
||||
if (packageEntry.size > 15000000) {
|
||||
return true
|
||||
}
|
||||
val input = it.getInputStream(packageEntry).buffered()
|
||||
val dexFile = try {
|
||||
DexBackedDexFile.fromInputStream(null, input)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
for (clazz in dexFile.classes) {
|
||||
val clazzName =
|
||||
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
|
||||
.replace("$", ".")
|
||||
if (clazzName.matches(chinaAppRegex)) return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,53 +447,47 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
channel =
|
||||
MethodChannel(flutterPluginBinding.binaryMessenger, "${Components.PACKAGE_NAME}/app")
|
||||
channel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
scope.cancel()
|
||||
fun requestGc() {
|
||||
channel.invokeMethod("gc", null)
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activityRef = WeakReference(binding.activity)
|
||||
activity = binding.activity;
|
||||
binding.addActivityResultListener(::onActivityResult)
|
||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activityRef = null
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activityRef = WeakReference(binding.activity)
|
||||
activity = binding.activity;
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
channel.invokeMethod("exit", null)
|
||||
activityRef = null
|
||||
activity = null
|
||||
}
|
||||
|
||||
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == FlutterActivity.RESULT_OK) {
|
||||
invokeVpnPrepareCallback()
|
||||
GlobalState.initServiceEngine(context)
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onRequestPermissionsResultListener(
|
||||
requestCode: Int, permissions: Array<String>, grantResults: IntArray
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
isBlockNotification = true
|
||||
}
|
||||
invokeRequestNotificationCallback()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,21 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.Service
|
||||
import com.follow.clash.State
|
||||
import com.follow.clash.awaitResult
|
||||
import com.follow.clash.common.Components
|
||||
import com.follow.clash.invokeMethodOnMainThread
|
||||
import com.follow.clash.models.AppState
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import com.google.gson.Gson
|
||||
import android.content.Context
|
||||
import com.follow.clash.GlobalState
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
|
||||
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
|
||||
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel = MethodChannel(
|
||||
flutterPluginBinding.binaryMessenger, "${Components.PACKAGE_NAME}/service"
|
||||
)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
@@ -37,27 +25,14 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"init" -> {
|
||||
handleInit(result)
|
||||
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
|
||||
GlobalState.initServiceEngine(context)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"invokeAction" -> {
|
||||
handleInvokeAction(call, result)
|
||||
}
|
||||
|
||||
"getRunTime" -> {
|
||||
handleGetRunTime(result)
|
||||
}
|
||||
|
||||
"syncState" -> {
|
||||
handleSyncState(call, result)
|
||||
}
|
||||
|
||||
"start" -> {
|
||||
handleStart(result)
|
||||
}
|
||||
|
||||
"stop" -> {
|
||||
handleStop(result)
|
||||
"destroy" -> {
|
||||
handleDestroy()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -65,73 +40,8 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInvokeAction(call: MethodCall, result: MethodChannel.Result) {
|
||||
launch {
|
||||
val data = call.arguments<String>()!!
|
||||
Service.invokeAction(data) {
|
||||
result.success(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStart(result: MethodChannel.Result) {
|
||||
State.handleStartService()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
private fun handleStop(result: MethodChannel.Result) {
|
||||
State.handleStopService()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
suspend fun handleGetVpnOptions(): VpnOptions? {
|
||||
val res = flutterMethodChannel.awaitResult<String>("getVpnOptions", null)
|
||||
return Gson().fromJson(res, VpnOptions::class.java)
|
||||
}
|
||||
|
||||
val semaphore = Semaphore(10)
|
||||
|
||||
fun handleSendEvent(value: String?) {
|
||||
launch(Dispatchers.Main) {
|
||||
semaphore.withPermit {
|
||||
flutterMethodChannel.invokeMethod("event", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onServiceCrash() {
|
||||
State.runStateFlow.tryEmit(RunState.STOP)
|
||||
flutterMethodChannel.invokeMethodOnMainThread<Any>("crash", null)
|
||||
}
|
||||
|
||||
private fun handleSyncState(call: MethodCall, result: MethodChannel.Result) {
|
||||
launch {
|
||||
val data = call.arguments<String>()!!
|
||||
val params = Gson().fromJson(data, AppState::class.java)
|
||||
Service.updateNotificationParams(
|
||||
NotificationParams(
|
||||
title = params.currentProfileName,
|
||||
stopText = params.stopText,
|
||||
onlyStatisticsProxy = params.onlyStatisticsProxy
|
||||
)
|
||||
)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun handleInit(result: MethodChannel.Result) {
|
||||
Service.bind()
|
||||
launch {
|
||||
Service.setMessageCallback {
|
||||
handleSendEvent(it)
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
Service.onServiceCrash = ::onServiceCrash
|
||||
}
|
||||
|
||||
private fun handleGetRunTime(result: MethodChannel.Result) {
|
||||
return result.success(State.runTime)
|
||||
private fun handleDestroy() {
|
||||
GlobalState.getCurrentVPNPlugin()?.stop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,36 @@
|
||||
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import com.follow.clash.common.Components
|
||||
import com.follow.clash.invokeMethodOnMainThread
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin,
|
||||
MethodChannel.MethodCallHandler {
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel =
|
||||
MethodChannel(flutterPluginBinding.binaryMessenger, "${Components.PACKAGE_NAME}/tile")
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
|
||||
channel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
handleDetached()
|
||||
channel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
fun handleStart() {
|
||||
channel.invokeMethodOnMainThread<Any>("start", null)
|
||||
onStart?.let { it() }
|
||||
channel.invokeMethod("start", null)
|
||||
}
|
||||
|
||||
fun handleStop() {
|
||||
channel.invokeMethodOnMainThread<Any>("stop", null)
|
||||
channel.invokeMethod("stop", null)
|
||||
onStop?.let { it() }
|
||||
}
|
||||
|
||||
private fun handleDetached() {
|
||||
channel.invokeMethod("detached", null)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.services.FlClashService
|
||||
import com.follow.clash.services.FlClashVpnService
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
|
||||
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
private lateinit var context: Context
|
||||
private var flClashService: BaseServiceInterface? = null
|
||||
private var port: Int = 7890
|
||||
private var props: Props? = null
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
flClashService = when (service) {
|
||||
is FlClashVpnService.LocalBinder -> service.getService()
|
||||
is FlClashService.LocalBinder -> service.getService()
|
||||
else -> throw Exception("invalid binder")
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
flClashService = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
|
||||
"start" -> {
|
||||
port = call.argument<Int>("port")!!
|
||||
val args = call.argument<String>("args")
|
||||
props =
|
||||
if (args != null) Gson().fromJson(args, Props::class.java) else null
|
||||
when (props?.enable == true) {
|
||||
true -> handleStartVpn()
|
||||
false -> start()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"stop" -> {
|
||||
stop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"setProtect" -> {
|
||||
val fd = call.argument<Int>("fd")
|
||||
if (fd != null) {
|
||||
if (flClashService is FlClashVpnService) {
|
||||
(flClashService as FlClashVpnService).protect(fd)
|
||||
}
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
"startForeground" -> {
|
||||
val title = call.argument<String>("title") as String
|
||||
val content = call.argument<String>("content") as String
|
||||
startForeground(title, content)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
private fun startForeground(title: String, content: String) {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
flClashService?.startForeground(title, content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
if (flClashService == null) {
|
||||
bindService()
|
||||
return
|
||||
}
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.START) return
|
||||
GlobalState.runState.value = RunState.START
|
||||
val fd = flClashService?.start(port, props)
|
||||
flutterMethodChannel.invokeMethod("started", fd)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
flClashService?.stop()
|
||||
}
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = when (props?.enable == true) {
|
||||
true -> Intent(context, FlClashVpnService::class.java)
|
||||
false -> Intent(context, FlClashService::class.java)
|
||||
}
|
||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.models.Props
|
||||
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
class FlClashService : Service(), BaseServiceInterface {
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): FlClashService = this@FlClashService
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun start(port: Int, props: Props?): Int? = null
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.lifecycle.Observer
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.TempActivity
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class FlClashTileService : TileService() {
|
||||
|
||||
private val observer = Observer<RunState> { runState ->
|
||||
updateTile(runState)
|
||||
}
|
||||
|
||||
private fun updateTile(runState: RunState) {
|
||||
if (qsTile != null) {
|
||||
qsTile.state = when (runState) {
|
||||
RunState.START -> Tile.STATE_ACTIVE
|
||||
RunState.PENDING -> Tile.STATE_UNAVAILABLE
|
||||
RunState.STOP -> Tile.STATE_INACTIVE
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
GlobalState.runState.value?.let { updateTile(it) }
|
||||
GlobalState.runState.observeForever(observer)
|
||||
}
|
||||
|
||||
private fun activityTransfer() {
|
||||
val intent = Intent(this, TempActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startActivityAndCollapse(pendingIntent)
|
||||
} else {
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
activityTransfer()
|
||||
if (GlobalState.runState.value == RunState.STOP) {
|
||||
GlobalState.runState.value = RunState.PENDING
|
||||
val titlePlugin = GlobalState.getCurrentTitlePlugin()
|
||||
if (titlePlugin != null) {
|
||||
titlePlugin.handleStart()
|
||||
} else {
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
}
|
||||
} else if (GlobalState.runState.value == RunState.START) {
|
||||
GlobalState.runState.value = RunState.PENDING
|
||||
GlobalState.getCurrentTitlePlugin()?.handleStop()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
GlobalState.runState.removeObserver(observer)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.models.AccessControlMode
|
||||
import com.follow.clash.models.Props
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
|
||||
private val passList = listOf(
|
||||
"*zhihu.com",
|
||||
"*zhimg.com",
|
||||
"*jd.com",
|
||||
"100ime-iat-api.xfyun.cn",
|
||||
"*360buyimg.com",
|
||||
"localhost",
|
||||
"*.local",
|
||||
"127.*",
|
||||
"10.*",
|
||||
"172.16.*",
|
||||
"172.17.*",
|
||||
"172.18.*",
|
||||
"172.19.*",
|
||||
"172.2*",
|
||||
"172.30.*",
|
||||
"172.31.*",
|
||||
"192.168.*"
|
||||
)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
}
|
||||
|
||||
override fun start(port: Int, props: Props?): Int? {
|
||||
return with(Builder()) {
|
||||
addAddress("172.16.0.1", 30)
|
||||
setMtu(9000)
|
||||
addRoute("0.0.0.0", 0)
|
||||
props?.accessControl?.let { accessControl ->
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
}
|
||||
}
|
||||
|
||||
AccessControlMode.rejectSelected -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
addDnsServer("172.16.0.2")
|
||||
setSession("FlClash")
|
||||
setBlocking(false)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
setMetered(false)
|
||||
}
|
||||
if (props?.allowBypass == true) {
|
||||
allowBypass()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && props?.systemProxy == true) {
|
||||
setHttpProxy(
|
||||
ProxyInfo.buildDirectProxy(
|
||||
"127.0.0.1",
|
||||
port,
|
||||
passList
|
||||
)
|
||||
)
|
||||
}
|
||||
establish()?.detachFd()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
GlobalState.getCurrentAppPlugin()?.requestGc()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): FlClashVpnService = this@FlClashVpnService
|
||||
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
try {
|
||||
val isSuccess = super.onTransact(code, data, reply, flags)
|
||||
if (!isSuccess) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
GlobalState.getCurrentTitlePlugin()?.handleStop()
|
||||
}
|
||||
}
|
||||
return isSuccess
|
||||
} catch (e: RemoteException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stop()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
BIN
android/app/src/main/res/drawable-hdpi/ic_stat_name.png
Normal file
BIN
android/app/src/main/res/drawable-hdpi/ic_stat_name.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_stat_name.png
Normal file
BIN
android/app/src/main/res/drawable-mdpi/ic_stat_name.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 423 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_stat_name.png
Normal file
BIN
android/app/src/main/res/drawable-xhdpi/ic_stat_name.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 803 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_stat_name.png
Normal file
BIN
android/app/src/main/res/drawable-xxhdpi/ic_stat_name.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png
Normal file
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="FlClash">FlClash</string>
|
||||
<string name="fl_clash">FlClash</string>
|
||||
</resources>
|
||||
32
android/build.gradle
Normal file
32
android/build.gradle
Normal file
@@ -0,0 +1,32 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = "${kotlin_version}"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$agp_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(libs.build.kotlin)
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library") apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
|
||||
1
android/common/.gitignore
vendored
1
android/common/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,42 +0,0 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.follow.clash.common"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.gson)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="${applicationId}.permission.RECEIVE_BROADCASTS" />
|
||||
</manifest>
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.follow.clash.common
|
||||
|
||||
import android.content.ComponentName
|
||||
|
||||
object Components {
|
||||
const val PACKAGE_NAME = "com.follow.clash"
|
||||
|
||||
val MAIN_ACTIVITY =
|
||||
ComponentName(GlobalState.packageName, "${PACKAGE_NAME}.MainActivity")
|
||||
|
||||
val TEMP_ACTIVITY =
|
||||
ComponentName(GlobalState.packageName, "${PACKAGE_NAME}.TempActivity")
|
||||
|
||||
val BROADCAST_RECEIVER =
|
||||
ComponentName(GlobalState.packageName, "${PACKAGE_NAME}.BroadcastReceiver")
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.follow.clash.common
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
enum class QuickAction {
|
||||
STOP,
|
||||
START,
|
||||
TOGGLE,
|
||||
}
|
||||
|
||||
enum class BroadcastAction {
|
||||
START,
|
||||
STOP,
|
||||
TOGGLE,
|
||||
}
|
||||
|
||||
enum class AccessControlMode {
|
||||
@SerializedName("acceptSelected")
|
||||
ACCEPT_SELECTED,
|
||||
|
||||
@SerializedName("rejectSelected")
|
||||
REJECT_SELECTED,
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package com.follow.clash.common
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Context.RECEIVER_NOT_EXPORTED
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
//fun Context.startForegroundServiceCompat(intent: Intent?) {
|
||||
// if (Build.VERSION.SDK_INT >= 26) {
|
||||
// startForegroundService(intent)
|
||||
// } else {
|
||||
// startService(intent)
|
||||
// }
|
||||
//}
|
||||
|
||||
val KClass<*>.intent: Intent
|
||||
get() = Intent(GlobalState.application, this.java)
|
||||
|
||||
fun Service.startForegroundCompat(id: Int, notification: Notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(id, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
startForeground(id, notification)
|
||||
}
|
||||
}
|
||||
|
||||
val QuickAction.action: String
|
||||
get() = "${GlobalState.application.packageName}.action.${this.name}"
|
||||
|
||||
val QuickAction.quickIntent: Intent
|
||||
get() = Intent().apply {
|
||||
setComponent(Components.TEMP_ACTIVITY)
|
||||
setPackage(GlobalState.packageName)
|
||||
action = this@quickIntent.action
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
}
|
||||
|
||||
val BroadcastAction.action: String
|
||||
get() = "${GlobalState.application.packageName}.intent.action.${this.name}"
|
||||
|
||||
val BroadcastAction.quickIntent: Intent
|
||||
get() = Intent().apply {
|
||||
setComponent(Components.BROADCAST_RECEIVER)
|
||||
setPackage(GlobalState.packageName)
|
||||
action = this@quickIntent.action
|
||||
}
|
||||
|
||||
fun BroadcastAction.sendBroadcast() {
|
||||
val intent = Intent().apply {
|
||||
action = this@sendBroadcast.action
|
||||
Log.d("[sendBroadcast]", "$action")
|
||||
setPackage(GlobalState.packageName)
|
||||
}
|
||||
GlobalState.application.sendBroadcast(
|
||||
intent, GlobalState.RECEIVE_BROADCASTS_PERMISSIONS
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val Intent.toPendingIntent: PendingIntent
|
||||
get() = PendingIntent.getActivity(
|
||||
GlobalState.application,
|
||||
0,
|
||||
this,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
|
||||
fun Service.startForeground(notification: Notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL)
|
||||
if (channel == null) {
|
||||
channel = NotificationChannel(
|
||||
GlobalState.NOTIFICATION_CHANNEL,
|
||||
"SERVICE_CHANNEL",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
startForegroundCompat(GlobalState.NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
fun Context.registerReceiverCompat(
|
||||
receiver: BroadcastReceiver,
|
||||
filter: IntentFilter,
|
||||
) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
registerReceiver(receiver, filter)
|
||||
}
|
||||
|
||||
fun Context.receiveBroadcastFlow(
|
||||
configure: IntentFilter.() -> Unit,
|
||||
): Flow<Intent> = callbackFlow {
|
||||
val filter = IntentFilter().apply(configure)
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (context == null || intent == null) return
|
||||
trySend(intent)
|
||||
}
|
||||
}
|
||||
registerReceiverCompat(receiver, filter)
|
||||
awaitClose { unregisterReceiver(receiver) }
|
||||
}
|
||||
|
||||
|
||||
sealed class BindServiceEvent<out T : IBinder> {
|
||||
data class Connected<T : IBinder>(val binder: T) : BindServiceEvent<T>()
|
||||
object Disconnected : BindServiceEvent<Nothing>()
|
||||
object Crashed : BindServiceEvent<Nothing>()
|
||||
}
|
||||
|
||||
inline fun <reified T : IBinder> Context.bindServiceFlow(
|
||||
intent: Intent,
|
||||
flags: Int = Context.BIND_AUTO_CREATE,
|
||||
): Flow<BindServiceEvent<T>> = callbackFlow {
|
||||
var currentBinder: IBinder? = null
|
||||
val deathRecipient = IBinder.DeathRecipient {
|
||||
trySend(BindServiceEvent.Crashed)
|
||||
}
|
||||
|
||||
val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
if (binder != null) {
|
||||
try {
|
||||
binder.linkToDeath(deathRecipient, 0)
|
||||
currentBinder = binder
|
||||
@Suppress("UNCHECKED_CAST") val casted = binder as? T
|
||||
if (casted != null) {
|
||||
trySend(BindServiceEvent.Connected(casted))
|
||||
} else {
|
||||
GlobalState.log("Binder is not of type ${T::class.java}")
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
GlobalState.log("Failed to link to death: ${e.message}")
|
||||
binder.unlinkToDeath(deathRecipient, 0)
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
}
|
||||
} else {
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
GlobalState.log("Service disconnected")
|
||||
currentBinder?.unlinkToDeath(deathRecipient, 0)
|
||||
currentBinder = null
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
}
|
||||
}
|
||||
|
||||
if (!bindService(intent, connection, flags)) {
|
||||
GlobalState.log("Failed to bind service")
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
close()
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
currentBinder?.unlinkToDeath(deathRecipient, 0)
|
||||
unbindService(connection)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val Long.formatBytes: String
|
||||
get() {
|
||||
val units = arrayOf("B", "KB", "MB", "GB", "TB")
|
||||
var size = this.toDouble()
|
||||
var unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.size - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return if (unitIndex == 0) {
|
||||
"${size.toLong()}${units[unitIndex]}"
|
||||
} else {
|
||||
"%.1f${units[unitIndex]}".format(size)
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.follow.clash.common
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
object GlobalState : CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
||||
|
||||
const val NOTIFICATION_CHANNEL = "FlClash"
|
||||
|
||||
const val NOTIFICATION_ID = 1
|
||||
|
||||
val packageName: String
|
||||
get() = _application.packageName
|
||||
|
||||
val RECEIVE_BROADCASTS_PERMISSIONS: String
|
||||
get() = "${packageName}.permission.RECEIVE_BROADCASTS"
|
||||
|
||||
|
||||
private lateinit var _application: Application
|
||||
|
||||
val application: Application
|
||||
get() = _application
|
||||
|
||||
|
||||
fun log(text: String) {
|
||||
Log.d("[FlClash]", text)
|
||||
}
|
||||
|
||||
fun init(application: Application) {
|
||||
_application = application
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.follow.clash.common
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
class ServiceDelegate<T>(
|
||||
private val intent: Intent,
|
||||
private val onServiceDisconnected: (() -> Unit)? = null,
|
||||
private val onServiceCrash: (() -> Unit)? = null,
|
||||
private val interfaceCreator: (IBinder) -> T,
|
||||
) : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
|
||||
private val _service = MutableStateFlow<T?>(null)
|
||||
|
||||
val service: StateFlow<T?> = _service
|
||||
|
||||
private var bindJob: Job? = null
|
||||
private fun handleBindEvent(event: BindServiceEvent<IBinder>) {
|
||||
when (event) {
|
||||
is BindServiceEvent.Connected -> {
|
||||
_service.value = event.binder.let(interfaceCreator)
|
||||
}
|
||||
|
||||
is BindServiceEvent.Disconnected -> {
|
||||
_service.value = null
|
||||
onServiceDisconnected?.invoke()
|
||||
}
|
||||
|
||||
is BindServiceEvent.Crashed -> {
|
||||
_service.value = null
|
||||
onServiceCrash?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind() {
|
||||
unbind()
|
||||
bindJob = launch {
|
||||
GlobalState.application.bindServiceFlow<IBinder>(intent).collect { it ->
|
||||
handleBindEvent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <R> useService(
|
||||
crossinline block: (T) -> R
|
||||
): Result<R> {
|
||||
return withTimeoutOrNull(10_000) {
|
||||
service.filterNotNull().retryWhen { _, _ ->
|
||||
delay(200)
|
||||
true
|
||||
}.first()
|
||||
}?.let { s ->
|
||||
try {
|
||||
Result.success(block(s))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
} ?: Result.failure(Exception("Service connection timeout"))
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
_service.value = null
|
||||
bindJob?.cancel()
|
||||
bindJob = null
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.follow.clash.common
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
|
||||
fun tickerFlow(delayMillis: Long, initialDelayMillis: Long = delayMillis): Flow<Unit> = flow {
|
||||
delay(initialDelayMillis)
|
||||
while (true) {
|
||||
emit(Unit)
|
||||
delay(delayMillis)
|
||||
}
|
||||
}
|
||||
1
android/core/.gitignore
vendored
1
android/core/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,81 +0,0 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.follow.clash.core"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
ndkVersion = libs.versions.ndkVersion.get()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
}
|
||||
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
jniLibs.srcDirs("src/main/jniLibs")
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation(libs.annotation.jvm)
|
||||
}
|
||||
|
||||
val copyNativeLibs by tasks.register<Copy>("copyNativeLibs") {
|
||||
doFirst {
|
||||
delete("src/main/jniLibs")
|
||||
}
|
||||
from("../../libclash/android")
|
||||
into("src/main/jniLibs")
|
||||
|
||||
doLast {
|
||||
val includesDir = file("src/main/jniLibs/includes")
|
||||
val targetDir = file("src/main/cpp/includes")
|
||||
if (includesDir.exists()) {
|
||||
copy {
|
||||
from(includesDir)
|
||||
into(targetDir)
|
||||
}
|
||||
delete(includesDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(copyNativeLibs)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
@@ -1,51 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
|
||||
project("core")
|
||||
|
||||
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
|
||||
|
||||
message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
|
||||
|
||||
|
||||
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
# set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
||||
add_compile_options(-O3)
|
||||
|
||||
add_compile_options(-flto)
|
||||
|
||||
add_compile_options(-g0)
|
||||
|
||||
add_compile_options(-ffunction-sections -fdata-sections)
|
||||
|
||||
add_compile_options(-fno-exceptions -fno-rtti)
|
||||
|
||||
add_link_options(
|
||||
-flto
|
||||
-Wl,--gc-sections
|
||||
-Wl,--strip-all
|
||||
-Wl,--exclude-libs=ALL
|
||||
)
|
||||
|
||||
add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden)
|
||||
endif ()
|
||||
|
||||
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")
|
||||
|
||||
message("LIB_CLASH_PATH ${LIB_CLASH_PATH}")
|
||||
if (EXISTS ${LIB_CLASH_PATH})
|
||||
message("Found libclash.so for ABI ${ANDROID_ABI}")
|
||||
add_compile_definitions(LIBCLASH)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/../cpp/includes/${ANDROID_ABI})
|
||||
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
jni_helper.cpp
|
||||
core.cpp)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
clash)
|
||||
else ()
|
||||
message("Not found libclash.so for ABI ${ANDROID_ABI}")
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
jni_helper.cpp
|
||||
core.cpp)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME})
|
||||
endif ()
|
||||
@@ -1,192 +0,0 @@
|
||||
#include <jni.h>
|
||||
|
||||
#ifdef LIBCLASH
|
||||
|
||||
#include "jni_helper.h"
|
||||
#include "libclash.h"
|
||||
#include "bride.h"
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb,
|
||||
jstring address, jstring dns) {
|
||||
const auto interface = new_global(cb);
|
||||
scoped_string addressChar = get_string(address);
|
||||
scoped_string dnsChar = get_string(dns);
|
||||
startTUN(interface, fd, addressChar, dnsChar);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_stopTun(JNIEnv *) {
|
||||
stopTun();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_forceGC(JNIEnv *env, jobject thiz) {
|
||||
forceGC();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_updateDNS(JNIEnv *env, jobject thiz, jstring dns) {
|
||||
scoped_string dnsChar = get_string(dns);
|
||||
updateDns(dnsChar);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_invokeAction(JNIEnv *env, jobject thiz, jstring data, jobject cb) {
|
||||
const auto interface = new_global(cb);
|
||||
scoped_string dataChar = get_string(data);
|
||||
invokeAction(interface, dataChar);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_setMessageCallback(JNIEnv *env, jobject thiz, jobject cb) {
|
||||
const auto interface = new_global(cb);
|
||||
setMessageCallback(interface);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_follow_clash_core_Core_getTraffic(JNIEnv *env, jobject thiz,
|
||||
const jboolean only_statistics_proxy) {
|
||||
scoped_string res = getTraffic(only_statistics_proxy);
|
||||
return new_string(res);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_follow_clash_core_Core_getTotalTraffic(JNIEnv *env, jobject thiz,
|
||||
const jboolean only_statistics_proxy) {
|
||||
scoped_string res = getTotalTraffic(only_statistics_proxy);
|
||||
return new_string(res);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean suspended) {
|
||||
suspend(suspended);
|
||||
}
|
||||
|
||||
|
||||
static jmethodID m_tun_interface_protect;
|
||||
static jmethodID m_tun_interface_resolve_process;
|
||||
static jmethodID m_invoke_interface_result;
|
||||
|
||||
|
||||
static void release_jni_object_impl(void *obj) {
|
||||
ATTACH_JNI();
|
||||
del_global(static_cast<jobject>(obj));
|
||||
}
|
||||
|
||||
static void call_tun_interface_protect_impl(void *tun_interface, const int fd) {
|
||||
ATTACH_JNI();
|
||||
env->CallVoidMethod(static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_protect,
|
||||
fd);
|
||||
}
|
||||
|
||||
static char *
|
||||
call_tun_interface_resolve_process_impl(void *tun_interface, const int protocol,
|
||||
const char *source,
|
||||
const char *target,
|
||||
const int uid) {
|
||||
ATTACH_JNI();
|
||||
const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod(
|
||||
static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_resolve_process,
|
||||
protocol,
|
||||
new_string(source),
|
||||
new_string(target),
|
||||
uid));
|
||||
scoped_string packageNameChar = get_string(packageName);
|
||||
return packageNameChar;
|
||||
}
|
||||
|
||||
static void call_invoke_interface_result_impl(void *invoke_interface, const char *data) {
|
||||
ATTACH_JNI();
|
||||
env->CallVoidMethod(static_cast<jobject>(invoke_interface),
|
||||
m_invoke_interface_result,
|
||||
new_string(data));
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
JNI_OnLoad(JavaVM *vm, void *) {
|
||||
JNIEnv *env = nullptr;
|
||||
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||
return JNI_ERR;
|
||||
}
|
||||
|
||||
initialize_jni(vm, env);
|
||||
|
||||
const auto c_tun_interface = find_class("com/follow/clash/core/TunInterface");
|
||||
|
||||
const auto c_invoke_interface = find_class("com/follow/clash/core/InvokeInterface");
|
||||
|
||||
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V");
|
||||
m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess",
|
||||
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
|
||||
m_invoke_interface_result = find_method(c_invoke_interface, "onResult",
|
||||
"(Ljava/lang/String;)V");
|
||||
|
||||
|
||||
protect_func = &call_tun_interface_protect_impl;
|
||||
resolve_process_func = &call_tun_interface_resolve_process_impl;
|
||||
result_func = &call_invoke_interface_result_impl;
|
||||
release_object_func = &release_jni_object_impl;
|
||||
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
#else
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb,
|
||||
jstring address, jstring dns) {
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_invokeAction(JNIEnv *env, jobject thiz, jstring data, jobject cb) {
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_forceGC(JNIEnv *env, jobject thiz) {
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_updateDNS(JNIEnv *env, jobject thiz, jstring dns) {
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_setMessageCallback(JNIEnv *env, jobject thiz, jobject cb) {
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_follow_clash_core_Core_getTraffic(JNIEnv *env, jobject thiz,
|
||||
const jboolean only_statistics_proxy) {
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_follow_clash_core_Core_getTotalTraffic(JNIEnv *env, jobject thiz,
|
||||
const jboolean only_statistics_proxy) {
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean suspended) {
|
||||
}
|
||||
#endif
|
||||
@@ -1,71 +0,0 @@
|
||||
#include "jni_helper.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <malloc.h>
|
||||
#include <cstring>
|
||||
|
||||
static JavaVM *global_vm;
|
||||
|
||||
static jclass c_string;
|
||||
static jmethodID m_new_string;
|
||||
static jmethodID m_get_bytes;
|
||||
|
||||
void initialize_jni(JavaVM *vm, JNIEnv *env) {
|
||||
global_vm = vm;
|
||||
|
||||
c_string = reinterpret_cast<jclass>(new_global(find_class("java/lang/String")));
|
||||
m_new_string = find_method(c_string, "<init>", "([B)V");
|
||||
m_get_bytes = find_method(c_string, "getBytes", "()[B");
|
||||
}
|
||||
|
||||
JavaVM *global_java_vm() {
|
||||
return global_vm;
|
||||
}
|
||||
|
||||
char *jni_get_string(JNIEnv *env, jstring str) {
|
||||
const auto array = reinterpret_cast<jbyteArray>(env->CallObjectMethod(str, m_get_bytes));
|
||||
const int length = env->GetArrayLength(array);
|
||||
const auto content = static_cast<char *>(malloc(length + 1));
|
||||
env->GetByteArrayRegion(array, 0, length, reinterpret_cast<jbyte *>(content));
|
||||
content[length] = 0;
|
||||
return content;
|
||||
}
|
||||
|
||||
jstring jni_new_string(JNIEnv *env, const char *str) {
|
||||
const auto length = static_cast<int>(strlen(str));
|
||||
const auto array = env->NewByteArray(length);
|
||||
env->SetByteArrayRegion(array, 0, length, reinterpret_cast<const jbyte *>(str));
|
||||
return reinterpret_cast<jstring>(env->NewObject(c_string, m_new_string, array));
|
||||
}
|
||||
|
||||
int jni_catch_exception(JNIEnv *env) {
|
||||
const int result = env->ExceptionCheck();
|
||||
if (result) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void jni_attach_thread(scoped_jni *jni) {
|
||||
JavaVM *vm = global_java_vm();
|
||||
if (vm->GetEnv(reinterpret_cast<void **>(&jni->env), JNI_VERSION_1_6) == JNI_OK) {
|
||||
jni->require_release = 0;
|
||||
return;
|
||||
}
|
||||
if (vm->AttachCurrentThread(&jni->env, nullptr) != JNI_OK) {
|
||||
abort();
|
||||
}
|
||||
jni->require_release = 1;
|
||||
}
|
||||
|
||||
void jni_detach_thread(const scoped_jni *env) {
|
||||
JavaVM *vm = global_java_vm();
|
||||
if (env->require_release) {
|
||||
vm->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
|
||||
void release_string(char **str) {
|
||||
free(*str);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
struct scoped_jni {
|
||||
JNIEnv *env;
|
||||
int require_release;
|
||||
};
|
||||
|
||||
extern void initialize_jni(JavaVM *vm, JNIEnv *env);
|
||||
|
||||
extern jstring jni_new_string(JNIEnv *env, const char *str);
|
||||
|
||||
extern char *jni_get_string(JNIEnv *env, jstring str);
|
||||
|
||||
extern int jni_catch_exception(JNIEnv *env);
|
||||
|
||||
extern void jni_attach_thread(scoped_jni *jni);
|
||||
|
||||
extern void jni_detach_thread(const scoped_jni *env);
|
||||
|
||||
extern void release_string( char **str);
|
||||
|
||||
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
|
||||
scoped_jni _jni{}; \
|
||||
jni_attach_thread(&_jni); \
|
||||
JNIEnv *env = _jni.env
|
||||
|
||||
#define scoped_string __attribute__((cleanup(release_string))) char*
|
||||
|
||||
#define find_class(name) env->FindClass(name)
|
||||
#define find_method(cls, name, signature) env->GetMethodID(cls, name, signature)
|
||||
#define new_global(obj) env->NewGlobalRef(obj)
|
||||
#define del_global(obj) env->DeleteGlobalRef(obj)
|
||||
#define get_string(jstr) jni_get_string(env, jstr)
|
||||
#define new_string(cstr) jni_new_string(env, cstr)
|
||||
@@ -1,107 +0,0 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URL
|
||||
|
||||
data object Core {
|
||||
private external fun startTun(
|
||||
fd: Int,
|
||||
cb: TunInterface,
|
||||
address: String,
|
||||
dns: String,
|
||||
)
|
||||
|
||||
external fun forceGC(
|
||||
)
|
||||
|
||||
external fun updateDNS(
|
||||
dns: String,
|
||||
)
|
||||
|
||||
private fun parseInetSocketAddress(address: String): InetSocketAddress {
|
||||
val url = URL("https://$address")
|
||||
|
||||
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
|
||||
}
|
||||
|
||||
fun startTun(
|
||||
fd: Int,
|
||||
protect: (Int) -> Boolean,
|
||||
resolverProcess: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int) -> String,
|
||||
address: String,
|
||||
dns: String,
|
||||
) {
|
||||
startTun(
|
||||
fd,
|
||||
object : TunInterface {
|
||||
override fun protect(fd: Int) {
|
||||
protect(fd)
|
||||
}
|
||||
|
||||
override fun resolverProcess(
|
||||
protocol: Int,
|
||||
source: String,
|
||||
target: String,
|
||||
uid: Int
|
||||
): String {
|
||||
return resolverProcess(
|
||||
protocol,
|
||||
parseInetSocketAddress(source),
|
||||
parseInetSocketAddress(target),
|
||||
uid,
|
||||
)
|
||||
}
|
||||
},
|
||||
address,
|
||||
dns
|
||||
)
|
||||
}
|
||||
|
||||
external fun suspended(
|
||||
suspended: Boolean,
|
||||
)
|
||||
|
||||
private external fun invokeAction(
|
||||
data: String,
|
||||
cb: InvokeInterface
|
||||
)
|
||||
|
||||
fun invokeAction(
|
||||
data: String,
|
||||
cb: (result: String?) -> Unit
|
||||
) {
|
||||
invokeAction(
|
||||
data,
|
||||
object : InvokeInterface {
|
||||
override fun onResult(result: String?) {
|
||||
cb(result)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private external fun setMessageCallback(cb: InvokeInterface)
|
||||
|
||||
fun setMessageCallback(
|
||||
cb: (result: String?) -> Unit
|
||||
) {
|
||||
setMessageCallback(
|
||||
object : InvokeInterface {
|
||||
override fun onResult(result: String?) {
|
||||
cb(result)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
external fun stopTun()
|
||||
|
||||
external fun getTraffic(onlyStatisticsProxy: Boolean): String
|
||||
|
||||
external fun getTotalTraffic(onlyStatisticsProxy: Boolean): String
|
||||
|
||||
init {
|
||||
System.loadLibrary("core")
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
interface InvokeInterface {
|
||||
fun onResult(result: String?)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
interface TunInterface {
|
||||
fun protect(fd: Int)
|
||||
fun resolverProcess(protocol: Int, source: String, target: String, uid: Int): String
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx4G
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
kotlin_version=1.9.22
|
||||
agp_version=8.2.1
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
[versions]
|
||||
#agp = "8.10.1"
|
||||
minSdk = "23"
|
||||
targetSdk = "36"
|
||||
compileSdk = "36"
|
||||
ndkVersion = "28.0.13004108"
|
||||
coreKtx = "1.17.0"
|
||||
annotationJvm = "1.9.1"
|
||||
coreSplashscreen = "1.0.1"
|
||||
gson = "2.13.1"
|
||||
kotlin = "2.2.10"
|
||||
smaliDexlib2 = "3.0.9"
|
||||
|
||||
[libraries]
|
||||
build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
|
||||
annotation-jvm = { module = "androidx.annotation:annotation-jvm", version.ref = "annotationJvm" }
|
||||
core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
smali-dexlib2 = { module = "com.android.tools.smali:smali-dexlib2", version.ref = "smaliDexlib2" }
|
||||
@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
|
||||
|
||||
1
android/service/.gitignore
vendored
1
android/service/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,48 +0,0 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.follow.clash.service"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation(project(":common"))
|
||||
implementation(libs.gson)
|
||||
implementation(libs.androidx.core)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name="com.follow.clash.service.VpnService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:process=":background">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.follow.clash.service.CommonService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:process=":background">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="service" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.follow.clash.service.RemoteService"
|
||||
android:exported="false"
|
||||
android:process=":background" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,6 +0,0 @@
|
||||
// ICallbackInterface.aidl
|
||||
package com.follow.clash.service;
|
||||
|
||||
interface ICallbackInterface {
|
||||
void onResult(String result);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// IRemoteInterface.aidl
|
||||
package com.follow.clash.service;
|
||||
|
||||
import com.follow.clash.service.ICallbackInterface;
|
||||
import com.follow.clash.service.models.VpnOptions;
|
||||
import com.follow.clash.service.models.NotificationParams;
|
||||
|
||||
interface IRemoteInterface {
|
||||
void invokeAction(in String data, in ICallbackInterface callback);
|
||||
void updateNotificationParams(in NotificationParams params);
|
||||
void startService(in VpnOptions options,in boolean inApp);
|
||||
void stopService();
|
||||
void setMessageCallback(in ICallbackInterface messageCallback);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
//AccessControl.aidl
|
||||
package com.follow.clash.service.models;
|
||||
|
||||
parcelable AccessControl;
|
||||
@@ -1,4 +0,0 @@
|
||||
//NotificationParams.aidl
|
||||
package com.follow.clash.service.models;
|
||||
|
||||
parcelable NotificationParams;
|
||||
@@ -1,6 +0,0 @@
|
||||
//VpnOptions.aidl
|
||||
package com.follow.clash.service.models;
|
||||
|
||||
import com.follow.clash.service.models.AccessControl;
|
||||
|
||||
parcelable VpnOptions;
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.follow.clash.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.service.modules.NetworkObserveModule
|
||||
import com.follow.clash.service.modules.NotificationModule
|
||||
import com.follow.clash.service.modules.SuspendModule
|
||||
import com.follow.clash.service.modules.moduleLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
class CommonService : Service(), IBaseService,
|
||||
CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
||||
|
||||
private val self: CommonService
|
||||
get() = this
|
||||
|
||||
private val loader = moduleLoader {
|
||||
install(NetworkObserveModule(self))
|
||||
install(NotificationModule(self))
|
||||
install(SuspendModule(self))
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
handleCreate()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
Core.forceGC()
|
||||
super.onLowMemory()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): CommonService = this@CommonService
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
loader.load()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
loader.cancel()
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.follow.clash.service
|
||||
|
||||
import com.follow.clash.common.BroadcastAction
|
||||
import com.follow.clash.common.sendBroadcast
|
||||
|
||||
interface IBaseService {
|
||||
fun handleCreate() {
|
||||
if (!State.inApp) {
|
||||
BroadcastAction.START.sendBroadcast()
|
||||
} else {
|
||||
State.inApp = false
|
||||
}
|
||||
}
|
||||
|
||||
fun start()
|
||||
|
||||
fun stop()
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.follow.clash.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.follow.clash.common.ServiceDelegate
|
||||
import com.follow.clash.common.intent
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RemoteService : Service(),
|
||||
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
private var delegate: ServiceDelegate<IBaseService>? = null
|
||||
private var intent: Intent? = null
|
||||
|
||||
private fun handleStopService() {
|
||||
launch {
|
||||
delegate?.useService { service ->
|
||||
service.stop()
|
||||
}
|
||||
delegate?.unbind()
|
||||
}
|
||||
}
|
||||
|
||||
fun onServiceDisconnected() {
|
||||
handleStopService()
|
||||
}
|
||||
|
||||
private fun handleStartService() {
|
||||
launch {
|
||||
val nextIntent = when (State.options?.enable == true) {
|
||||
true -> VpnService::class.intent
|
||||
false -> CommonService::class.intent
|
||||
}
|
||||
if (intent != nextIntent) {
|
||||
delegate?.unbind()
|
||||
delegate = ServiceDelegate(nextIntent, ::onServiceDisconnected) { binder ->
|
||||
when (binder) {
|
||||
is VpnService.LocalBinder -> binder.getService()
|
||||
is CommonService.LocalBinder -> binder.getService()
|
||||
else -> throw IllegalArgumentException("Invalid binder type")
|
||||
}
|
||||
}
|
||||
intent = nextIntent
|
||||
delegate?.bind()
|
||||
}
|
||||
delegate?.useService { service ->
|
||||
service.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val binder: IRemoteInterface.Stub = object : IRemoteInterface.Stub() {
|
||||
override fun invokeAction(data: String, callback: ICallbackInterface) {
|
||||
Core.invokeAction(data, callback::onResult)
|
||||
}
|
||||
|
||||
override fun updateNotificationParams(params: NotificationParams?) {
|
||||
State.notificationParamsFlow.tryEmit(params)
|
||||
}
|
||||
|
||||
override fun startService(
|
||||
options: VpnOptions, inApp: Boolean
|
||||
) {
|
||||
State.options = options
|
||||
State.inApp = inApp
|
||||
handleStartService()
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
handleStopService()
|
||||
}
|
||||
|
||||
override fun setMessageCallback(messageCallback: ICallbackInterface) {
|
||||
setMessageCallback(messageCallback::onResult)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMessageCallback(cb: (result: String?) -> Unit) {
|
||||
Core.setMessageCallback(cb)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
return binder
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.follow.clash.service
|
||||
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
object State {
|
||||
var options: VpnOptions? = null
|
||||
var inApp: Boolean = false
|
||||
var notificationParamsFlow: MutableStateFlow<NotificationParams?> = MutableStateFlow(
|
||||
NotificationParams()
|
||||
)
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
package com.follow.clash.service
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ProxyInfo
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.common.AccessControlMode
|
||||
import com.follow.clash.common.BroadcastAction
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.common.sendBroadcast
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import com.follow.clash.service.models.getIpv4RouteAddress
|
||||
import com.follow.clash.service.models.getIpv6RouteAddress
|
||||
import com.follow.clash.service.models.toCIDR
|
||||
import com.follow.clash.service.modules.NetworkObserveModule
|
||||
import com.follow.clash.service.modules.NotificationModule
|
||||
import com.follow.clash.service.modules.SuspendModule
|
||||
import com.follow.clash.service.modules.moduleLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.net.InetSocketAddress
|
||||
import android.net.VpnService as SystemVpnService
|
||||
|
||||
class VpnService : SystemVpnService(), IBaseService,
|
||||
CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
||||
|
||||
private val self: VpnService
|
||||
get() = this
|
||||
|
||||
private val loader = moduleLoader {
|
||||
install(NetworkObserveModule(self))
|
||||
install(NotificationModule(self))
|
||||
install(SuspendModule(self))
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
handleCreate()
|
||||
}
|
||||
|
||||
private val connectivity by lazy {
|
||||
getSystemService<ConnectivityManager>()
|
||||
}
|
||||
private val uidPageNameMap = mutableMapOf<Int, String>()
|
||||
|
||||
private fun resolverProcess(
|
||||
protocol: Int,
|
||||
source: InetSocketAddress,
|
||||
target: InetSocketAddress,
|
||||
uid: Int,
|
||||
): String {
|
||||
val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1
|
||||
} else {
|
||||
uid
|
||||
}
|
||||
if (nextUid == -1) {
|
||||
return ""
|
||||
}
|
||||
if (!uidPageNameMap.containsKey(nextUid)) {
|
||||
uidPageNameMap[nextUid] = this.packageManager?.getPackagesForUid(nextUid)?.first() ?: ""
|
||||
}
|
||||
return uidPageNameMap[nextUid] ?: ""
|
||||
}
|
||||
|
||||
val VpnOptions.address
|
||||
get(): String = buildString {
|
||||
append(IPV4_ADDRESS)
|
||||
if (ipv6) {
|
||||
append(",")
|
||||
append(IPV6_ADDRESS)
|
||||
}
|
||||
}
|
||||
|
||||
val VpnOptions.dns
|
||||
get(): String {
|
||||
if (dnsHijacking) {
|
||||
return NET_ANY
|
||||
}
|
||||
return buildString {
|
||||
append(DNS)
|
||||
if (ipv6) {
|
||||
append(",")
|
||||
append(DNS6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onLowMemory() {
|
||||
Core.forceGC()
|
||||
super.onLowMemory()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): VpnService = this@VpnService
|
||||
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
try {
|
||||
val isSuccess = super.onTransact(code, data, reply, flags)
|
||||
if (!isSuccess) {
|
||||
GlobalState.log("onTransact error ===>")
|
||||
BroadcastAction.STOP.sendBroadcast()
|
||||
}
|
||||
return isSuccess
|
||||
} catch (e: RemoteException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
private fun handleStart(options: VpnOptions) {
|
||||
val fd = with(Builder()) {
|
||||
val cidr = IPV4_ADDRESS.toCIDR()
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
Log.d(
|
||||
"addAddress", "address: ${cidr.address} prefixLength:${cidr.prefixLength}"
|
||||
)
|
||||
val routeAddress = options.getIpv4RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute(NET_ANY, 0)
|
||||
}
|
||||
} else {
|
||||
addRoute(NET_ANY, 0)
|
||||
}
|
||||
if (options.ipv6) {
|
||||
try {
|
||||
val cidr = IPV6_ADDRESS.toCIDR()
|
||||
Log.d(
|
||||
"addAddress6", "address: ${cidr.address} prefixLength:${cidr.prefixLength}"
|
||||
)
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
} catch (_: Exception) {
|
||||
Log.d(
|
||||
"addAddress6", "IPv6 is not supported."
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
val routeAddress = options.getIpv6RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute6",
|
||||
"address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute("::", 0)
|
||||
}
|
||||
} else {
|
||||
addRoute(NET_ANY6, 0)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute(NET_ANY6, 0)
|
||||
}
|
||||
}
|
||||
addDnsServer(DNS)
|
||||
if (options.ipv6) {
|
||||
addDnsServer(DNS6)
|
||||
}
|
||||
setMtu(9000)
|
||||
options.accessControl.let { accessControl ->
|
||||
if (accessControl.enable) {
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.ACCEPT_SELECTED -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
}
|
||||
}
|
||||
|
||||
AccessControlMode.REJECT_SELECTED -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setSession("FlClash")
|
||||
setBlocking(false)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
setMetered(false)
|
||||
}
|
||||
if (options.allowBypass) {
|
||||
allowBypass()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
|
||||
setHttpProxy(
|
||||
ProxyInfo.buildDirectProxy(
|
||||
"127.0.0.1", options.port, options.bypassDomain
|
||||
)
|
||||
)
|
||||
}
|
||||
establish()?.detachFd()
|
||||
?: throw NullPointerException("Establish VPN rejected by system")
|
||||
}
|
||||
Core.startTun(
|
||||
fd,
|
||||
protect = this::protect,
|
||||
resolverProcess = this::resolverProcess,
|
||||
options.address,
|
||||
options.dns
|
||||
)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
loader.load()
|
||||
State.options?.let {
|
||||
handleStart(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
loader.cancel()
|
||||
Core.stopTun()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val IPV4_ADDRESS = "172.19.0.1/30"
|
||||
private const val IPV6_ADDRESS = "fdfe:dcba:9876::1/126"
|
||||
private const val DNS = "172.19.0.2"
|
||||
private const val DNS6 = "fdfe:dcba:9876::2"
|
||||
private const val NET_ANY = "0.0.0.0"
|
||||
private const val NET_ANY6 = "::"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.follow.clash.service.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class NotificationParams(
|
||||
val title: String = "FlClash",
|
||||
val stopText: String = "STOP",
|
||||
val onlyStatisticsProxy: Boolean = false,
|
||||
) : Parcelable
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.follow.clash.service.models
|
||||
|
||||
import com.follow.clash.common.formatBytes
|
||||
import com.follow.clash.core.Core
|
||||
import com.google.gson.Gson
|
||||
|
||||
data class Traffic(
|
||||
val up: Long,
|
||||
val down: Long,
|
||||
)
|
||||
|
||||
val Traffic.speedText: String
|
||||
get() = "${up.formatBytes}/s↑ ${down.formatBytes}/s↓"
|
||||
|
||||
fun Core.getSpeedTrafficText(onlyStatisticsProxy: Boolean): String {
|
||||
val res = getTraffic(onlyStatisticsProxy)
|
||||
val traffic = Gson().fromJson(res, Traffic::class.java)
|
||||
return traffic.speedText
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.follow.clash.service.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.follow.clash.common.AccessControlMode
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.net.InetAddress
|
||||
|
||||
@Parcelize
|
||||
data class AccessControl(
|
||||
val enable: Boolean,
|
||||
val mode: AccessControlMode,
|
||||
val acceptList: List<String>,
|
||||
val rejectList: List<String>,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class VpnOptions(
|
||||
val enable: Boolean,
|
||||
val port: Int,
|
||||
val ipv6: Boolean,
|
||||
val dnsHijacking: Boolean,
|
||||
val accessControl: AccessControl,
|
||||
val allowBypass: Boolean,
|
||||
val systemProxy: Boolean,
|
||||
val bypassDomain: List<String>,
|
||||
val routeAddress: List<String>,
|
||||
) : Parcelable
|
||||
|
||||
data class CIDR(val address: InetAddress, val prefixLength: Int)
|
||||
|
||||
fun VpnOptions.getIpv4RouteAddress(): List<CIDR> {
|
||||
return routeAddress.filter {
|
||||
it.isIpv4()
|
||||
}.map {
|
||||
it.toCIDR()
|
||||
}
|
||||
}
|
||||
|
||||
fun VpnOptions.getIpv6RouteAddress(): List<CIDR> {
|
||||
return routeAddress.filter {
|
||||
it.isIpv6()
|
||||
}.map {
|
||||
it.toCIDR()
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isIpv4(): Boolean {
|
||||
val parts = split("/")
|
||||
if (parts.size != 2) {
|
||||
throw IllegalArgumentException("Invalid CIDR format")
|
||||
}
|
||||
val address = InetAddress.getByName(parts[0])
|
||||
return address.address.size == 4
|
||||
}
|
||||
|
||||
fun String.isIpv6(): Boolean {
|
||||
val parts = split("/")
|
||||
if (parts.size != 2) {
|
||||
throw IllegalArgumentException("Invalid CIDR format")
|
||||
}
|
||||
val address = InetAddress.getByName(parts[0])
|
||||
return address.address.size == 16
|
||||
}
|
||||
|
||||
fun String.toCIDR(): CIDR {
|
||||
val parts = split("/")
|
||||
if (parts.size != 2) {
|
||||
throw IllegalArgumentException("Invalid CIDR format")
|
||||
}
|
||||
val ipAddress = parts[0]
|
||||
val prefixLength =
|
||||
parts[1].toIntOrNull() ?: throw IllegalArgumentException("Invalid prefix length")
|
||||
|
||||
val address = InetAddress.getByName(ipAddress)
|
||||
|
||||
val maxPrefix = if (address.address.size == 4) 32 else 128
|
||||
if (prefixLength < 0 || prefixLength > maxPrefix) {
|
||||
throw IllegalArgumentException("Invalid prefix length for IP version")
|
||||
}
|
||||
|
||||
return CIDR(address, prefixLength)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.follow.clash.service.modules
|
||||
|
||||
abstract class Module {
|
||||
|
||||
private var isInstall: Boolean = false
|
||||
|
||||
protected abstract fun onInstall()
|
||||
protected abstract fun onUninstall()
|
||||
|
||||
fun install() {
|
||||
isInstall = true
|
||||
onInstall()
|
||||
}
|
||||
|
||||
fun uninstall() {
|
||||
onUninstall()
|
||||
isInstall = false
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.follow.clash.service.modules
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
interface ModuleLoaderScope {
|
||||
fun <T : Module> install(module: T): T
|
||||
}
|
||||
|
||||
interface ModuleLoader {
|
||||
fun load()
|
||||
|
||||
fun cancel()
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
fun CoroutineScope.moduleLoader(block: suspend ModuleLoaderScope.() -> Unit): ModuleLoader {
|
||||
val modules = mutableListOf<Module>()
|
||||
var job: Job? = null
|
||||
|
||||
return object : ModuleLoader {
|
||||
override fun load() {
|
||||
job = launch(Dispatchers.IO) {
|
||||
mutex.withLock {
|
||||
val scope = object : ModuleLoaderScope {
|
||||
override fun <T : Module> install(module: T): T {
|
||||
modules.add(module)
|
||||
module.install()
|
||||
return module
|
||||
}
|
||||
}
|
||||
scope.block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
launch(Dispatchers.IO) {
|
||||
job?.cancel()
|
||||
mutex.withLock {
|
||||
modules.asReversed().forEach { it.uninstall() }
|
||||
modules.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package com.follow.clash.service.modules
|
||||
|
||||
import android.app.Service
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkCapabilities.TRANSPORT_SATELLITE
|
||||
import android.net.NetworkCapabilities.TRANSPORT_USB
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.core.Core
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
private data class NetworkInfo(
|
||||
@Volatile var losingMs: Long = 0, @Volatile var dnsList: List<InetAddress> = emptyList()
|
||||
) {
|
||||
fun isAvailable(): Boolean = losingMs < System.currentTimeMillis()
|
||||
}
|
||||
|
||||
class NetworkObserveModule(private val service: Service) : Module() {
|
||||
|
||||
private val networkInfos = ConcurrentHashMap<Network, NetworkInfo>()
|
||||
private val connectivity by lazy {
|
||||
service.getSystemService<ConnectivityManager>()
|
||||
}
|
||||
|
||||
private val request = NetworkRequest.Builder().apply {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)
|
||||
}
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
}.build()
|
||||
|
||||
private val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
networkInfos[network] = NetworkInfo()
|
||||
onUpdateNetwork()
|
||||
super.onAvailable(network)
|
||||
}
|
||||
|
||||
override fun onLosing(network: Network, maxMsToLive: Int) {
|
||||
networkInfos[network]?.losingMs = System.currentTimeMillis() + maxMsToLive
|
||||
onUpdateNetwork()
|
||||
setUnderlyingNetworks(network)
|
||||
super.onLosing(network, maxMsToLive)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
networkInfos.remove(network)
|
||||
onUpdateNetwork()
|
||||
setUnderlyingNetworks(network)
|
||||
super.onLost(network)
|
||||
}
|
||||
|
||||
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
|
||||
networkInfos[network]?.dnsList = linkProperties.dnsServers
|
||||
setUnderlyingNetworks(network)
|
||||
super.onLinkPropertiesChanged(network, linkProperties)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onInstall() {
|
||||
onUpdateNetwork()
|
||||
connectivity?.registerNetworkCallback(request, callback)
|
||||
}
|
||||
|
||||
private fun networkToInt(entry: Map.Entry<Network, NetworkInfo>): Int {
|
||||
val capabilities = connectivity?.getNetworkCapabilities(entry.key)
|
||||
return when {
|
||||
capabilities == null -> 100
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> 90
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 0
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 1
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && capabilities.hasTransport(
|
||||
TRANSPORT_USB
|
||||
) -> 2
|
||||
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> 3
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 4
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && capabilities.hasTransport(
|
||||
TRANSPORT_SATELLITE
|
||||
) -> 5
|
||||
|
||||
else -> 20
|
||||
} + (if (entry.value.isAvailable()) 0 else 10)
|
||||
}
|
||||
|
||||
fun onUpdateNetwork() {
|
||||
val dnsList = (networkInfos.asSequence().minByOrNull { networkToInt(it) }?.value?.dnsList
|
||||
?: emptyList()).map { x -> x.asSocketAddressText(53) }
|
||||
Core.updateDNS(dnsList.joinToString { "," })
|
||||
}
|
||||
|
||||
fun setUnderlyingNetworks(network: Network) {
|
||||
// if (service is VpnService && Build.VERSION.SDK_INT in 22..28) {
|
||||
// service.setUnderlyingNetworks(arrayOf(network))
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onUninstall() {
|
||||
connectivity?.unregisterNetworkCallback(callback)
|
||||
networkInfos.clear()
|
||||
onUpdateNetwork()
|
||||
}
|
||||
}
|
||||
|
||||
fun InetAddress.asSocketAddressText(port: Int): String {
|
||||
return when (this) {
|
||||
is Inet6Address -> "[${numericToTextFormat(this)}]:$port"
|
||||
|
||||
is Inet4Address -> "${this.hostAddress}:$port"
|
||||
|
||||
else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun numericToTextFormat(address: Inet6Address): String {
|
||||
val src = address.address
|
||||
val sb = StringBuilder(39)
|
||||
for (i in 0 until 8) {
|
||||
sb.append(
|
||||
Integer.toHexString(
|
||||
src[i shl 1].toInt() shl 8 and 0xff00 or (src[(i shl 1) + 1].toInt() and 0xff)
|
||||
)
|
||||
)
|
||||
if (i < 7) {
|
||||
sb.append(":")
|
||||
}
|
||||
}
|
||||
if (address.scopeId > 0) {
|
||||
sb.append("%")
|
||||
sb.append(address.scopeId)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package com.follow.clash.service.modules
|
||||
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.Service
|
||||
import android.app.Service.STOP_FOREGROUND_REMOVE
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.common.Components
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.common.QuickAction
|
||||
import com.follow.clash.common.quickIntent
|
||||
import com.follow.clash.common.receiveBroadcastFlow
|
||||
import com.follow.clash.common.startForeground
|
||||
import com.follow.clash.common.tickerFlow
|
||||
import com.follow.clash.common.toPendingIntent
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.service.R
|
||||
import com.follow.clash.service.State
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.getSpeedTrafficText
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.zip
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NotificationModule(private val service: Service) : Module() {
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
override fun onInstall() {
|
||||
State.notificationParamsFlow.value?.let {
|
||||
update(it)
|
||||
}
|
||||
scope.launch {
|
||||
val screenFlow = service.receiveBroadcastFlow {
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
}.map { intent ->
|
||||
intent.action == Intent.ACTION_SCREEN_ON
|
||||
}.onStart {
|
||||
emit(isScreenOn())
|
||||
}
|
||||
|
||||
tickerFlow(1000, 0)
|
||||
.combine(State.notificationParamsFlow.zip(screenFlow) { params, screenOn ->
|
||||
params to screenOn
|
||||
}) { _, (params, screenOn) -> params to screenOn }
|
||||
.filter { (params, screenOn) -> params != null && screenOn }
|
||||
.collect { (params, _) ->
|
||||
update(params!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isScreenOn(): Boolean {
|
||||
val pm = service.getSystemService<PowerManager>()
|
||||
return when (pm != null) {
|
||||
true -> pm.isInteractive
|
||||
false -> true
|
||||
}
|
||||
}
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent().setComponent(Components.MAIN_ACTIVITY)
|
||||
with(
|
||||
NotificationCompat.Builder(
|
||||
service, GlobalState.NOTIFICATION_CHANNEL
|
||||
)
|
||||
) {
|
||||
setSmallIcon(R.drawable.ic)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(intent.toPendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(params: NotificationParams) {
|
||||
val contentText = Core.getSpeedTrafficText(params.onlyStatisticsProxy)
|
||||
service.startForeground(
|
||||
with(notificationBuilder) {
|
||||
setContentTitle(params.title)
|
||||
setContentText(contentText)
|
||||
setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
clearActions()
|
||||
addAction(
|
||||
0,
|
||||
params.stopText,
|
||||
QuickAction.STOP.quickIntent.toPendingIntent
|
||||
).build()
|
||||
})
|
||||
}
|
||||
|
||||
override fun onUninstall() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
service.stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
service.stopForeground(true)
|
||||
}
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.follow.clash.service.modules
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.common.receiveBroadcastFlow
|
||||
import com.follow.clash.core.Core
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class SuspendModule(private val service: Service) : Module() {
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
private fun isScreenOn(): Boolean {
|
||||
val pm = service.getSystemService<PowerManager>()
|
||||
return when (pm != null) {
|
||||
true -> pm.isInteractive
|
||||
false -> true
|
||||
}
|
||||
}
|
||||
|
||||
val isDeviceIdleMode: Boolean
|
||||
get() {
|
||||
return service.getSystemService<PowerManager>()?.isDeviceIdleMode ?: true
|
||||
}
|
||||
|
||||
private fun onUpdate(isScreenOn: Boolean) {
|
||||
if (isScreenOn) {
|
||||
Core.suspended(false)
|
||||
return
|
||||
}
|
||||
Core.suspended(isDeviceIdleMode)
|
||||
}
|
||||
|
||||
override fun onInstall() {
|
||||
scope.launch {
|
||||
val screenFlow = service.receiveBroadcastFlow {
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
}.map { intent ->
|
||||
intent.action == Intent.ACTION_SCREEN_ON
|
||||
}.onStart {
|
||||
emit(isScreenOn())
|
||||
}
|
||||
|
||||
screenFlow.collect {
|
||||
onUpdate(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUninstall() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="240dp"
|
||||
android:height="240dp"
|
||||
android:viewportWidth="240"
|
||||
android:viewportHeight="240"
|
||||
tools:ignore="VectorRaster">
|
||||
<path
|
||||
android:pathData="M48.1,80.89L168.44,11.41c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0l-120.34,69.48c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M78.98,134.37l60.18,-34.74c11.07,-6.39 25.23,-2.59 31.63,8.48h0c6.4,11.07 2.61,25.24 -8.47,31.64l-60.18,34.74c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64h0Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M109.86,187.86h0c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0h0c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
26
android/settings.gradle
Normal file
26
android/settings.gradle
Normal file
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}
|
||||
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||
|
||||
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "$agp_version" apply false
|
||||
id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
@@ -1,29 +0,0 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath = run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.12.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.10" apply false
|
||||
}
|
||||
|
||||
|
||||
include(":app")
|
||||
include(":core")
|
||||
include(":service")
|
||||
include(":common")
|
||||
433
arb/intl_ja.arb
433
arb/intl_ja.arb
@@ -1,433 +0,0 @@
|
||||
{
|
||||
"rule": "ルール",
|
||||
"global": "グローバル",
|
||||
"direct": "ダイレクト",
|
||||
"dashboard": "ダッシュボード",
|
||||
"proxies": "プロキシ",
|
||||
"profile": "プロファイル",
|
||||
"profiles": "プロファイル一覧",
|
||||
"tools": "ツール",
|
||||
"logs": "ログ",
|
||||
"logsDesc": "ログキャプチャ記録",
|
||||
"resources": "リソース",
|
||||
"resourcesDesc": "外部リソース関連情報",
|
||||
"trafficUsage": "トラフィック使用量",
|
||||
"coreInfo": "コア情報",
|
||||
"networkSpeed": "ネットワーク速度",
|
||||
"outboundMode": "アウトバウンドモード",
|
||||
"networkDetection": "ネットワーク検出",
|
||||
"upload": "アップロード",
|
||||
"download": "ダウンロード",
|
||||
"noProxy": "プロキシなし",
|
||||
"noProxyDesc": "プロファイルを作成するか、有効なプロファイルを追加してください",
|
||||
"nullProfileDesc": "プロファイルがありません。追加してください",
|
||||
"settings": "設定",
|
||||
"language": "言語",
|
||||
"defaultText": "デフォルト",
|
||||
"more": "詳細",
|
||||
"other": "その他",
|
||||
"about": "について",
|
||||
"en": "英語",
|
||||
"ja": "日本語",
|
||||
"ru": "ロシア語",
|
||||
"zh_CN": "簡体字中国語",
|
||||
"theme": "テーマ",
|
||||
"themeDesc": "ダークモードの設定、色の調整",
|
||||
"override": "上書き",
|
||||
"overrideDesc": "プロキシ関連設定を上書き",
|
||||
"allowLan": "LANを許可",
|
||||
"allowLanDesc": "LAN経由でのプロキシアクセスを許可",
|
||||
"tun": "TUN",
|
||||
"tunDesc": "管理者モードでのみ有効",
|
||||
"minimizeOnExit": "終了時に最小化",
|
||||
"minimizeOnExitDesc": "システムの終了イベントを変更",
|
||||
"autoLaunch": "自動起動",
|
||||
"autoLaunchDesc": "システムの自動起動に従う",
|
||||
"silentLaunch": "バックグラウンド起動",
|
||||
"silentLaunchDesc": "バックグラウンドで起動",
|
||||
"autoRun": "自動実行",
|
||||
"autoRunDesc": "アプリ起動時に自動実行",
|
||||
"logcat": "ログキャット",
|
||||
"logcatDesc": "無効化するとログエントリを非表示",
|
||||
"autoCheckUpdate": "自動更新チェック",
|
||||
"autoCheckUpdateDesc": "起動時に更新を自動チェック",
|
||||
"accessControl": "アクセス制御",
|
||||
"accessControlDesc": "アプリケーションのプロキシアクセスを設定",
|
||||
"application": "アプリケーション",
|
||||
"applicationDesc": "アプリ関連設定を変更",
|
||||
"edit": "編集",
|
||||
"confirm": "確認",
|
||||
"update": "更新",
|
||||
"add": "追加",
|
||||
"save": "保存",
|
||||
"delete": "削除",
|
||||
"years": "年",
|
||||
"months": "月",
|
||||
"hours": "時間",
|
||||
"days": "日",
|
||||
"minutes": "分",
|
||||
"seconds": "秒",
|
||||
"ago": "前",
|
||||
"just": "たった今",
|
||||
"qrcode": "QRコード",
|
||||
"qrcodeDesc": "QRコードをスキャンしてプロファイルを取得",
|
||||
"url": "URL",
|
||||
"urlDesc": "URL経由でプロファイルを取得",
|
||||
"file": "ファイル",
|
||||
"fileDesc": "プロファイルを直接アップロード",
|
||||
"name": "名前",
|
||||
"profileNameNullValidationDesc": "プロファイル名を入力してください",
|
||||
"profileUrlNullValidationDesc": "プロファイルURLを入力してください",
|
||||
"profileUrlInvalidValidationDesc": "有効なプロファイルURLを入力してください",
|
||||
"autoUpdate": "自動更新",
|
||||
"autoUpdateInterval": "自動更新間隔(分)",
|
||||
"profileAutoUpdateIntervalNullValidationDesc": "自動更新間隔を入力してください",
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc": "有効な間隔形式を入力してください",
|
||||
"themeMode": "テーマモード",
|
||||
"themeColor": "テーマカラー",
|
||||
"preview": "プレビュー",
|
||||
"auto": "自動",
|
||||
"light": "ライト",
|
||||
"dark": "ダーク",
|
||||
"importFromURL": "URLからインポート",
|
||||
"submit": "送信",
|
||||
"doYouWantToPass": "通過させますか?",
|
||||
"create": "作成",
|
||||
"defaultSort": "デフォルト順",
|
||||
"delaySort": "遅延順",
|
||||
"nameSort": "名前順",
|
||||
"pleaseUploadFile": "ファイルをアップロードしてください",
|
||||
"pleaseUploadValidQrcode": "有効なQRコードをアップロードしてください",
|
||||
"blacklistMode": "ブラックリストモード",
|
||||
"whitelistMode": "ホワイトリストモード",
|
||||
"filterSystemApp": "システムアプリを除外",
|
||||
"cancelFilterSystemApp": "システムアプリの除外を解除",
|
||||
"selectAll": "すべて選択",
|
||||
"cancelSelectAll": "全選択解除",
|
||||
"appAccessControl": "アプリアクセス制御",
|
||||
"accessControlAllowDesc": "選択したアプリのみVPNを許可",
|
||||
"accessControlNotAllowDesc": "選択したアプリをVPNから除外",
|
||||
"selected": "選択済み",
|
||||
"unableToUpdateCurrentProfileDesc": "現在のプロファイルを更新できません",
|
||||
"noMoreInfoDesc": "追加情報なし",
|
||||
"profileParseErrorDesc": "プロファイル解析エラー",
|
||||
"proxyPort": "プロキシポート",
|
||||
"proxyPortDesc": "Clashのリスニングポートを設定",
|
||||
"port": "ポート",
|
||||
"logLevel": "ログレベル",
|
||||
"show": "表示",
|
||||
"exit": "終了",
|
||||
"systemProxy": "システムプロキシ",
|
||||
"project": "プロジェクト",
|
||||
"core": "コア",
|
||||
"tabAnimation": "タブアニメーション",
|
||||
"desc": "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。",
|
||||
"startVpn": "VPNを開始中...",
|
||||
"stopVpn": "VPNを停止中...",
|
||||
"discovery": "新しいバージョンを発見",
|
||||
"compatible": "互換モード",
|
||||
"compatibleDesc": "有効化すると一部機能を失いますが、Clashの完全サポートを獲得",
|
||||
"notSelectedTip": "現在のプロキシグループは選択できません",
|
||||
"tip": "ヒント",
|
||||
"backupAndRecovery": "バックアップと復元",
|
||||
"backupAndRecoveryDesc": "WebDAVまたはファイルでデータを同期",
|
||||
"account": "アカウント",
|
||||
"backup": "バックアップ",
|
||||
"recovery": "復元",
|
||||
"recoveryProfiles": "プロファイルのみ復元",
|
||||
"recoveryAll": "全データ復元",
|
||||
"recoverySuccess": "復元成功",
|
||||
"backupSuccess": "バックアップ成功",
|
||||
"noInfo": "情報なし",
|
||||
"pleaseBindWebDAV": "WebDAVをバインドしてください",
|
||||
"bind": "バインド",
|
||||
"connectivity": "接続性:",
|
||||
"webDAVConfiguration": "WebDAV設定",
|
||||
"address": "アドレス",
|
||||
"addressHelp": "WebDAVサーバーアドレス",
|
||||
"addressTip": "有効なWebDAVアドレスを入力",
|
||||
"password": "パスワード",
|
||||
"checkUpdate": "更新を確認",
|
||||
"discoverNewVersion": "新バージョンを発見",
|
||||
"checkUpdateError": "アプリは最新版です",
|
||||
"goDownload": "ダウンロードへ",
|
||||
"unknown": "不明",
|
||||
"geoData": "地域データ",
|
||||
"externalResources": "外部リソース",
|
||||
"checking": "確認中...",
|
||||
"country": "国",
|
||||
"checkError": "確認エラー",
|
||||
"search": "検索",
|
||||
"allowBypass": "アプリがVPNをバイパスすることを許可",
|
||||
"allowBypassDesc": "有効化すると一部アプリがVPNをバイパス",
|
||||
"externalController": "外部コントローラー",
|
||||
"externalControllerDesc": "有効化するとClashコアをポート9090で制御可能",
|
||||
"ipv6Desc": "有効化するとIPv6トラフィックを受信可能",
|
||||
"app": "アプリ",
|
||||
"general": "一般",
|
||||
"vpnSystemProxyDesc": "HTTPプロキシをVpnServiceに接続",
|
||||
"systemProxyDesc": "HTTPプロキシをVpnServiceに接続",
|
||||
"unifiedDelay": "統一遅延",
|
||||
"unifiedDelayDesc": "ハンドシェイクなどの余分な遅延を削除",
|
||||
"tcpConcurrent": "TCP並列処理",
|
||||
"tcpConcurrentDesc": "TCP並列処理を許可",
|
||||
"geodataLoader": "Geo低メモリモード",
|
||||
"geodataLoaderDesc": "有効化するとGeo低メモリローダーを使用",
|
||||
"requests": "リクエスト",
|
||||
"requestsDesc": "最近のリクエスト記録を表示",
|
||||
"findProcessMode": "プロセス検出",
|
||||
"init": "初期化",
|
||||
"infiniteTime": "長期有効",
|
||||
"expirationTime": "有効期限",
|
||||
"connections": "接続",
|
||||
"connectionsDesc": "現在の接続データを表示",
|
||||
"intranetIP": "イントラネットIP",
|
||||
"view": "表示",
|
||||
"cut": "切り取り",
|
||||
"copy": "コピー",
|
||||
"paste": "貼り付け",
|
||||
"testUrl": "URLテスト",
|
||||
"sync": "同期",
|
||||
"exclude": "最近のタスクから非表示",
|
||||
"excludeDesc": "アプリがバックグラウンド時に最近のタスクから非表示",
|
||||
"oneColumn": "1列",
|
||||
"twoColumns": "2列",
|
||||
"threeColumns": "3列",
|
||||
"fourColumns": "4列",
|
||||
"expand": "標準",
|
||||
"shrink": "縮小",
|
||||
"min": "最小化",
|
||||
"tab": "タブ",
|
||||
"list": "リスト",
|
||||
"delay": "遅延",
|
||||
"style": "スタイル",
|
||||
"size": "サイズ",
|
||||
"sort": "並び替え",
|
||||
"columns": "列",
|
||||
"proxiesSetting": "プロキシ設定",
|
||||
"proxyGroup": "プロキシグループ",
|
||||
"go": "移動",
|
||||
"externalLink": "外部リンク",
|
||||
"otherContributors": "その他の貢献者",
|
||||
"autoCloseConnections": "接続を自動閉じる",
|
||||
"autoCloseConnectionsDesc": "ノード変更後に接続を自動閉じる",
|
||||
"onlyStatisticsProxy": "プロキシのみ統計",
|
||||
"onlyStatisticsProxyDesc": "有効化するとプロキシトラフィックのみ統計",
|
||||
"pureBlackMode": "純黒モード",
|
||||
"keepAliveIntervalDesc": "TCPキープアライブ間隔",
|
||||
"entries": " エントリ",
|
||||
"local": "ローカル",
|
||||
"remote": "リモート",
|
||||
"remoteBackupDesc": "WebDAVにデータをバックアップ",
|
||||
"remoteRecoveryDesc": "WebDAVからデータを復元",
|
||||
"localBackupDesc": "ローカルにデータをバックアップ",
|
||||
"localRecoveryDesc": "ファイルからデータを復元",
|
||||
"mode": "モード",
|
||||
"time": "時間",
|
||||
"source": "ソース",
|
||||
"allApps": "全アプリ",
|
||||
"onlyOtherApps": "サードパーティアプリのみ",
|
||||
"action": "アクション",
|
||||
"intelligentSelected": "インテリジェント選択",
|
||||
"clipboardImport": "クリップボードからインポート",
|
||||
"clipboardExport": "クリップボードにエクスポート",
|
||||
"layout": "レイアウト",
|
||||
"tight": "密",
|
||||
"standard": "標準",
|
||||
"loose": "疎",
|
||||
"profilesSort": "プロファイルの並び替え",
|
||||
"start": "開始",
|
||||
"stop": "停止",
|
||||
"appDesc": "アプリ関連設定の処理",
|
||||
"vpnDesc": "VPN関連設定の変更",
|
||||
"dnsDesc": "DNS関連設定の更新",
|
||||
"key": "キー",
|
||||
"value": "値",
|
||||
"hostsDesc": "ホストを追加",
|
||||
"vpnTip": "変更はVPN再起動後に有効",
|
||||
"vpnEnableDesc": "VpnService経由で全システムトラフィックをルーティング",
|
||||
"options": "オプション",
|
||||
"loopback": "ループバック解除ツール",
|
||||
"loopbackDesc": "UWPループバック解除用",
|
||||
"providers": "プロバイダー",
|
||||
"proxyProviders": "プロキシプロバイダー",
|
||||
"ruleProviders": "ルールプロバイダー",
|
||||
"overrideDns": "DNS上書き",
|
||||
"overrideDnsDesc": "有効化するとプロファイルのDNS設定を上書き",
|
||||
"status": "ステータス",
|
||||
"statusDesc": "無効時はシステムDNSを使用",
|
||||
"preferH3Desc": "DOHのHTTP/3を優先使用",
|
||||
"respectRules": "ルール尊重",
|
||||
"respectRulesDesc": "DNS接続がルールに従う(proxy-server-nameserverの設定が必要)",
|
||||
"dnsMode": "DNSモード",
|
||||
"fakeipRange": "Fakeip範囲",
|
||||
"fakeipFilter": "Fakeipフィルター",
|
||||
"defaultNameserver": "デフォルトネームサーバー",
|
||||
"defaultNameserverDesc": "DNSサーバーの解決用",
|
||||
"nameserver": "ネームサーバー",
|
||||
"nameserverDesc": "ドメイン解決用",
|
||||
"useHosts": "ホストを使用",
|
||||
"useSystemHosts": "システムホストを使用",
|
||||
"nameserverPolicy": "ネームサーバーポリシー",
|
||||
"nameserverPolicyDesc": "対応するネームサーバーポリシーを指定",
|
||||
"proxyNameserver": "プロキシネームサーバー",
|
||||
"proxyNameserverDesc": "プロキシノード解決用ドメイン",
|
||||
"fallback": "フォールバック",
|
||||
"fallbackDesc": "通常はオフショアDNSを使用",
|
||||
"fallbackFilter": "フォールバックフィルター",
|
||||
"geoipCode": "GeoIPコード",
|
||||
"ipcidr": "IPCIDR",
|
||||
"domain": "ドメイン",
|
||||
"reset": "リセット",
|
||||
"action_view": "表示/非表示",
|
||||
"action_start": "開始/停止",
|
||||
"action_mode": "モード切替",
|
||||
"action_proxy": "システムプロキシ",
|
||||
"action_tun": "TUN",
|
||||
"disclaimer": "免責事項",
|
||||
"disclaimerDesc": "本ソフトウェアは学習交流や科学研究などの非営利目的でのみ使用されます。商用利用は厳禁です。いかなる商用活動も本ソフトウェアとは無関係です。",
|
||||
"agree": "同意",
|
||||
"hotkeyManagement": "ホットキー管理",
|
||||
"hotkeyManagementDesc": "キーボードでアプリを制御",
|
||||
"pressKeyboard": "キーボードを押してください",
|
||||
"inputCorrectHotkey": "正しいホットキーを入力",
|
||||
"hotkeyConflict": "ホットキー競合",
|
||||
"remove": "削除",
|
||||
"noHotKey": "ホットキーなし",
|
||||
"noNetwork": "ネットワークなし",
|
||||
"ipv6InboundDesc": "IPv6インバウンドを許可",
|
||||
"exportLogs": "ログをエクスポート",
|
||||
"exportSuccess": "エクスポート成功",
|
||||
"iconStyle": "アイコンスタイル",
|
||||
"onlyIcon": "アイコンのみ",
|
||||
"noIcon": "なし",
|
||||
"stackMode": "スタックモード",
|
||||
"network": "ネットワーク",
|
||||
"networkDesc": "ネットワーク関連設定の変更",
|
||||
"bypassDomain": "バイパスドメイン",
|
||||
"bypassDomainDesc": "システムプロキシ有効時のみ適用",
|
||||
"resetTip": "リセットを確定",
|
||||
"regExp": "正規表現",
|
||||
"icon": "アイコン",
|
||||
"iconConfiguration": "アイコン設定",
|
||||
"noData": "データなし",
|
||||
"adminAutoLaunch": "管理者自動起動",
|
||||
"adminAutoLaunchDesc": "管理者モードで起動",
|
||||
"fontFamily": "フォントファミリー",
|
||||
"systemFont": "システムフォント",
|
||||
"toggle": "トグル",
|
||||
"system": "システム",
|
||||
"routeMode": "ルートモード",
|
||||
"routeMode_bypassPrivate": "プライベートルートをバイパス",
|
||||
"routeMode_config": "設定を使用",
|
||||
"routeAddress": "ルートアドレス",
|
||||
"routeAddressDesc": "ルートアドレスを設定",
|
||||
"pleaseInputAdminPassword": "管理者パスワードを入力",
|
||||
"copyEnvVar": "環境変数をコピー",
|
||||
"memoryInfo": "メモリ情報",
|
||||
"cancel": "キャンセル",
|
||||
"fileIsUpdate": "ファイルが変更されました。保存しますか?",
|
||||
"profileHasUpdate": "プロファイルが変更されました。自動更新を無効化しますか?",
|
||||
"hasCacheChange": "変更をキャッシュしますか?",
|
||||
"copySuccess": "コピー成功",
|
||||
"copyLink": "リンクをコピー",
|
||||
"exportFile": "ファイルをエクスポート",
|
||||
"cacheCorrupt": "キャッシュが破損しています。クリアしますか?",
|
||||
"detectionTip": "サードパーティAPIに依存(参考値)",
|
||||
"listen": "リスン",
|
||||
"undo": "元に戻す",
|
||||
"redo": "やり直す",
|
||||
"none": "なし",
|
||||
"basicConfig": "基本設定",
|
||||
"basicConfigDesc": "基本設定をグローバルに変更",
|
||||
"selectedCountTitle": "{count} 項目が選択されています",
|
||||
"addRule": "ルールを追加",
|
||||
"ruleName": "ルール名",
|
||||
"content": "内容",
|
||||
"subRule": "サブルール",
|
||||
"ruleTarget": "ルール対象",
|
||||
"sourceIp": "送信元IP",
|
||||
"noResolve": "IPを解決しない",
|
||||
"getOriginRules": "元のルールを取得",
|
||||
"overrideOriginRules": "元のルールを上書き",
|
||||
"addedOriginRules": "元のルールに追加",
|
||||
"enableOverride": "上書きを有効化",
|
||||
"saveChanges": "変更を保存しますか?",
|
||||
"generalDesc": "一般設定を変更",
|
||||
"findProcessModeDesc": "有効化するとパフォーマンスが若干低下します",
|
||||
"tabAnimationDesc": "モバイル表示でのみ有効",
|
||||
"saveTip": "保存してもよろしいですか?",
|
||||
"colorSchemes": "カラースキーム",
|
||||
"palette": "パレット",
|
||||
"tonalSpotScheme": "トーンスポット",
|
||||
"fidelityScheme": "ハイファイデリティー",
|
||||
"monochromeScheme": "モノクローム",
|
||||
"neutralScheme": "ニュートラル",
|
||||
"vibrantScheme": "ビブラント",
|
||||
"expressiveScheme": "エクスプレッシブ",
|
||||
"contentScheme": "コンテンツテーマ",
|
||||
"rainbowScheme": "レインボー",
|
||||
"fruitSaladScheme": "フルーツサラダ",
|
||||
"developerMode": "デベロッパーモード",
|
||||
"developerModeEnableTip": "デベロッパーモードが有効になりました。",
|
||||
"messageTest": "メッセージテスト",
|
||||
"messageTestTip": "これはメッセージです。",
|
||||
"crashTest": "クラッシュテスト",
|
||||
"clearData": "データを消去",
|
||||
"zoom": "ズーム",
|
||||
"textScale": "テキストスケーリング",
|
||||
"internet": "インターネット",
|
||||
"systemApp": "システムアプリ",
|
||||
"noNetworkApp": "ネットワークなしアプリ",
|
||||
"contactMe": "連絡する",
|
||||
"recoveryStrategy": "リカバリー戦略",
|
||||
"recoveryStrategy_override": "オーバーライド",
|
||||
"recoveryStrategy_compatible": "互換性",
|
||||
"logsTest": "ログテスト",
|
||||
"emptyTip": "{label}は空欄にできません",
|
||||
"urlTip": "{label}はURLである必要があります",
|
||||
"numberTip": "{label}は数字でなければなりません",
|
||||
"interval": "インターバル",
|
||||
"existsTip": "現在の{label}は既に存在しています",
|
||||
"deleteTip": "現在の{label}を削除してもよろしいですか?",
|
||||
"deleteMultipTip": "選択された{label}を削除してもよろしいですか?",
|
||||
"nullTip": "現在{label}はありません",
|
||||
"script": "スクリプト",
|
||||
"color": "カラー",
|
||||
"rename": "リネーム",
|
||||
"unnamed": "無題",
|
||||
"pleaseEnterScriptName": "スクリプト名を入力してください",
|
||||
"overrideInvalidTip": "スクリプトモードでは有効になりません",
|
||||
"mixedPort": "混合ポート",
|
||||
"socksPort": "Socksポート",
|
||||
"redirPort": "Redirポート",
|
||||
"tproxyPort": "Tproxyポート",
|
||||
"portTip": "{label} は 1024 から 49151 の間でなければなりません",
|
||||
"portConflictTip": "別のポートを入力してください",
|
||||
"import": "インポート",
|
||||
"importFile": "ファイルからインポート",
|
||||
"importUrl": "URLからインポート",
|
||||
"autoSetSystemDns": "オートセットシステムDNS",
|
||||
"details": "{label}詳細",
|
||||
"creationTime": "作成時間",
|
||||
"progress": "進捗",
|
||||
"host": "ホスト",
|
||||
"destination": "宛先",
|
||||
"destinationGeoIP": "宛先地理情報",
|
||||
"destinationIPASN": "宛先IP ASN",
|
||||
"specialProxy": "特殊プロキシ",
|
||||
"specialRules": "特殊ルール",
|
||||
"remoteDestination": "リモート宛先",
|
||||
"networkType": "ネットワーク種別",
|
||||
"proxyChains": "プロキシチェーン",
|
||||
"log": "ログ",
|
||||
"connection": "接続",
|
||||
"request": "リクエスト",
|
||||
"connected": "接続済み",
|
||||
"disconnected": "切断済み",
|
||||
"connecting": "接続中...",
|
||||
"restartCoreTip": "コアを再起動してもよろしいですか?",
|
||||
"forceRestartCoreTip": "コアを強制再起動してもよろしいですか?",
|
||||
"dnsHijacking": "DNSハイジャッキング",
|
||||
"coreStatus": "コアステータス"
|
||||
}
|
||||
433
arb/intl_ru.arb
433
arb/intl_ru.arb
@@ -1,433 +0,0 @@
|
||||
{
|
||||
"rule": "Правило",
|
||||
"global": "Глобальный",
|
||||
"direct": "Прямой",
|
||||
"dashboard": "Панель управления",
|
||||
"proxies": "Прокси",
|
||||
"profile": "Профиль",
|
||||
"profiles": "Профили",
|
||||
"tools": "Инструменты",
|
||||
"logs": "Логи",
|
||||
"logsDesc": "Записи захвата логов",
|
||||
"resources": "Ресурсы",
|
||||
"resourcesDesc": "Информация, связанная с внешними ресурсами",
|
||||
"trafficUsage": "Использование трафика",
|
||||
"coreInfo": "Информация о ядре",
|
||||
"networkSpeed": "Скорость сети",
|
||||
"outboundMode": "Режим исходящего трафика",
|
||||
"networkDetection": "Обнаружение сети",
|
||||
"upload": "Загрузка",
|
||||
"download": "Скачивание",
|
||||
"noProxy": "Нет прокси",
|
||||
"noProxyDesc": "Пожалуйста, создайте профиль или добавьте действительный профиль",
|
||||
"nullProfileDesc": "Нет профиля, пожалуйста, добавьте профиль",
|
||||
"settings": "Настройки",
|
||||
"language": "Язык",
|
||||
"defaultText": "По умолчанию",
|
||||
"more": "Еще",
|
||||
"other": "Другое",
|
||||
"about": "О программе",
|
||||
"en": "Английский",
|
||||
"ja": "Японский",
|
||||
"ru": "Русский",
|
||||
"zh_CN": "Упрощенный китайский",
|
||||
"theme": "Тема",
|
||||
"themeDesc": "Установить темный режим, настроить цвет",
|
||||
"override": "Переопределить",
|
||||
"overrideDesc": "Переопределить конфигурацию, связанную с прокси",
|
||||
"allowLan": "Разрешить LAN",
|
||||
"allowLanDesc": "Разрешить доступ к прокси через локальную сеть",
|
||||
"tun": "TUN",
|
||||
"tunDesc": "действительно только в режиме администратора",
|
||||
"minimizeOnExit": "Свернуть при выходе",
|
||||
"minimizeOnExitDesc": "Изменить стандартное событие выхода из системы",
|
||||
"autoLaunch": "Автозапуск",
|
||||
"autoLaunchDesc": "Следовать автозапуску системы",
|
||||
"silentLaunch": "Тихий запуск",
|
||||
"silentLaunchDesc": "Запуск в фоновом режиме",
|
||||
"autoRun": "Автозапуск",
|
||||
"autoRunDesc": "Автоматический запуск при открытии приложения",
|
||||
"logcat": "Logcat",
|
||||
"logcatDesc": "Отключение скроет запись логов",
|
||||
"autoCheckUpdate": "Автопроверка обновлений",
|
||||
"autoCheckUpdateDesc": "Автоматически проверять обновления при запуске приложения",
|
||||
"accessControl": "Контроль доступа",
|
||||
"accessControlDesc": "Настройка доступа приложений к прокси",
|
||||
"application": "Приложение",
|
||||
"applicationDesc": "Изменение настроек, связанных с приложением",
|
||||
"edit": "Редактировать",
|
||||
"confirm": "Подтвердить",
|
||||
"update": "Обновить",
|
||||
"add": "Добавить",
|
||||
"save": "Сохранить",
|
||||
"delete": "Удалить",
|
||||
"years": "Лет",
|
||||
"months": "Месяцев",
|
||||
"hours": "Часов",
|
||||
"days": "Дней",
|
||||
"minutes": "Минут",
|
||||
"seconds": "Секунд",
|
||||
"ago": " назад",
|
||||
"just": "Только что",
|
||||
"qrcode": "QR-код",
|
||||
"qrcodeDesc": "Сканируйте QR-код для получения профиля",
|
||||
"url": "URL",
|
||||
"urlDesc": "Получить профиль через URL",
|
||||
"file": "Файл",
|
||||
"fileDesc": "Прямая загрузка профиля",
|
||||
"name": "Имя",
|
||||
"profileNameNullValidationDesc": "Пожалуйста, введите имя профиля",
|
||||
"profileUrlNullValidationDesc": "Пожалуйста, введите URL профиля",
|
||||
"profileUrlInvalidValidationDesc": "Пожалуйста, введите действительный URL профиля",
|
||||
"autoUpdate": "Автообновление",
|
||||
"autoUpdateInterval": "Интервал автообновления (минуты)",
|
||||
"profileAutoUpdateIntervalNullValidationDesc": "Пожалуйста, введите интервал времени для автообновления",
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc": "Пожалуйста, введите действительный формат интервала времени",
|
||||
"themeMode": "Режим темы",
|
||||
"themeColor": "Цвет темы",
|
||||
"preview": "Предпросмотр",
|
||||
"auto": "Авто",
|
||||
"light": "Светлый",
|
||||
"dark": "Темный",
|
||||
"importFromURL": "Импорт из URL",
|
||||
"submit": "Отправить",
|
||||
"doYouWantToPass": "Вы хотите пропустить",
|
||||
"create": "Создать",
|
||||
"defaultSort": "Сортировка по умолчанию",
|
||||
"delaySort": "Сортировка по задержке",
|
||||
"nameSort": "Сортировка по имени",
|
||||
"pleaseUploadFile": "Пожалуйста, загрузите файл",
|
||||
"pleaseUploadValidQrcode": "Пожалуйста, загрузите действительный QR-код",
|
||||
"blacklistMode": "Режим черного списка",
|
||||
"whitelistMode": "Режим белого списка",
|
||||
"filterSystemApp": "Фильтровать системные приложения",
|
||||
"cancelFilterSystemApp": "Отменить фильтрацию системных приложений",
|
||||
"selectAll": "Выбрать все",
|
||||
"cancelSelectAll": "Отменить выбор всего",
|
||||
"appAccessControl": "Контроль доступа приложений",
|
||||
"accessControlAllowDesc": "Разрешить только выбранным приложениям доступ к VPN",
|
||||
"accessControlNotAllowDesc": "Выбранные приложения будут исключены из VPN",
|
||||
"selected": "Выбрано",
|
||||
"unableToUpdateCurrentProfileDesc": "невозможно обновить текущий профиль",
|
||||
"noMoreInfoDesc": "Нет дополнительной информации",
|
||||
"profileParseErrorDesc": "ошибка разбора профиля",
|
||||
"proxyPort": "Порт прокси",
|
||||
"proxyPortDesc": "Установить порт прослушивания Clash",
|
||||
"port": "Порт",
|
||||
"logLevel": "Уровень логов",
|
||||
"show": "Показать",
|
||||
"exit": "Выход",
|
||||
"systemProxy": "Системный прокси",
|
||||
"project": "Проект",
|
||||
"core": "Ядро",
|
||||
"tabAnimation": "Анимация вкладок",
|
||||
"desc": "Многоплатформенный прокси-клиент на основе ClashMeta, простой и удобный в использовании, с открытым исходным кодом и без рекламы.",
|
||||
"startVpn": "Запуск VPN...",
|
||||
"stopVpn": "Остановка VPN...",
|
||||
"discovery": "Обнаружена новая версия",
|
||||
"compatible": "Режим совместимости",
|
||||
"compatibleDesc": "Включение приведет к потере части функциональности приложения, но обеспечит полную поддержку Clash.",
|
||||
"notSelectedTip": "Текущая группа прокси не может быть выбрана.",
|
||||
"tip": "подсказка",
|
||||
"backupAndRecovery": "Резервное копирование и восстановление",
|
||||
"backupAndRecoveryDesc": "Синхронизация данных через WebDAV или файл",
|
||||
"account": "Аккаунт",
|
||||
"backup": "Резервное копирование",
|
||||
"recovery": "Восстановление",
|
||||
"recoveryProfiles": "Только восстановление профилей",
|
||||
"recoveryAll": "Восстановить все данные",
|
||||
"recoverySuccess": "Восстановление успешно",
|
||||
"backupSuccess": "Резервное копирование успешно",
|
||||
"noInfo": "Нет информации",
|
||||
"pleaseBindWebDAV": "Пожалуйста, привяжите WebDAV",
|
||||
"bind": "Привязать",
|
||||
"connectivity": "Связь:",
|
||||
"webDAVConfiguration": "Конфигурация WebDAV",
|
||||
"address": "Адрес",
|
||||
"addressHelp": "Адрес сервера WebDAV",
|
||||
"addressTip": "Пожалуйста, введите действительный адрес WebDAV",
|
||||
"password": "Пароль",
|
||||
"checkUpdate": "Проверить обновления",
|
||||
"discoverNewVersion": "Обнаружена новая версия",
|
||||
"checkUpdateError": "Текущее приложение уже является последней версией",
|
||||
"goDownload": "Перейти к загрузке",
|
||||
"unknown": "Неизвестно",
|
||||
"geoData": "Геоданные",
|
||||
"externalResources": "Внешние ресурсы",
|
||||
"checking": "Проверка...",
|
||||
"country": "Страна",
|
||||
"checkError": "Ошибка проверки",
|
||||
"search": "Поиск",
|
||||
"allowBypass": "Разрешить приложениям обходить VPN",
|
||||
"allowBypassDesc": "Некоторые приложения могут обходить VPN при включении",
|
||||
"externalController": "Внешний контроллер",
|
||||
"externalControllerDesc": "При включении ядро Clash можно контролировать на порту 9090",
|
||||
"ipv6Desc": "При включении будет возможно получать IPv6 трафик",
|
||||
"app": "Приложение",
|
||||
"general": "Общие",
|
||||
"vpnSystemProxyDesc": "Прикрепить HTTP-прокси к VpnService",
|
||||
"systemProxyDesc": "Прикрепить HTTP-прокси к VpnService",
|
||||
"unifiedDelay": "Унифицированная задержка",
|
||||
"unifiedDelayDesc": "Убрать дополнительные задержки, такие как рукопожатие",
|
||||
"tcpConcurrent": "TCP параллелизм",
|
||||
"tcpConcurrentDesc": "Включение позволит использовать параллелизм TCP",
|
||||
"geodataLoader": "Режим низкого потребления памяти для геоданных",
|
||||
"geodataLoaderDesc": "Включение будет использовать загрузчик геоданных с низким потреблением памяти",
|
||||
"requests": "Запросы",
|
||||
"requestsDesc": "Просмотр последних записей запросов",
|
||||
"findProcessMode": "Режим поиска процесса",
|
||||
"init": "Инициализация",
|
||||
"infiniteTime": "Долгосрочное действие",
|
||||
"expirationTime": "Время истечения",
|
||||
"connections": "Соединения",
|
||||
"connectionsDesc": "Просмотр текущих данных о соединениях",
|
||||
"intranetIP": "Внутренний IP",
|
||||
"view": "Просмотр",
|
||||
"cut": "Вырезать",
|
||||
"copy": "Копировать",
|
||||
"paste": "Вставить",
|
||||
"testUrl": "Тест URL",
|
||||
"sync": "Синхронизация",
|
||||
"exclude": "Скрыть из последних задач",
|
||||
"excludeDesc": "Когда приложение находится в фоновом режиме, оно скрыто из последних задач",
|
||||
"oneColumn": "Один столбец",
|
||||
"twoColumns": "Два столбца",
|
||||
"threeColumns": "Три столбца",
|
||||
"fourColumns": "Четыре столбца",
|
||||
"expand": "Стандартный",
|
||||
"shrink": "Сжать",
|
||||
"min": "Мин",
|
||||
"tab": "Вкладка",
|
||||
"list": "Список",
|
||||
"delay": "Задержка",
|
||||
"style": "Стиль",
|
||||
"size": "Размер",
|
||||
"sort": "Сортировка",
|
||||
"columns": "Столбцы",
|
||||
"proxiesSetting": "Настройка прокси",
|
||||
"proxyGroup": "Группа прокси",
|
||||
"go": "Перейти",
|
||||
"externalLink": "Внешняя ссылка",
|
||||
"otherContributors": "Другие участники",
|
||||
"autoCloseConnections": "Автоматическое закрытие соединений",
|
||||
"autoCloseConnectionsDesc": "Автоматически закрывать соединения после смены узла",
|
||||
"onlyStatisticsProxy": "Только статистика прокси",
|
||||
"onlyStatisticsProxyDesc": "При включении будет учитываться только трафик прокси",
|
||||
"pureBlackMode": "Чисто черный режим",
|
||||
"keepAliveIntervalDesc": "Интервал поддержания TCP-соединения",
|
||||
"entries": " записей",
|
||||
"local": "Локальный",
|
||||
"remote": "Удаленный",
|
||||
"remoteBackupDesc": "Резервное копирование локальных данных на WebDAV",
|
||||
"remoteRecoveryDesc": "Восстановление данных с WebDAV",
|
||||
"localBackupDesc": "Резервное копирование локальных данных на локальный диск",
|
||||
"localRecoveryDesc": "Восстановление данных из файла",
|
||||
"mode": "Режим",
|
||||
"time": "Время",
|
||||
"source": "Источник",
|
||||
"allApps": "Все приложения",
|
||||
"onlyOtherApps": "Только сторонние приложения",
|
||||
"action": "Действие",
|
||||
"intelligentSelected": "Интеллектуальный выбор",
|
||||
"clipboardImport": "Импорт из буфера обмена",
|
||||
"clipboardExport": "Экспорт в буфер обмена",
|
||||
"layout": "Макет",
|
||||
"tight": "Плотный",
|
||||
"standard": "Стандартный",
|
||||
"loose": "Свободный",
|
||||
"profilesSort": "Сортировка профилей",
|
||||
"start": "Старт",
|
||||
"stop": "Стоп",
|
||||
"appDesc": "Обработка настроек, связанных с приложением",
|
||||
"vpnDesc": "Изменение настроек, связанных с VPN",
|
||||
"dnsDesc": "Обновление настроек, связанных с DNS",
|
||||
"key": "Ключ",
|
||||
"value": "Значение",
|
||||
"hostsDesc": "Добавить Hosts",
|
||||
"vpnTip": "Изменения вступят в силу после перезапуска VPN",
|
||||
"vpnEnableDesc": "Автоматически направляет весь системный трафик через VpnService",
|
||||
"options": "Опции",
|
||||
"loopback": "Инструмент разблокировки Loopback",
|
||||
"loopbackDesc": "Используется для разблокировки Loopback UWP",
|
||||
"providers": "Провайдеры",
|
||||
"proxyProviders": "Провайдеры прокси",
|
||||
"ruleProviders": "Провайдеры правил",
|
||||
"overrideDns": "Переопределить DNS",
|
||||
"overrideDnsDesc": "Включение переопределит настройки DNS в профиле",
|
||||
"status": "Статус",
|
||||
"statusDesc": "Системный DNS будет использоваться при выключении",
|
||||
"preferH3Desc": "Приоритетное использование HTTP/3 для DOH",
|
||||
"respectRules": "Соблюдение правил",
|
||||
"respectRulesDesc": "DNS-соединение следует правилам, необходимо настроить proxy-server-nameserver",
|
||||
"dnsMode": "Режим DNS",
|
||||
"fakeipRange": "Диапазон Fakeip",
|
||||
"fakeipFilter": "Фильтр Fakeip",
|
||||
"defaultNameserver": "Сервер имен по умолчанию",
|
||||
"defaultNameserverDesc": "Для разрешения DNS-сервера",
|
||||
"nameserver": "Сервер имен",
|
||||
"nameserverDesc": "Для разрешения домена",
|
||||
"useHosts": "Использовать hosts",
|
||||
"useSystemHosts": "Использовать системные hosts",
|
||||
"nameserverPolicy": "Политика сервера имен",
|
||||
"nameserverPolicyDesc": "Указать соответствующую политику сервера имен",
|
||||
"proxyNameserver": "Прокси-сервер имен",
|
||||
"proxyNameserverDesc": "Домен для разрешения прокси-узлов",
|
||||
"fallback": "Резервный",
|
||||
"fallbackDesc": "Обычно используется оффшорный DNS",
|
||||
"fallbackFilter": "Фильтр резервного DNS",
|
||||
"geoipCode": "Код Geoip",
|
||||
"ipcidr": "IPCIDR",
|
||||
"domain": "Домен",
|
||||
"reset": "Сброс",
|
||||
"action_view": "Показать/Скрыть",
|
||||
"action_start": "Старт/Стоп",
|
||||
"action_mode": "Переключить режим",
|
||||
"action_proxy": "Системный прокси",
|
||||
"action_tun": "TUN",
|
||||
"disclaimer": "Отказ от ответственности",
|
||||
"disclaimerDesc": "Это программное обеспечение используется только в некоммерческих целях, таких как учебные обмены и научные исследования. Запрещено использовать это программное обеспечение в коммерческих целях. Любая коммерческая деятельность, если таковая имеется, не имеет отношения к этому программному обеспечению.",
|
||||
"agree": "Согласен",
|
||||
"hotkeyManagement": "Управление горячими клавишами",
|
||||
"hotkeyManagementDesc": "Использование клавиатуры для управления приложением",
|
||||
"pressKeyboard": "Пожалуйста, нажмите клавишу.",
|
||||
"inputCorrectHotkey": "Пожалуйста, введите правильную горячую клавишу",
|
||||
"hotkeyConflict": "Конфликт горячих клавиш",
|
||||
"remove": "Удалить",
|
||||
"noHotKey": "Нет горячей клавиши",
|
||||
"noNetwork": "Нет сети",
|
||||
"ipv6InboundDesc": "Разрешить входящий IPv6",
|
||||
"exportLogs": "Экспорт логов",
|
||||
"exportSuccess": "Экспорт успешен",
|
||||
"iconStyle": "Стиль иконки",
|
||||
"onlyIcon": "Только иконка",
|
||||
"noIcon": "Нет иконки",
|
||||
"stackMode": "Режим стека",
|
||||
"network": "Сеть",
|
||||
"networkDesc": "Изменение настроек, связанных с сетью",
|
||||
"bypassDomain": "Обход домена",
|
||||
"bypassDomainDesc": "Действует только при включенном системном прокси",
|
||||
"resetTip": "Убедитесь, что хотите сбросить",
|
||||
"regExp": "Регулярное выражение",
|
||||
"icon": "Иконка",
|
||||
"iconConfiguration": "Конфигурация иконки",
|
||||
"noData": "Нет данных",
|
||||
"adminAutoLaunch": "Автозапуск с правами администратора",
|
||||
"adminAutoLaunchDesc": "Запуск с правами администратора при загрузке системы",
|
||||
"fontFamily": "Семейство шрифтов",
|
||||
"systemFont": "Системный шрифт",
|
||||
"toggle": "Переключить",
|
||||
"system": "Система",
|
||||
"routeMode": "Режим маршрутизации",
|
||||
"routeMode_bypassPrivate": "Обход частных адресов маршрутизации",
|
||||
"routeMode_config": "Использовать конфигурацию",
|
||||
"routeAddress": "Адрес маршрутизации",
|
||||
"routeAddressDesc": "Настройка адреса прослушивания маршрутизации",
|
||||
"pleaseInputAdminPassword": "Пожалуйста, введите пароль администратора",
|
||||
"copyEnvVar": "Копирование переменных окружения",
|
||||
"memoryInfo": "Информация о памяти",
|
||||
"cancel": "Отмена",
|
||||
"fileIsUpdate": "Файл был изменен. Хотите сохранить изменения?",
|
||||
"profileHasUpdate": "Профиль был изменен. Хотите отключить автообновление?",
|
||||
"hasCacheChange": "Хотите сохранить изменения в кэше?",
|
||||
"copySuccess": "Копирование успешно",
|
||||
"copyLink": "Копировать ссылку",
|
||||
"exportFile": "Экспорт файла",
|
||||
"cacheCorrupt": "Кэш поврежден. Хотите очистить его?",
|
||||
"detectionTip": "Опирается на сторонний API, только для справки",
|
||||
"listen": "Слушать",
|
||||
"undo": "Отменить",
|
||||
"redo": "Повторить",
|
||||
"none": "Нет",
|
||||
"basicConfig": "Базовая конфигурация",
|
||||
"basicConfigDesc": "Глобальное изменение базовых настроек",
|
||||
"selectedCountTitle": "Выбрано {count} элементов",
|
||||
"addRule": "Добавить правило",
|
||||
"ruleName": "Название правила",
|
||||
"content": "Содержание",
|
||||
"subRule": "Подправило",
|
||||
"ruleTarget": "Цель правила",
|
||||
"sourceIp": "Исходный IP",
|
||||
"noResolve": "Не разрешать IP",
|
||||
"getOriginRules": "Получить оригинальные правила",
|
||||
"overrideOriginRules": "Переопределить оригинальное правило",
|
||||
"addedOriginRules": "Добавить к оригинальным правилам",
|
||||
"enableOverride": "Включить переопределение",
|
||||
"saveChanges": "Сохранить изменения?",
|
||||
"generalDesc": "Изменение общих настроек",
|
||||
"findProcessModeDesc": "При включении возможны небольшие потери производительности",
|
||||
"tabAnimationDesc": "Действительно только в мобильном виде",
|
||||
"saveTip": "Вы уверены, что хотите сохранить?",
|
||||
"colorSchemes": "Цветовые схемы",
|
||||
"palette": "Палитра",
|
||||
"tonalSpotScheme": "Тональный акцент",
|
||||
"fidelityScheme": "Точная передача",
|
||||
"monochromeScheme": "Монохром",
|
||||
"neutralScheme": "Нейтральные",
|
||||
"vibrantScheme": "Яркие",
|
||||
"expressiveScheme": "Экспрессивные",
|
||||
"contentScheme": "Контентная тема",
|
||||
"rainbowScheme": "Радужные",
|
||||
"fruitSaladScheme": "Фруктовый микс",
|
||||
"developerMode": "Режим разработчика",
|
||||
"developerModeEnableTip": "Режим разработчика активирован.",
|
||||
"messageTest": "Тестирование сообщения",
|
||||
"messageTestTip": "Это сообщение.",
|
||||
"crashTest": "Тест на сбои",
|
||||
"clearData": "Очистить данные",
|
||||
"zoom": "Масштаб",
|
||||
"textScale": "Масштабирование текста",
|
||||
"internet": "Интернет",
|
||||
"systemApp": "Системное приложение",
|
||||
"noNetworkApp": "Приложение без сети",
|
||||
"contactMe": "Свяжитесь со мной",
|
||||
"recoveryStrategy": "Стратегия восстановления",
|
||||
"recoveryStrategy_override": "Переопределение",
|
||||
"recoveryStrategy_compatible": "Совместимый",
|
||||
"logsTest": "Тест журналов",
|
||||
"emptyTip": "{label} не может быть пустым",
|
||||
"urlTip": "{label} должен быть URL",
|
||||
"numberTip": "{label} должно быть числом",
|
||||
"interval": "Интервал",
|
||||
"existsTip": "Текущий {label} уже существует",
|
||||
"deleteTip": "Вы уверены, что хотите удалить текущий {label}?",
|
||||
"deleteMultipTip": "Вы уверены, что хотите удалить выбранные {label}?",
|
||||
"nullTip": "Сейчас {label} нет",
|
||||
"script": "Скрипт",
|
||||
"color": "Цвет",
|
||||
"rename": "Переименовать",
|
||||
"unnamed": "Без имени",
|
||||
"pleaseEnterScriptName": "Пожалуйста, введите название скрипта",
|
||||
"overrideInvalidTip": "В скриптовом режиме не действует",
|
||||
"mixedPort": "Смешанный порт",
|
||||
"socksPort": "Socks-порт",
|
||||
"redirPort": "Redir-порт",
|
||||
"tproxyPort": "Tproxy-порт",
|
||||
"portTip": "{label} должен быть числом от 1024 до 49151",
|
||||
"portConflictTip": "Введите другой порт",
|
||||
"import": "Импорт",
|
||||
"importFile": "Импорт из файла",
|
||||
"importUrl": "Импорт по URL",
|
||||
"autoSetSystemDns": "Автоматическая настройка системного DNS",
|
||||
"details": "Детали {}",
|
||||
"creationTime": "Время создания",
|
||||
"progress": "Прогресс",
|
||||
"host": "Хост",
|
||||
"destination": "Назначение",
|
||||
"destinationGeoIP": "Геолокация назначения",
|
||||
"destinationIPASN": "ASN назначения",
|
||||
"specialProxy": "Специальный прокси",
|
||||
"specialRules": "Специальные правила",
|
||||
"remoteDestination": "Удалённое назначение",
|
||||
"networkType": "Тип сети",
|
||||
"proxyChains": "Цепочки прокси",
|
||||
"log": "Журнал",
|
||||
"connection": "Соединение",
|
||||
"request": "Запрос",
|
||||
"connected": "Подключено",
|
||||
"disconnected": "Отключено",
|
||||
"connecting": "Подключение...",
|
||||
"restartCoreTip": "Вы уверены, что хотите перезапустить ядро?",
|
||||
"forceRestartCoreTip": "Вы уверены, что хотите принудительно перезапустить ядро?",
|
||||
"dnsHijacking": "DNS-перехват",
|
||||
"coreStatus": "Основной статус"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user