Compare commits

..

49 Commits

Author SHA1 Message Date
chen08209
513ee9ff7a Fix quickStart change proxy error 2024-06-05 17:59:21 +08:00
chen08209
ddad3e40b9 cache 2024-06-05 16:19:23 +08:00
chen08209
eb9c3232ec Fix core version 2024-06-05 14:29:38 +08:00
chen08209
e8c9c619bf Fix core version 2024-06-05 14:13:04 +08:00
chen08209
17cd0cf1ed Update file_picker
Add resources page

Optimize more detail
2024-06-05 13:42:33 +08:00
chen08209
925c331498 Add access selected sorted 2024-06-03 14:46:16 +08:00
chen08209
0512d692f2 Fix notification duplicate creation issue
Fix AccessControl click issue
2024-06-03 11:43:41 +08:00
chen08209
844ce8ffb0 Fix Linux unable to open 2024-05-31 22:12:06 +08:00
chen08209
d2588596df Update README.md 3 2024-05-31 15:40:47 +08:00
chen08209
0e999e3deb Create LICENSE
(cherry picked from commit 3ef3190785)
2024-05-31 15:11:45 +08:00
chen08209
8109bbf46c Update README.md 2
(cherry picked from commit b1c763fcfa)
2024-05-31 15:11:45 +08:00
chen08209
32ea45e5ea Update README.md
(cherry picked from commit 9452ffa514)
2024-05-31 15:11:44 +08:00
chen08209
ecce17ad75 optimize checkUpdate 2024-05-31 09:59:18 +08:00
chen08209
96738090cf Fix submit error
(cherry picked from commit fd3040283c)
2024-05-31 09:09:05 +08:00
chen08209
4a8ea37142 add WebDAV
add Auto check updates

Optimize more details
2024-05-30 17:06:53 +08:00
chen08209
a5fdb90da5 optimize delayTest 2024-05-17 19:54:26 +08:00
chen08209
f9722cc761 upgrade flutter version
(cherry picked from commit 9a07c785f2)
2024-05-17 09:35:22 +08:00
chen08209
f01fb2ed1d Update kernel
Add import profile via QR code image
2024-05-15 20:19:50 +08:00
chen08209
74f4481071 Add compatibility mode and adapt clash scheme. 2024-05-11 14:08:13 +08:00
chen08209
9018f512ae Reconstruction application proxy logic 2024-05-07 17:59:11 +08:00
chen08209
265fc4a701 Fix Tab destroy error 2024-05-06 19:03:49 +08:00
chen08209
755974fc9e Optimize repeat healthcheck 2024-05-06 17:15:42 +08:00
chen08209
ba8eab4fc9 Optimize Direct mode ui 2024-05-06 15:26:38 +08:00
chen08209
f5cb46710f Optimize Healthcheck 2024-05-06 14:31:20 +08:00
chen08209
6483e80416 Remove proxies position animation, improve performance
Add Telegram Link
2024-05-06 14:31:19 +08:00
chen08209
535e6dc3a5 Update healthcheck policy 2024-05-06 14:31:19 +08:00
chen08209
ad86c20cfb New Check URLTest 2024-05-05 21:40:12 +08:00
chen08209
665330e17a Fix the problem of invalid auto-selection 2024-05-05 16:13:52 +08:00
chen08209
0eb001e717 New Async UpdateConfig 2024-05-05 03:12:45 +08:00
chen08209
a563991d74 add changeProfileDebounce 2024-05-04 21:51:40 +08:00
chen08209
3e2a30008c Update Workflow 2024-05-04 16:50:37 +08:00
chen08209
ff68d573d6 Fix ChangeProfile block 2024-05-04 16:39:21 +08:00
chen08209
3223fca7ba Fix Release Message Error
(cherry picked from commit aef50fe0e3)
2024-05-04 16:38:03 +08:00
chen08209
006f9127fc Update Selector 2
(cherry picked from commit fc0767ed25)
2024-05-04 16:37:59 +08:00
chen08209
a2709e155c Update Version
(cherry picked from commit dbf1724cca)
2024-05-04 16:37:57 +08:00
chen08209
684fa7b58e Fix Proxies Select Error
(cherry picked from commit 909aa4038e)
2024-05-04 16:37:55 +08:00
chen08209
03b4da54b5 Fix the problem that the proxy group is empty in global mode.
(cherry picked from commit 2d0a7d8d46)
2024-05-04 16:37:54 +08:00
chen08209
a904b55d11 Fix the problem that the proxy group is empty in global mode.
(cherry picked from commit ca96cd1d82)
2024-05-04 16:37:54 +08:00
chen08209
d711935e2e Add ProxyProvider2
(cherry picked from commit 91ab1e5dac)
2024-05-04 16:37:53 +08:00
chen08209
98b1496eff Add ProxyProvider
(cherry picked from commit b3a5f74df8)
2024-05-03 21:28:41 +08:00
chen08209
442c32b6eb Update Version 2024-05-03 15:32:12 +08:00
chen08209
949a2aaac3 Update ProxyGroup Sort 2024-05-03 14:31:10 +08:00
chen08209
c77463f337 Fix Android quickStart VpnService some problems 2024-05-02 00:46:42 +08:00
chen08209
00377d6070 Update version 2024-05-01 23:39:21 +08:00
chen08209
f393b4b3e9 Set Android notification low importance 2024-05-01 23:29:32 +08:00
chen08209
75e6cfde15 Add Telegram in README_zh_CN.md
(cherry picked from commit 8a188a37c9)
2024-05-01 21:52:22 +08:00
chen08209
7bfe5617d9 Add Telegram 2024-05-01 21:49:18 +08:00
chen08209
97cc96c243 Fix the issue that VpnService can't be closed correctly in special cases 2024-05-01 21:29:54 +08:00
chen08209
1821ee2f61 Fix the problem that TileService is not destroyed correctly in some cases
Adjust tab animation defaults
2024-05-01 15:13:09 +08:00
493 changed files with 56106 additions and 114297 deletions

View File

@@ -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

View File

@@ -1,4 +0,0 @@
contact_links:
- name: 讨论交流 / Communication
url: https://t.me/+G-veVtwBOl4wODc1
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group

View File

@@ -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

View File

@@ -1,58 +0,0 @@
<div align=center>
[![Release Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/vVERSION/total?style=flat-square&logo=github)](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>

View File

@@ -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/

117
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,117 @@
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
- platform: linux
os: ubuntu-latest
- platform: macos
os: macos-13
steps:
- 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.x'
channel: 'stable'
cache: true
- name: Get Flutter Dependency
run: flutter pub get
- name: Setup
run: |
dart setup.dart ${{ matrix.platform }}
- name: Upload
uses: actions/upload-artifact@v4
with:
name: artifact-${{ matrix.platform }}
path: ./dist
retention-days: 1
overwrite: true
upload-release:
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'

4
.gitignore vendored
View File

@@ -5,11 +5,9 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
@@ -33,7 +31,6 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
/dist/
# Symbolication related
app.*.symbols
@@ -47,7 +44,6 @@ app.*.map.json
/android/app/release
#libclash
/libclash/

4
.gitmodules vendored
View File

@@ -5,6 +5,4 @@
[submodule "plugins/flutter_distributor"]
path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git
branch = FlClash
branch = main

View File

@@ -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

View File

@@ -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

View File

@@ -6,9 +6,13 @@
## FlClash
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](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.
@@ -30,36 +34,11 @@ on Mobile:
💡 Based on Material You Design, [Surfboard](https://github.com/getsurfboard/surfboard)-like UI
☁️ Supports data sync via WebDAV
✨ Support subscription link, Dark mode
## Use
## Contact
### 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.TOGGLE
```
## 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>
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
## Build
@@ -93,7 +72,7 @@ Support the following actions
3. Run build script
```bash
dart .\setup.dart windows --arch <arm64 | amd64>
dart .\setup.dart
```
- linux
@@ -103,7 +82,7 @@ Support the following actions
2. Run build script
```bash
dart .\setup.dart linux --arch <arm64 | amd64>
dart .\setup.dart
```
- macOS
@@ -113,8 +92,11 @@ Support the following actions
2. Run build script
```bash
dart .\setup.dart macos --arch <arm64 | amd64>
dart .\setup.dart
```
## Star

View File

@@ -6,9 +6,13 @@
## FlClash
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](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的多平台代理客户端简单易用开源无广告。
@@ -30,36 +34,11 @@ on Mobile:
💡 基本 Material You 设计, 类[Surfboard](https://github.com/getsurfboard/surfboard)用户界面
☁️ 支持通过WebDAV同步数据
✨ 支持一键导入订阅, 深色模式
## Use
## Contact
### 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.TOGGLE
```
## 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>
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
## Build
@@ -93,7 +72,7 @@ on Mobile:
3. 运行构建脚本
```bash
dart .\setup.dart windows --arch <arm64 | amd64>
dart .\setup.dart
```
- linux
@@ -103,7 +82,7 @@ on Mobile:
2. 运行构建脚本
```bash
dart .\setup.dart linux --arch <arm64 | amd64>
dart .\setup.dart
```
- macOS
@@ -113,7 +92,7 @@ on Mobile:
2. 运行构建脚本
```bash
dart .\setup.dart macos --arch <arm64 | amd64>
dart .\setup.dart
```
## Star History

View File

@@ -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:
exclude:
- lib/l10n/intl/**
errors:
invalid_annotation_target: ignore
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

112
android/app/build.gradle Normal file
View File

@@ -0,0 +1,112 @@
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'
}
afterEvaluate {
assembleDebug.dependsOn copyNativeLibs
assembleRelease.dependsOn copyNativeLibs
}

View File

@@ -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")
}
}

View File

@@ -1,4 +1,2 @@
-keep class com.follow.clash.models.**{ *; }
-keep class com.follow.clash.service.models.**{ *; }
-keep class com.follow.clash.models.**{ *; }

View File

@@ -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>

View File

@@ -1,38 +1,29 @@
<?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" />
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
android:name="android.hardware.camera"
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.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission"/>
<application
android:name=".Application"
android:banner="@mipmap/ic_banner"
android:extractNativeLibs="true"
android:hardwareAccelerated="true"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:label="FlClash">
<activity
android:name="com.follow.clash.MainActivity"
@@ -52,9 +43,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" />
@@ -65,37 +54,26 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="clash" />
<data android:scheme="clashmeta" />
<data android:scheme="flclash" />
<data android:scheme="clash"/>
<data android:scheme="clashmeta"/>
<data android:scheme="flclash"/>
<data android:host="install-config" />
<data android:host="install-config"/>
</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:icon="@drawable/tile_icon"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
@@ -105,41 +83,15 @@
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
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"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS"
android:process=":background">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
<service
android:name=".services.FlClashVpnService"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<meta-data
android:name="flutterEmbedding"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}
}
}

View File

@@ -1,124 +0,0 @@
package com.follow.clash
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.common.GlobalState
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.File
import java.io.FileOutputStream
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
private const val ICON_TTL_DAYS = 1L
suspend fun PackageManager.getPackageIconPath(packageName: String): String =
withContext(Dispatchers.IO) {
val cacheDir = GlobalState.application.cacheDir
val iconDir = File(cacheDir, "icons").apply { mkdirs() }
val pkgInfo = getPackageInfo(packageName, 0)
val lastUpdateTime = pkgInfo.lastUpdateTime
val iconFile = File(iconDir, "${packageName}_${lastUpdateTime}.webp")
if (iconFile.exists() && !isExpired(iconFile)) {
return@withContext iconFile.absolutePath
}
iconDir.listFiles()?.forEach { file ->
if (file.name.startsWith(packageName + "_")) file.delete()
}
return@withContext try {
val icon = getApplicationIcon(packageName)
saveDrawableToFile(icon, iconFile)
iconFile.absolutePath
} catch (_: Exception) {
val defaultIconFile = File(iconDir, "default_icon.webp")
if (!defaultIconFile.exists()) {
saveDrawableToFile(defaultActivityIcon, defaultIconFile)
}
defaultIconFile.absolutePath
}
}
private fun saveDrawableToFile(drawable: Drawable, file: File) {
val bitmap = drawable.toBitmap()
try {
val format = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
Bitmap.CompressFormat.WEBP_LOSSY
}
else -> {
Bitmap.CompressFormat.WEBP
}
}
FileOutputStream(file).use { fos ->
bitmap.compress(format, 90, fos)
}
} finally {
if (!bitmap.isRecycled) bitmap.recycle()
}
}
private fun isExpired(file: File): Boolean {
val now = System.currentTimeMillis()
val age = now - file.lastModified()
return age > TimeUnit.DAYS.toMillis(ICON_TTL_DAYS)
}
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))
}
})
}
}

View File

@@ -1,112 +0,0 @@
package com.follow.clash
import android.database.Cursor
import android.database.MatrixCursor
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract.Document
import android.provider.DocumentsContract.Root
import android.provider.DocumentsProvider
import java.io.File
import java.io.FileNotFoundException
class FilesProvider : DocumentsProvider() {
companion object {
private const val DEFAULT_ROOT_ID = "0"
private val DEFAULT_DOCUMENT_COLUMNS = arrayOf(
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE,
)
private val DEFAULT_ROOT_COLUMNS = arrayOf(
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID
)
}
override fun onCreate(): Boolean {
return true
}
override fun queryRoots(projection: Array<String>?): Cursor {
return MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS).apply {
newRow().apply {
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_SUMMARY, "Data")
add(Root.COLUMN_DOCUMENT_ID, "/")
}
}
}
override fun queryChildDocuments(
parentDocumentId: String,
projection: Array<String>?,
sortOrder: String?
): Cursor {
val result = MatrixCursor(resolveDocumentProjection(projection))
val parentFile = if (parentDocumentId == "/") {
context?.filesDir
} else {
File(parentDocumentId)
} ?: throw FileNotFoundException("Parent directory not found")
parentFile.listFiles()?.forEach { file ->
includeFile(result, file)
}
return result
}
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
val result = MatrixCursor(resolveDocumentProjection(projection))
val file = File(documentId)
includeFile(result, file)
return result
}
override fun openDocument(
documentId: String,
mode: String,
signal: CancellationSignal?
): ParcelFileDescriptor {
val file = File(documentId)
val accessMode = ParcelFileDescriptor.parseMode(mode)
return ParcelFileDescriptor.open(file, accessMode)
}
private fun includeFile(result: MatrixCursor, file: File) {
result.newRow().apply {
add(Document.COLUMN_DOCUMENT_ID, file.absolutePath)
add(Document.COLUMN_DISPLAY_NAME, file.name)
add(Document.COLUMN_SIZE, file.length())
add(
Document.COLUMN_FLAGS,
Document.FLAG_SUPPORTS_WRITE or Document.FLAG_SUPPORTS_DELETE
)
add(Document.COLUMN_MIME_TYPE, getDocumentType(file))
}
}
private fun getDocumentType(file: File): String {
return if (file.isDirectory) {
Document.MIME_TYPE_DIR
} else {
"application/octet-stream"
}
}
private fun resolveDocumentProjection(projection: Array<String>?): Array<String> {
return projection ?: DEFAULT_DOCUMENT_COLUMNS
}
}

View File

@@ -0,0 +1,24 @@
package com.follow.clash
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.engine.FlutterEngine
import java.util.Date
enum class RunState {
START,
PENDING,
STOP
}
class GlobalState {
companion object {
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var runTime: Date? = null
var flutterEngine: FlutterEngine? = null
fun getCurrentTilePlugin(): TilePlugin? =
flutterEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
}
}

View File

@@ -1,37 +1,25 @@
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.ProxyPlugin
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) {
GlobalState.flutterEngine?.destroy()
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(ProxyPlugin())
flutterEngine.plugins.add(TilePlugin())
State.flutterEngine = flutterEngine
GlobalState.flutterEngine = flutterEngine
}
override fun onDestroy() {
State.flutterEngine = null
GlobalState.flutterEngine = null
super.onDestroy()
}
}

View File

@@ -1,78 +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.IMessageInterface
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: ByteArray?, isSuccess: Boolean) -> Unit
) {
delegate.useService {
it.invokeAction(data, object : ICallbackInterface.Stub() {
override fun onResult(result: ByteArray?, isSuccess: Boolean) {
cb(result, isSuccess)
}
})
}
}
suspend fun updateNotificationParams(
params: NotificationParams
) {
delegate.useService {
it.updateNotificationParams(params)
}
}
suspend fun setMessageCallback(
cb: (result: String?) -> Unit
) {
delegate.useService {
it.setMessageCallback(object : IMessageInterface.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() }
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1,31 @@
package com.follow.clash.extensions
import java.net.InetSocketAddress
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64
import androidx.core.graphics.drawable.toBitmap
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
}

View File

@@ -0,0 +1,12 @@
package com.follow.clash.models
enum class AccessControlMode {
acceptSelected,
rejectSelected,
}
data class AccessControl(
val mode: AccessControlMode,
val acceptList: List<String>,
val rejectList: List<String>,
)

View File

@@ -0,0 +1,11 @@
package com.follow.clash.models
data class Metadata(
val network: String,
val sourceIP: String,
val sourcePort: Int,
val destinationIP: String,
val destinationPort: Int,
val remoteDestination: String,
val host: String
)

View File

@@ -3,7 +3,5 @@ package com.follow.clash.models
data class Package(
val packageName: String,
val label: String,
val system: Boolean,
val internet: Boolean,
val lastUpdateTime: Long,
val isSystem:Boolean
)

View File

@@ -1,8 +0,0 @@
package com.follow.clash.models
data class AppState(
val currentProfileName: String,
val stopText: String,
val onlyStatisticsProxy: Boolean,
)

View File

@@ -2,421 +2,182 @@ package com.follow.clash.plugins
import android.Manifest
import android.app.Activity
import android.app.ActivityManager
import android.content.Intent
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.net.VpnService
import android.net.ConnectivityManager
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.getPackageIconPath
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import com.follow.clash.extensions.getBase64
import com.follow.clash.extensions.getProtocol
import com.follow.clash.models.Metadata
import com.follow.clash.models.Package
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.lang.ref.WeakReference
import java.util.zip.ZipFile
import java.net.InetSocketAddress
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 var context: Context? = null
private lateinit var channel: MethodChannel
private lateinit var scope: CoroutineScope
private var vpnPrepareCallback: (suspend () -> Unit)? = null
private var requestNotificationCallback: (() -> Unit)? = null
private var connectivity: ConnectivityManager? = null
private val iconMap = mutableMapOf<String, String?>()
private val packages = mutableListOf<Package>()
private val skipPrefixList = listOf(
"com.google",
"com.android.chrome",
"com.android.vending",
"com.microsoft",
"com.apple",
"com.zhiliaoapp.musically", // Banned by China
)
private val chinaAppPrefixList = listOf(
"com.tencent",
"com.alibaba",
"com.umeng",
"com.qihoo",
"com.ali",
"com.alipay",
"com.amap",
"com.sina",
"com.weibo",
"com.vivo",
"com.xiaomi",
"com.huawei",
"com.taobao",
"com.secneo",
"s.h.e.l.l",
"com.stub",
"com.kiwisec",
"com.secshell",
"com.wrapper",
"cn.securitystack",
"com.mogosec",
"com.secoen",
"com.netease",
"com.mx",
"com.qq.e",
"com.baidu",
"com.bytedance",
"com.bugly",
"com.miui",
"com.oppo",
"com.coloros",
"com.iqoo",
"com.meizu",
"com.gionee",
"cn.nubia",
"com.oplus",
"andes.oplus",
"com.unionpay",
"cn.wps"
)
private val chinaAppRegex by lazy {
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this)
}
private var isBlockNotification: Boolean = false
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: Result) {
private fun tip(message: String?) {
if (toast != null) {
toast!!.cancel()
}
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
toast!!.show()
}
@RequiresApi(Build.VERSION_CODES.Q)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"moveTaskToBack" -> {
activityRef?.get()?.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)
activity?.moveTaskToBack(true)
result.success(true);
}
"getPackages" -> {
scope.launch {
result.success(getPackagesToJson())
}
}
"getChinaPackageNames" -> {
scope.launch {
result.success(getChinaPackageNames())
result.success(getPackages())
}
}
"getPackageIcon" -> {
handleGetPackageIcon(call, result)
scope.launch {
val packageName = call.argument<String>("packageName")
if (packageName != null) {
result.success(getPackageIcon(packageName))
} else {
result.success(null)
}
}
}
"getPackageName" -> {
val data = call.argument<String>("data")
val metadata =
if (data != null) Gson().fromJson(
data,
Metadata::class.java
) else null
val protocol = metadata?.getProtocol()
if (protocol == null) {
result.success(null)
return
}
scope.launch {
withContext(Dispatchers.Default) {
if (context == null) result.success(null)
val source = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val target = InetSocketAddress(
metadata.host.ifEmpty { metadata.destinationIP },
metadata.destinationPort
)
if (connectivity == null) {
connectivity = context!!.getSystemService<ConnectivityManager>()
}
val uid =
connectivity?.getConnectionOwnerUid(protocol, source, target)
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)
}
else -> {
result.notImplemented()
result.notImplemented();
}
}
}
private fun handleGetPackageIcon(call: MethodCall, result: Result) {
scope.launch {
val packageName = call.argument<String>("packageName")
if (packageName == null) {
result.success("")
return@launch
}
val path =
GlobalState.application.packageManager.getPackageIconPath(packageName)
result.success(path)
private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context?.packageManager
if (iconMap[packageName] == null) {
iconMap[packageName] = packageManager?.getApplicationIcon(packageName)?.getBase64()
}
return iconMap[packageName]
}
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 suspend fun getPackages(): String {
return withContext(Dispatchers.Default){
val packageManager = context?.packageManager
val packages: List<Package>? =
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != context?.packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
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 task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activityRef?.get()?.taskId
} else {
it.taskInfo.id == activityRef?.get()?.taskId
}
}
when (value) {
true -> task?.setExcludeFromRecents(value)
false -> task?.setExcludeFromRecents(value)
null -> task?.setExcludeFromRecents(false)
}
}
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"
}?.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
}
private suspend fun getPackagesToJson(): String {
return withContext(Dispatchers.Default) {
Gson().toJson(getPackages())
}
}
private suspend fun getChinaPackageNames(): String {
return withContext(Dispatchers.Default) {
val packages: List<String> =
getPackages().map { it.packageName }.filter { isChinaPackage(it) }
}?.map {
Package(
packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1
)
}
Gson().toJson(packages)
}
}
fun requestNotificationsPermission(callBack: () -> Unit) {
requestNotificationCallback = callBack
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = ContextCompat.checkSelfPermission(
GlobalState.application, Manifest.permission.POST_NOTIFICATIONS
)
if (permission == PackageManager.PERMISSION_GRANTED || isBlockNotification) {
invokeRequestNotificationCallback()
return
}
activityRef?.get()?.let {
ActivityCompat.requestPermissions(
it,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
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
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 {
PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
if (packageName.matches(chinaAppRegex)) {
return true
}
try {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(
packageName, PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
)
} else {
packageManager.getPackageInfo(
packageName, packageManagerFlags
)
}
mutableListOf<ComponentInfo>().apply {
packageInfo.services?.let { addAll(it) }
packageInfo.activities?.let { addAll(it) }
packageInfo.receivers?.let { addAll(it) }
packageInfo.providers?.let { addAll(it) }
}.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
}
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
}
}
}
}
} catch (_: Exception) {
return false
}
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()
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityRef = WeakReference(binding.activity)
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
activity = binding.activity;
scope = CoroutineScope(Dispatchers.Default)
}
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
}
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) {
invokeVpnPrepareCallback()
}
}
return true
}
private fun onRequestPermissionsResultListener(
requestCode: Int, permissions: Array<String>, grantResults: IntArray
): Boolean {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
isBlockNotification = true
}
invokeRequestNotificationCallback()
return true
scope.cancel()
activity = null;
}
}

View File

@@ -0,0 +1,227 @@
package com.follow.clash.plugins
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.IBinder
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.models.AccessControl
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.Date
class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
private lateinit var flutterMethodChannel: MethodChannel
private var activity: Activity? = null
private var context: Context? = null
private var flClashVpnService: FlClashVpnService? = null
private var isBound = false
private var port: Int? = null
private var accessControl: AccessControl? = null
private lateinit var title: String
private lateinit var content: String
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as FlClashVpnService.LocalBinder
flClashVpnService = binder.getService()
port?.let { startVpn(it) }
isBound = true
}
override fun onServiceDisconnected(arg0: ComponentName) {
flClashVpnService = null
isBound = false
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "proxy")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"StartProxy" -> {
port = call.argument<Int>("port")
val args = call.argument<String>("args")
accessControl =
if (args != null) Gson().fromJson(args, AccessControl::class.java) else null
handleStartVpn()
result.success(true)
}
"StopProxy" -> {
stopVpn()
result.success(true)
}
"SetProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null) {
flClashVpnService?.protect(fd)
result.success(true)
} else {
result.success(false)
}
}
"GetRunTimeStamp" -> {
result.success(GlobalState.runTime?.time)
}
"startForeground" -> {
title = call.argument<String>("title") as String
content = call.argument<String>("content") as String
requestNotificationsPermission()
result.success(true)
}
else -> {
result.notImplemented()
}
}
private fun handleStartVpn() {
val intent = VpnService.prepare(context)
if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
} else {
bindService()
}
}
private fun startVpn(port: Int) {
if (GlobalState.runState.value == RunState.START) return;
flClashVpnService?.start(port, accessControl)
GlobalState.runState.value = RunState.START
GlobalState.runTime = Date()
startAfter()
}
private fun stopVpn() {
if (GlobalState.runState.value == RunState.STOP) return
flClashVpnService?.stop()
unbindService()
GlobalState.runState.value = RunState.STOP;
GlobalState.runTime = null;
}
@SuppressLint("ForegroundServiceType")
private fun startForeground() {
if (GlobalState.runState.value != RunState.START) return
flClashVpnService?.startForeground(title, content)
}
private fun requestNotificationsPermission() {
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
val permission = context?.let {
ContextCompat.checkSelfPermission(
it,
Manifest.permission.POST_NOTIFICATIONS
)
}
if (permission == PackageManager.PERMISSION_GRANTED) {
startForeground()
} else {
activity?.let {
ActivityCompat.requestPermissions(
it,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
}
} else {
startForeground()
}
}
private fun startAfter() {
flutterMethodChannel.invokeMethod("startAfter", flClashVpnService?.fd)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
}
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) {
bindService()
} else {
stopVpn()
}
}
return true;
}
private fun onRequestPermissionsResultListener(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
): Boolean {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startForeground()
}
}
return true;
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null;
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
stopVpn()
activity = null
}
private fun bindService() {
val intent = Intent(context, FlClashVpnService::class.java)
context?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
private fun unbindService() {
if (isBound) {
context?.unbindService(connection)
isBound = false
}
}
}

View File

@@ -1,141 +0,0 @@
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.common.formatString
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 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) {
private lateinit var flutterMethodChannel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel = MethodChannel(
flutterPluginBinding.binaryMessenger, "${Components.PACKAGE_NAME}/service"
)
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"init" -> {
handleInit(result)
}
"invokeAction" -> {
handleInvokeAction(call, result)
}
"getRunTime" -> {
handleGetRunTime(result)
}
"syncState" -> {
handleSyncState(call, result)
}
"start" -> {
handleStart(result)
}
"stop" -> {
handleStop(result)
}
else -> {
result.notImplemented()
}
}
private fun handleInvokeAction(call: MethodCall, result: MethodChannel.Result) {
launch {
val data = call.arguments<String>()!!
val res = mutableListOf<ByteArray>()
Service.invokeAction(data) { byteArray, isSuccess ->
res.add(byteArray ?: byteArrayOf())
if (isSuccess) {
result.success(res.formatString())
}
}
}
}
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)
}
}

View File

@@ -1,31 +1,35 @@
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)
}

View File

@@ -0,0 +1,102 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
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
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
@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)
}
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun activityTransfer() {
val intent = Intent(this, TempActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (Build.VERSION.SDK_INT >= 34) {
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
startActivityAndCollapse(pendingIntent)
} else {
startActivityAndCollapse(intent)
}
}
private var flutterEngine: FlutterEngine? = null;
private fun initFlutterEngine() {
flutterEngine = FlutterEngine(this)
flutterEngine?.plugins?.add(ProxyPlugin())
flutterEngine?.plugins?.add(TilePlugin())
flutterEngine?.plugins?.add(AppPlugin())
GlobalState.flutterEngine = flutterEngine
if (flutterEngine?.dartExecutor?.isExecutingDart != true) {
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService"
)
flutterEngine?.dartExecutor?.executeDartEntrypoint(vpnService)
}
}
override fun onClick() {
super.onClick()
activityTransfer()
val currentTilePlugin = GlobalState.getCurrentTilePlugin()
if (GlobalState.runState.value == RunState.STOP) {
GlobalState.runState.value = RunState.PENDING
if(currentTilePlugin == null){
initFlutterEngine()
}else{
currentTilePlugin.handleStart()
}
} else if(GlobalState.runState.value == RunState.START){
GlobalState.runState.value = RunState.PENDING
currentTilePlugin?.handleStop()
}
}
override fun onDestroy() {
GlobalState.runState.removeObserver(observer)
super.onDestroy()
}
}

View File

@@ -0,0 +1,181 @@
package com.follow.clash.services
import android.annotation.SuppressLint
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.net.ProxyInfo
import android.net.VpnService
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
import androidx.core.graphics.drawable.IconCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.models.AccessControl
import com.follow.clash.models.AccessControlMode
@SuppressLint("WrongConstant")
class FlClashVpnService : VpnService() {
private val CHANNEL = "FlClash"
var fd: Int? = null
private val notificationId: Int = 1
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 onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
fun start(port: Int, accessControl: AccessControl?) {
fd = with(Builder()) {
addAddress("172.16.0.1", 30)
setMtu(9000)
addRoute("0.0.0.0", 0)
if (accessControl != null) {
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)
}
allowBypass()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setHttpProxy(
ProxyInfo.buildDirectProxy(
"127.0.0.1",
port,
passList
)
)
}
establish()?.detachFd()
}
}
fun stop() {
stopSelf()
stopForeground()
}
private val notificationBuilder by lazy {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
val icon = IconCompat.createWithResource(this, this.applicationInfo.icon)
with(NotificationCompat.Builder(this, CHANNEL)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setSmallIcon(icon)
}
setContentTitle("FlClash")
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_LOW
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
}
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
val manager = getSystemService(NotificationManager::class.java)
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)
}
}
private fun stopForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(Service.STOP_FOREGROUND_REMOVE)
}
}
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): FlClashVpnService = this@FlClashVpnService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
GlobalState.getCurrentTilePlugin()?.handleStop();
return super.onUnbind(intent)
}
override fun onDestroy() {
stop()
super.onDestroy()
}
}

View File

@@ -1,25 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="240"
android:viewportHeight="240">
<group android:scaleX="0.924"
android:scaleY="0.924"
android:translateX="9.12"
android:translateY="9.12">
<group android:scaleX="0.63461536"
android:scaleY="0.63461536"
android:translateX="45.96154"
android:translateY="43.846153">
<path
android:pathData="M60.65,89.6L154.18,35.6A18,18 107.59,0 1,178.77 42.19L178.77,42.19A18,18 107.59,0 1,172.18 66.78L78.65,120.78A18,18 106.67,0 1,54.06 114.19L54.06,114.19A18,18 106.67,0 1,60.65 89.6z"
android:fillColor="#6666FB"/>
<path
android:pathData="M84.65,131.17L131.42,104.17A18,18 107.83,0 1,156 110.76L156,110.76A18,18 107.83,0 1,149.42 135.35L102.65,162.35A18,18 106.67,0 1,78.06 155.76L78.06,155.76A18,18 106.67,0 1,84.65 131.17z"
android:fillColor="#336AB6"/>
<path
android:pathData="M108.65,172.74L108.65,172.74A18,18 116.03,0 1,133.24 179.33L133.24,179.33A18,18 116.03,0 1,126.65 203.92L126.65,203.92A18,18 116.03,0 1,102.06 197.33L102.06,197.33A18,18 116.03,0 1,108.65 172.74z"
android:fillColor="#5CA8E9"/>
</group>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -6,7 +6,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">#121212</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/ic_launcher_foreground</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@mipmap/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
</style>
</resources>

View File

@@ -4,6 +4,7 @@
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@mipmap/ic_launcher_foreground</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -6,7 +6,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/ic_launcher_foreground</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@mipmap/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
</style>
</resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FAFAFA</color>
<color name="ic_launcher_background">#EFEFEF</color>
</resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="FlClash">FlClash</string>
<string name="fl_clash">FlClash</string>
</resources>

View File

@@ -4,6 +4,7 @@
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@mipmap/ic_launcher_foreground</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -1,6 +0,0 @@
<paths>
<files-path
name="files"
path="."/>
</paths>

View File

@@ -7,8 +7,4 @@
<certificates src="user" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config>

32
android/build.gradle Normal file
View 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
}

View File

@@ -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)
}

View File

@@ -1 +0,0 @@
/build

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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")
}

View File

@@ -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,
}

View File

@@ -1,238 +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 java.nio.charset.Charset
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)
}
}
fun String.chunkedForAidl(charset: Charset = Charsets.UTF_8): List<ByteArray> {
val allBytes = toByteArray(charset)
val total = allBytes.size
val maxBytes = when {
total <= 100 * 1024 -> total
total <= 1024 * 1024 -> 64 * 1024
total <= 10 * 1024 * 1024 -> 128 * 1024
else -> 256 * 1024
}
val result = mutableListOf<ByteArray>()
var index = 0
while (index < total) {
val end = minOf(index + maxBytes, total)
result.add(allBytes.copyOfRange(index, end))
index = end
}
return result
}
fun <T : List<ByteArray>> T.formatString(charset: Charset = Charsets.UTF_8): String {
val totalSize = this.sumOf { it.size }
val combined = ByteArray(totalSize)
var offset = 0
this.forEach { byteArray ->
byteArray.copyInto(combined, offset)
offset += byteArray.size
}
return String(combined, charset)
}

View File

@@ -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
}
}

View File

@@ -1,78 +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
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(
retries: Int = 10,
delayMillis: Long = 200,
crossinline block: (T) -> R
): Result<R> {
return runCatching {
service.filterNotNull()
.retryWhen { _, attempt ->
(attempt < retries).also {
if (it) delay(delayMillis)
}
}.first().let {
block(it)
}
}
}
fun unbind() {
_service.value = null
bindJob?.cancel()
bindJob = null
}
}

View File

@@ -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)
}
}

View File

@@ -1 +0,0 @@
/build

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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 ()

View File

@@ -1,193 +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 stack, jstring address, jstring dns) {
const auto interface = new_global(cb);
scoped_string stackChar = get_string(stack);
scoped_string addressChar = get_string(address);
scoped_string dnsChar = get_string(dns);
startTUN(interface, fd, stackChar, addressChar, dnsChar);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
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 stack, 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

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -1,110 +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,
stack: String,
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,
stack: 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,
)
}
},
stack,
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")
}
}

View File

@@ -1,8 +0,0 @@
package com.follow.clash.core
import androidx.annotation.Keep
@Keep
interface InvokeInterface {
fun onResult(result: String?)
}

View File

@@ -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
}

View File

@@ -1,3 +1,5 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
kotlin_version=1.9.22
agp_version=8.2.1

View File

@@ -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" }

View File

@@ -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

View File

@@ -1 +0,0 @@
/build

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -1,6 +0,0 @@
// ICallbackInterface.aidl
package com.follow.clash.service;
interface ICallbackInterface {
void onResult(in byte[] result, boolean isSuccess);
}

View File

@@ -1,6 +0,0 @@
// IMessageInterface.aidl
package com.follow.clash.service;
interface IMessageInterface {
void onResult(String result);
}

View File

@@ -1,15 +0,0 @@
// IRemoteInterface.aidl
package com.follow.clash.service;
import com.follow.clash.service.ICallbackInterface;
import com.follow.clash.service.IMessageInterface;
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 IMessageInterface messageCallback);
}

View File

@@ -1,4 +0,0 @@
//AccessControl.aidl
package com.follow.clash.service.models;
parcelable AccessControl;

View File

@@ -1,4 +0,0 @@
//NotificationParams.aidl
package com.follow.clash.service.models;
parcelable NotificationParams;

Some files were not shown because too many files have changed in this diff Show More