Compare commits

...

94 Commits

Author SHA1 Message Date
chen08209
672eaccd35 Update changelog 2026-02-02 02:38:00 +00:00
chen08209
2fbb96f5c1 Add sqlite store
Optimize android quick action

Optimize backup and restore

Optimize more details
2026-02-02 10:15:42 +08:00
chen08209
243b3037d9 Update changelog 2025-12-12 06:52:14 +00:00
chen08209
6e404ab19c Fix windows some issues
Optimize overwrite handle

Optimize access control page

Optimize some details
2025-12-12 14:33:03 +08:00
chen08209
2395a4b20c Update changelog 2025-10-08 08:41:35 +00:00
chen08209
d3c3f04062 Fix android tile service
Support append system DNS

Fix some issues
2025-10-08 16:28:02 +08:00
chen08209
201062dc5d Update changelog 2025-09-27 07:31:55 +00:00
chen08209
45b163184d Fix some issues
Optimize Windows service mode

Update core
2025-09-27 15:17:19 +08:00
chen08209
2ab70f193a Update changelog 2025-09-23 07:44:50 +00:00
chen08209
ed7868282a Add android separates the core process
Support core status check and force restart

Optimize proxies page and access page

Update flutter and pub dependencies

Update go version

Optimize more details
2025-09-23 15:23:58 +08:00
chen08209
e956373ef4 Update changelog 2025-07-29 02:57:43 +00:00
chen08209
1154e7b245 Optimize desktop view
Optimize logs, requests, connection pages

Optimize windows tray auto hide

Optimize some details

Update core
2025-07-29 10:43:05 +08:00
chen08209
adb890d763 Update changelog 2025-06-15 10:59:15 +00:00
chen08209
1477f9bd9c Fix windows tun issues
Optimize android get system dns

Optimize more details
2025-06-15 18:44:19 +08:00
chen08209
a06e813249 Update changelog 2025-06-07 16:08:38 +00:00
chen08209
afbc5adb05 Support override script
Support proxies search

Support svg display

Optimize config persistence

Add some scenes auto close connections

Update core

Optimize more details
2025-06-07 23:52:27 +08:00
chen08209
76c9f08d4a Fix issues that TUN repeat failed to open. 2025-05-01 22:12:05 +08:00
chen08209
f83a8e0cce Update changelog 2025-05-01 13:03:36 +00:00
chen08209
f5544f1af7 Fix windows service verify issues 2025-05-01 20:45:23 +08:00
chen08209
eeb543780a Update changelog 2025-04-30 16:20:40 +00:00
chen08209
676f2d058a 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
2025-05-01 00:02:29 +08:00
chen08209
fb9d0cb22c Add issues template 2025-04-19 22:57:07 +08:00
chen08209
e7eb312254 Update changelog 2025-04-18 09:09:33 +00:00
chen08209
c9cd80bcb3 Optimize android vpn performance
Add custom primary color and color scheme

Add linux nad windows arm release

Optimize requests and logs page
2025-04-18 16:54:05 +08:00
chen08209
a77b3a35e8 Fix map input page delete issues 2025-04-13 17:32:01 +08:00
chen08209
2d2708d7bd Update changelog 2025-04-08 07:55:45 +00:00
chen08209
ef5f6dbd59 Add rule override
Update core

Optimize more details
2025-04-08 15:35:14 +08:00
chen08209
b6c7b15e3e Update changelog 2025-03-10 10:53:24 +00:00
chen08209
de9c5ba9cc Optimize dashboard performance
Fix some issues
2025-03-10 18:41:42 +08:00
chen08209
2aae00cf68 Fix unselected proxy group delay issues 2025-03-10 18:41:42 +08:00
chen08209
68be2d34a1 Fix asn url issues 2025-03-08 04:22:32 +08:00
chen08209
7895ccf720 Update changelog 2025-03-07 16:03:59 +00:00
chen08209
e92900dbbd 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
2025-03-07 23:49:27 +08:00
chen08209
eada271c49 Update changelog 2025-03-05 07:22:18 +00:00
chen08209
5dda2854be Fix list form input view issues
Fix traffic view issues
2025-03-05 15:11:19 +08:00
chen08209
5184ed6fc7 Update changelog 2025-03-05 02:36:31 +00:00
chen08209
4e679f776e Optimize performance
Update core

Optimize core stability

Fix linux tun authority check error

Fix some issues
2025-03-05 10:21:51 +08:00
chen08209
96328f66e9 Fix scroll physics error 2025-02-09 16:51:57 +08:00
chen08209
3eb14ab8a1 Update changelog 2025-02-09 08:36:14 +00:00
chen08209
c6266b7917 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
2025-02-09 16:23:40 +08:00
chen08209
6c27f2e2f1 Update changelog 2025-02-03 13:28:20 +00:00
chen08209
e04a0094b1 Fix some issues 2025-02-03 21:15:26 +08:00
chen08209
683e6a58ea Update changelog 2025-02-02 11:48:19 +00:00
chen08209
b340feeb49 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
2025-02-02 19:34:42 +08:00
chen08209
6a39b7ef5a Update changelog 2025-01-10 11:22:18 +00:00
chen08209
35f89fea90 Update core
Fix some issues
2025-01-10 19:08:15 +08:00
chen08209
58acd9c1ab Update changelog 2025-01-09 02:24:30 +00:00
chen08209
ef97ef40a1 Remake dashboard
Optimize theme

Optimize more details

Update flutter version
2025-01-09 10:10:06 +08:00
chen08209
9cb75f4814 Update changelog 2024-12-08 16:50:30 +00:00
chen08209
375c4e0884 Support better window position memory
Add windows arm64 and linux arm64 build script

Optimize some details
2024-12-09 00:37:27 +08:00
chen08209
ece8a48181 Remake desktop
Optimize change proxy

Optimize network check

Fix fallback issues

Optimize lots of details
2024-12-06 20:38:51 +08:00
chen08209
4b32a096dd Update change.yaml 2024-12-06 19:24:11 +08:00
chen08209
04d6a928eb Fix android tile issues 2024-12-06 19:24:11 +08:00
chen08209
22e71ec6e1 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
2024-12-06 19:24:11 +08:00
chen08209
6f1b07e26b Update changelog 2024-12-06 19:24:11 +08:00
chen08209
1e3ab1c717 Update CHANGELOG.md 2024-12-06 19:24:11 +08:00
chen08209
3a43dc2fe9 Add android shortcuts
Fix init params issues

Fix dynamic color issues

Optimize navigator animate

Optimize window init

Optimize fab

Optimize save
2024-12-06 19:24:11 +08:00
chen08209
c94f64cf78 Fix the collapse issues
Add fontFamily options
2024-12-06 19:24:11 +08:00
chen08209
25764fc4d3 Update core version
Update flutter version

Optimize ip check

Optimize url-test
2024-12-06 19:24:11 +08:00
chen08209
649dbb9886 Update release message
Init auto gen changelog
2024-12-06 19:24:11 +08:00
chen08209
e1dd616d74 Fix windows tray issues
Fix urltest issues

Add auto changelog
2024-12-06 19:24:11 +08:00
chen08209
c91d78b63f Fix windows admin auto launch issues
Add android vpn options

Support proxies icon configuration

Optimize android immersion display

Fix some issues
2024-12-06 19:24:11 +08:00
chen08209
82767325e5 Optimize ip detection
Support android vpn ipv6 inbound switch

Support log export

Optimize more details
2024-12-06 19:24:11 +08:00
chen08209
3f0f7f051b Fix android system dns issues
Optimize dns default option

Fix some issues
2024-12-06 19:24:11 +08:00
chen08209
15c64327db Update readme 2024-12-06 19:24:11 +08:00
chen08209
09e35eb5dd Fix build error2 2024-12-06 19:24:11 +08:00
chen08209
77c4935106 Fix build error 2024-12-06 19:24:11 +08:00
chen08209
e6da643186 Support desktop hotkey
Support android ipv6 inbound

Support android system dns

fix some bugs
2024-12-06 19:24:11 +08:00
chen08209
61bd4e4549 Fix delete profile error 2024-12-06 19:24:11 +08:00
chen08209
a5f4d12748 Fix submit error 2 2024-12-06 19:24:11 +08:00
chen08209
9dc67f31a3 Fix submit error 2024-12-06 19:24:11 +08:00
chen08209
846ec9728f Optimize DNS strategy
Fix the problem that the tray is not displayed in some cases

Optimize tray

Update core

Fix some error
2024-12-06 19:24:11 +08:00
chen08209
6e5f1b8e5f Fix tun update issues 2024-12-06 19:24:11 +08:00
chen08209
aca4a3e979 Add DNS override
Fixed some bugs
Optimize more detail
2024-12-06 19:24:11 +08:00
chen08209
3783c3c650 Add Hosts override 2024-12-06 19:24:11 +08:00
chen08209
01aed99002 fix android tip error
fix windows auto launch error
2024-12-06 19:24:11 +08:00
chen08209
9aec1e75e4 Fix windows tray issues
Optimize windows logic
2024-12-06 19:24:11 +08:00
chen08209
5dfb95a22d Optimize app logic
Support windows administrator auto launch

Support android close vpn
2024-12-06 19:24:11 +08:00
chen08209
0f8cfa20b2 Change flutter version 2024-12-06 19:24:11 +08:00
chen08209
3933fd9d9e Support profiles sort
Support windows country flags display

Optimize proxies page and profiles page columns
2024-12-06 19:24:11 +08:00
chen08209
b7400f2ce8 Update flutter version 2024-12-06 19:24:11 +08:00
chen08209
d0ec580932 Update version 2024-12-06 19:24:11 +08:00
chen08209
6bc0be9876 Update timeout time 2024-12-06 19:24:11 +08:00
chen08209
38221bcd10 Update access control page
Fix bug
2024-12-06 19:24:11 +08:00
chen08209
bee2f8aa4f Optimize provider page
Optimize delay test

Support local backup and recovery
2024-12-06 19:24:11 +08:00
chen08209
6cfcaa4edc Fix android tile service issues 2024-12-06 19:24:11 +08:00
chen08209
3fdc69edd5 Fix linux core build error 2024-12-06 19:24:11 +08:00
chen08209
ed7ade0d72 Add proxy-only traffic statistics
Update core

Optimize more details
2024-12-06 19:24:11 +08:00
txyyh
621ddefc65 Add fdroid-repo 2024-12-06 19:24:11 +08:00
chen08209
6de89d7de4 Optimize proxies page
Fix ua issues

Optimize more details
2024-07-25 13:57:37 +08:00
chen08209
c36df8cb4a Fix windows build error 2024-07-22 15:49:59 +08:00
chen08209
530817b268 Update app icon
Fix desktop backup error
2024-07-22 15:05:05 +08:00
chen08209
721dd20251 Optimize request ua
Change android icon

Optimize dashboard
2024-07-20 18:05:49 +08:00
chen08209
f2aa8851ae Remove request validate certificate
Sync core
2024-07-18 21:28:27 +08:00
570 changed files with 137842 additions and 70448 deletions

57
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: 问题反馈 / Bug report
title: "[BUG] "
description: 反馈你遇到的问题 / Report the issue you are experiencing
body:
- type: markdown
attributes:
value: |
## 在提交问题之前,请确认以下事项:
1. 请务必给issue填写一个简洁明了的标题以便他人快速检索
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue否则请在已有的issue下进行讨论
3. 请务必按照模板规范详细描述问题否则issue将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
- type: textarea
id: description
attributes:
label: 问题描述 / Describe the bug
description: 详细清晰地描述你遇到的问题,并配合截图 / Describe the problem you encountered in detail and clearly, and provide screenshots
validations:
required: true
- type: textarea
attributes:
label: 软件版本 / Version
description: 请提供FlClash的具体版本 / Please provide the specific version of FlClash.
validations:
required: true
- type: textarea
attributes:
label: 复现步骤 / To Reproduce
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
validations:
required: true
- type: dropdown
attributes:
label: 操作系统 / OS
options:
- Android
- Windows
- MacOS
- Linux
validations:
required: true
- type: input
attributes:
label: 操作系统版本 / OS Version
description: 请提供你的操作系统版本Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
validations:
required: true
- type: textarea
attributes:
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
description: 请提供完整或相关部分的Debug日志请在“软件左侧菜单”->“设置”->“日志等级”调整到debug / Please provide a complete or relevant Debug log (please adjust it to debug in the left menu of software-> Settings-> Log Level)
validations:
required: true

4
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

View File

@@ -0,0 +1,42 @@
name: 功能请求 / Feature request
title: "[Feature] "
description: 提出你的功能请求 / Propose your feature request
body:
- type: markdown
attributes:
value: |
## 在提交问题之前,请确认以下事项:
1. 请务必给issue填写一个简洁明了的标题以便他人快速检索
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue否则请在已有的issue下进行讨论
3. 请务必按照模板规范详细描述问题否则issue将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
- type: textarea
id: description
attributes:
label: 功能描述 / Feature description
description: 详细清晰地描述你的功能请求 / A clear and concise description of what the feature is
validations:
required: true
- type: textarea
attributes:
label: 使用场景 / Use case
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
validations:
required: true
- type: checkboxes
id: os-labels
attributes:
label: 适用系统 / Target OS
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
options:
- label: Android
- label: Windows
- label: MacOS
- label: Linux
validations:
required: true

58
.github/release_template.md vendored Normal file
View File

@@ -0,0 +1,58 @@
<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>

248
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,248 @@
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-2022
arch: amd64
- platform: linux
os: ubuntu-22.04
arch: amd64
- platform: macos
os: macos-15-intel
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 "${{ secrets.SERVICE_JSON }}" | base64 --decode > android/app/google-services.json
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
if: ${{ !(startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) }}
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.35.7
cache: true
- name: Setup Flutter With Other
if: startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')
uses: subosito/flutter-action@v2
with:
channel: master
flutter-version: 3.35.7
cache: true
- name: Get Flutter Dependency
run: |
flutter --version
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: |
last_ver=$(grep -m1 '^## ' CHANGELOG.md 2>/dev/null | sed 's/^## //')
tags=($(git tag --merged HEAD --sort=-creatordate))
temp="NEW_CHANGELOG.md" > "$temp"
for i in "${!tags[@]}"; do
curr="${tags[i]}"
[[ "$curr" == "$last_ver" ]] && break
prev="${tags[i+1]}"
range="${prev:+$prev..}$curr"
echo -e "## $curr\n" >> "$temp"
git log --no-merges --pretty=format:"%B" "$range" | \
awk '!/Update changelog/ && NF {print "- " $0 "\n"}' >> "$temp"
done
[ -f CHANGELOG.md ] && cat CHANGELOG.md >> "$temp"
mv "$temp" 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 HEAD --sort=-creatordate))
preTag=$(curl -s "https://api.github.com/repos/chen08209/FlClash/releases/latest" | \
sed -nE 's/.*"tag_name": "([^"]+)".*/\1/p')
[ -z "$preTag" ] && preTag=""
out="release.md" > "$out"
for i in "${!tags[@]}"; do
curr="${tags[i]}"
[[ "$curr" == "$preTag" ]] && break
prev="${tags[i+1]}"
range="${prev:+$prev..}$curr"
git log --no-merges --pretty=format:"%B" "$range" | \
awk '!/Update changelog/ && NF {print "- " $0 "\n"}' >> "$out"
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/

View File

@@ -1,143 +0,0 @@
name: build
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- platform: android
os: ubuntu-latest
- platform: windows
os: windows-latest
arch: amd64
- platform: linux
os: ubuntu-latest
arch: amd64
- platform: macos
os: macos-13
arch: amd64
- platform: macos
os: macos-latest
arch: arm64
steps:
- name: Setup Mingw64
if: startsWith(matrix.platform,'windows')
uses: msys2/setup-msys2@v2
with:
msystem: mingw64
install: mingw-w64-x86_64-gcc
update: true
- name: Set Mingw64 Env
if: startsWith(matrix.platform,'windows')
run: |
echo "${{ runner.temp }}\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Check Matrix
run: |
echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}"
gcc --version
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup JAVA
if: startsWith(matrix.platform,'android')
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 17
- name: Setup NDK
if: startsWith(matrix.platform,'android')
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r26b
add-to-path: true
link-to-sdk: true
- name: Setup Android Signing
if: startsWith(matrix.platform,'android')
run: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'core/go.mod'
cache-dependency-path: |
core/go.sum
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
channel: 'stable'
cache: true
- name: Get Flutter Dependency
run: flutter pub get
- name: Setup
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }}
- name: Upload
uses: actions/upload-artifact@v4
with:
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
path: ./dist
retention-days: 1
overwrite: true
upload-release:
if: ${{ !contains(github.ref, '+') }}
permissions: write-all
needs: [ build ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download
uses: actions/download-artifact@v4
with:
path: ./dist/
pattern: artifact-*
merge-multiple: true
- name: Pre Release
run: |
pip install gitchangelog pystache mustache markdown
pre=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
if [ -z "pre" ]; then
echo "init" > release.md
else
current="${{ github.ref_name }}"
echo -e "\n\n<details markdown=1><summary>All changes from $current to the latest commit:</summary>\n\n" >> release.md
gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog"
echo -e "\n\n</details>" >> release.md
fi
- name: Release
uses: softprops/action-gh-release@v2
with:
files: ./dist/*
body_path: './release.md'

29
.gitignore vendored
View File

@@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
@@ -19,7 +21,7 @@ migrate_working_dir/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
@@ -39,15 +41,30 @@ app.*.symbols
# Obfuscation related
app.*.map.json
#AI generated
CLAUDE.md
/.claude
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
/android/**/.cxx
/android/**/build
/android/common/**/.**/
/android/common/local.*
/android/core/**/includes/
/android/core/**/cmake-build-*/
/android/core/**/jniLibs/
#libclash
#FlClash
/libclash/
#jniLibs
/android/app/src/main/jniLibs/
/services/helper/target
/macos/**/Package.resolved
devtools_options.yaml
# FVM Version Cache
.fvm/
.fvmrc

6
.gitmodules vendored
View File

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

View File

@@ -1,11 +1,11 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
# This file should be version controlled and should not be manually edited.
version:
revision: 796c8ef79279f9c774545b3771238c3098dbefab
channel: stable
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: app
@@ -13,26 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: android
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: ios
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: linux
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: macos
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: web
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: windows
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
# User provided section

7
.run/main.dart.run.xml Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="additionalArgs" value="--dart-define-from-file env.json" />
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method v="2" />
</configuration>
</component>

974
CHANGELOG.md Normal file
View File

@@ -0,0 +1,974 @@
## v0.8.92
- Add sqlite store
- Optimize android quick action
- Optimize backup and restore
- Optimize more details
## v0.8.91
- Fix windows some issues
- Optimize overwrite handle
- Optimize access control page
- Optimize some details
## v0.8.90
- Fix android tile service
- Support append system DNS
- Fix some issues
- Update changelog
## v0.8.89
- Fix some issues
- Optimize Windows service mode
- Update core
- Update changelog
## v0.8.88
- Add android separates the core process
- Support core status check and force restart
- Optimize proxies page and access page
- Update flutter and pub dependencies
- Update go version
- Optimize more details
- Update changelog
## 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

@@ -6,13 +6,9 @@
## 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>
[![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)
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
@@ -38,9 +34,32 @@ on Mobile:
✨ Support subscription link, Dark mode
## Contact
## Use
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
### 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>
## Build
@@ -74,7 +93,7 @@ on Mobile:
3. Run build script
```bash
dart .\setup.dart
dart .\setup.dart windows --arch <arm64 | amd64>
```
- linux
@@ -84,7 +103,7 @@ on Mobile:
2. Run build script
```bash
dart .\setup.dart
dart .\setup.dart linux --arch <arm64 | amd64>
```
- macOS
@@ -94,11 +113,8 @@ on Mobile:
2. Run build script
```bash
dart .\setup.dart
dart .\setup.dart macos --arch <arm64 | amd64>
```
## Star

View File

@@ -6,13 +6,9 @@
## 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>
[![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)
基于ClashMeta的多平台代理客户端简单易用开源无广告。
@@ -38,9 +34,32 @@ on Mobile:
✨ 支持一键导入订阅, 深色模式
## Contact
## Use
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
### 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>
## Build
@@ -74,7 +93,7 @@ on Mobile:
3. 运行构建脚本
```bash
dart .\setup.dart
dart .\setup.dart windows --arch <arm64 | amd64>
```
- linux
@@ -84,7 +103,7 @@ on Mobile:
2. 运行构建脚本
```bash
dart .\setup.dart
dart .\setup.dart linux --arch <arm64 | amd64>
```
- macOS
@@ -94,7 +113,7 @@ on Mobile:
2. 运行构建脚本
```bash
dart .\setup.dart
dart .\setup.dart macos --arch <arm64 | amd64>
```
## Star History

View File

@@ -1,29 +1,10 @@
# 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:
# 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
prefer_single_quotes: true

View File

@@ -1,112 +0,0 @@
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

@@ -0,0 +1,109 @@
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")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
}
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
}
}
}
packaging {
jniLibs {
useLegacyPackaging = true
}
}
buildTypes {
debug {
isMinifyEnabled = false
applicationIdSuffix = ".dev"
}
release {
isMinifyEnabled = true
isShrinkResources = true
if (isRelease) {
signingConfig = signingConfigs.getByName("release")
} else {
signingConfig = signingConfigs.getByName("debug")
applicationIdSuffix = ".dev"
}
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")
}
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics.ndk)
implementation(libs.firebase.analytics)
}

View File

@@ -0,0 +1,65 @@
{
"project_info": {
"project_number": "000000000000",
"project_id": "dev"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.follow.clash"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.follow.clash.debug"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.follow.clash.dev"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
]
}

View File

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

View File

@@ -1,13 +1,16 @@
<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:label="FlClash Debug" tools:replace="android:label">
<application
android:icon="@mipmap/ic_launcher"
android:label="FlClash Debug"
tools:replace="android:label">
<service
android:name=".services.FlClashTileService"
android:label="FlClash Debug"
tools:replace="android:label">
</service>
android:name=".TileService"
android:label="FlClash Debug"
tools:replace="android:label" />
</application>
</manifest>

View File

@@ -1,50 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name="${applicationName}"
android:name=".Application"
android:banner="@mipmap/ic_banner"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:extractNativeLibs="true"
android:label="FlClash"
tools:targetApi="n">
android:label="FlClash">
<activity
android:name="com.follow.clash.MainActivity"
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<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" />
@@ -55,27 +56,37 @@
<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:theme="@style/TransparentTheme" />
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>
<service
android:name=".services.FlClashTileService"
android:name=".TileService"
android:exported="true"
android:icon="@drawable/icon"
android:foregroundServiceType="specialUse"
android:icon="@drawable/ic"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
@@ -85,15 +96,17 @@
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
<service
android:name=".services.FlClashVpnService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE">
<receiver
android:name=".BroadcastReceiver"
android:enabled="true"
android:exported="true"
android:permission="${applicationId}.permission.RECEIVE_BROADCASTS">
<intent-filter>
<action android:name="android.net.VpnService" />
<action android:name="${applicationId}.intent.action.SERVICE_CREATED" />
<action android:name="${applicationId}.intent.action.SERVICE_DESTROYED" />
</intent-filter>
</service>
</receiver>
<meta-data
android:name="flutterEmbedding"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,29 @@
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.GlobalState
import com.follow.clash.common.action
import kotlinx.coroutines.launch
class BroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BroadcastAction.SERVICE_CREATED.action -> {
GlobalState.log("Receiver service created")
GlobalState.launch {
State.handleStartServiceAction()
}
}
BroadcastAction.SERVICE_DESTROYED.action -> {
GlobalState.log("Receiver service destroyed")
GlobalState.launch {
State.handleStopServiceAction()
}
}
}
}
}

View File

@@ -0,0 +1,150 @@
package com.follow.clash
import android.app.Application
import android.content.Context.MODE_PRIVATE
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 android.widget.Toast
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.common.GlobalState
import com.follow.clash.models.SharedState
import com.google.gson.Gson
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
val Application.sharedState: SharedState
get() {
try {
val sp = getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE)
val res = sp.getString("flutter.sharedState", "")
return Gson().fromJson(res, SharedState::class.java)
} catch (_: Exception) {
return SharedState()
}
}
private var lastToast: Toast? = null
fun Application.showToast(text: String?) {
Handler(Looper.getMainLooper()).post {
lastToast?.cancel()
lastToast = Toast.makeText(this, text, Toast.LENGTH_LONG).apply {
show()
}
}
}
suspend fun PackageManager.getPackageIconPath(packageName: String): String =
withContext(Dispatchers.IO) {
val cacheDir = GlobalState.application.cacheDir
val iconDir = File(cacheDir, "icons").apply { mkdirs() }
return@withContext try {
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 { f -> f.name.startsWith("${packageName}_") }?.forEach(File::delete)
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 suspend fun saveDrawableToFile(drawable: Drawable, file: File) {
val bitmap = withContext(Dispatchers.Default) {
drawable.toBitmap(width = 128, height = 128)
}
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,63 +0,0 @@
package com.follow.clash
import android.content.Context
import androidx.lifecycle.MutableLiveData
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
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
enum class RunState {
START,
PENDING,
STOP
}
object GlobalState {
private val lock = ReentrantLock()
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null
private var serviceEngine: FlutterEngine? = null
fun getCurrentAppPlugin(): AppPlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
fun getCurrentTitlePlugin(): TilePlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
}
fun destroyServiceEngine() {
serviceEngine?.destroy()
serviceEngine = null
}
fun initServiceEngine(context: Context) {
if (serviceEngine != null) return
lock.withLock {
destroyServiceEngine()
serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(ProxyPlugin())
serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin())
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService"
)
serviceEngine?.dartExecutor?.executeDartEntrypoint(
vpnService,
)
}
}
}

View File

@@ -1,24 +1,37 @@
package com.follow.clash
import android.os.Bundle
import com.follow.clash.common.GlobalState
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin
import com.follow.clash.plugins.ServicePlugin
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() {
class MainActivity : FlutterActivity(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(ProxyPlugin())
flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(TilePlugin())
GlobalState.flutterEngine = flutterEngine
State.flutterEngine = flutterEngine
}
override fun onDestroy() {
GlobalState.flutterEngine = null
GlobalState.launch {
Service.setEventListener(null)
}
State.flutterEngine = null
super.onDestroy()
}
}

View File

@@ -0,0 +1,187 @@
package com.follow.clash
import com.follow.clash.common.GlobalState
import com.follow.clash.common.ServiceDelegate
import com.follow.clash.common.formatString
import com.follow.clash.common.intent
import com.follow.clash.service.IAckInterface
import com.follow.clash.service.ICallbackInterface
import com.follow.clash.service.IEventInterface
import com.follow.clash.service.IRemoteInterface
import com.follow.clash.service.IResultInterface
import com.follow.clash.service.IVoidInterface
import com.follow.clash.service.RemoteService
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object Service {
private val delegate by lazy {
ServiceDelegate<IRemoteInterface>(
RemoteService::class.intent, ::handleServiceDisconnected
) {
IRemoteInterface.Stub.asInterface(it)
}
}
var onServiceDisconnected: ((String) -> Unit)? = null
private fun handleServiceDisconnected(message: String) {
onServiceDisconnected?.let {
it(message)
}
}
fun bind() {
delegate.bind()
}
fun unbind() {
delegate.unbind()
}
suspend fun invokeAction(data: String, cb: ((result: String) -> Unit)?): Result<Unit> {
val res = mutableListOf<ByteArray>()
return delegate.useService {
it.invokeAction(
data, object : ICallbackInterface.Stub() {
override fun onResult(
result: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
) {
res.add(result ?: byteArrayOf())
ack?.onAck()
if (isSuccess) {
cb?.let { cb ->
cb(res.formatString())
}
}
}
})
}
}
suspend fun quickSetup(
initParamsString: String,
setupParamsString: String,
onStarted: (() -> Unit)?,
onResult: ((result: String) -> Unit)?,
): Result<Unit> {
val res = mutableListOf<ByteArray>()
return delegate.useService {
it.quickSetup(
initParamsString,
setupParamsString,
object : ICallbackInterface.Stub() {
override fun onResult(
result: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
) {
res.add(result ?: byteArrayOf())
ack?.onAck()
if (isSuccess) {
onResult?.let { cb ->
cb(res.formatString())
}
}
}
},
object : IVoidInterface.Stub() {
override fun invoke() {
onStarted?.let { onStarted ->
onStarted()
}
}
}
)
}
}
suspend fun setEventListener(
cb: ((result: String?) -> Unit)?
): Result<Unit> {
val results = HashMap<String, MutableList<ByteArray>>()
return delegate.useService {
it.setEventListener(
when (cb != null) {
true -> object : IEventInterface.Stub() {
override fun onEvent(
id: String, data: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
) {
if (results[id] == null) {
results[id] = mutableListOf()
}
results[id]?.add(data ?: byteArrayOf())
ack?.onAck()
if (isSuccess) {
cb(results[id]?.formatString())
results.remove(id)
}
}
}
false -> null
})
}
}
suspend fun updateNotificationParams(
params: NotificationParams
): Result<Unit> {
return delegate.useService {
it.updateNotificationParams(params)
}
}
suspend fun setCrashlytics(
enable: Boolean
): Result<Unit> {
return delegate.useService {
it.setCrashlytics(enable)
}
}
private suspend fun awaitIResultInterface(
block: (IResultInterface) -> Unit
): Long = suspendCancellableCoroutine { continuation ->
val callback = object : IResultInterface.Stub() {
override fun onResult(time: Long) {
if (continuation.isActive) {
continuation.resume(time)
}
}
}
try {
block(callback)
} catch (e: Exception) {
GlobalState.log("awaitIResultInterface $e")
if (continuation.isActive) {
continuation.resumeWithException(e)
}
}
}
suspend fun startService(options: VpnOptions, runTime: Long): Long {
return delegate.useService {
awaitIResultInterface { callback ->
it.startService(options, runTime, callback)
}
}.getOrNull() ?: 0L
}
suspend fun stopService(): Long {
return delegate.useService {
awaitIResultInterface { callback ->
it.stopService(callback)
}
}.getOrNull() ?: 0L
}
suspend fun getRunTime(): Long {
return delegate.useService {
it.runTime
}.getOrNull() ?: 0L
}
}

View File

@@ -0,0 +1,205 @@
package com.follow.clash
import android.net.VpnService
import com.follow.clash.common.GlobalState
import com.follow.clash.models.SharedState
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.service.models.NotificationParams
import com.google.gson.Gson
import io.flutter.embedding.engine.FlutterEngine
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
enum class RunState {
START, PENDING, STOP
}
object State {
val runLock = Mutex()
var runTime: Long = 0
var sharedState: SharedState = SharedState()
val runStateFlow: MutableStateFlow<RunState> = MutableStateFlow(RunState.STOP)
var flutterEngine: FlutterEngine? = null
val appPlugin: AppPlugin?
get() = flutterEngine?.plugin<AppPlugin>()
val tilePlugin: TilePlugin?
get() = flutterEngine?.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 handleSyncState() {
runLock.withLock {
try {
Service.bind()
runTime = Service.getRunTime()
val runState = when (runTime == 0L) {
true -> RunState.STOP
false -> RunState.START
}
runStateFlow.tryEmit(runState)
} catch (_: Exception) {
runStateFlow.tryEmit(RunState.STOP)
}
}
}
suspend fun handleStartServiceAction() {
runLock.withLock {
if (runStateFlow.value != RunState.STOP) {
return
}
tilePlugin?.handleStart()
if (flutterEngine != null) {
return
}
startServiceWithPref()
}
}
suspend fun handleStopServiceAction() {
runLock.withLock {
if (runStateFlow.value != RunState.START) {
return
}
tilePlugin?.handleStop()
if (flutterEngine != null) {
return
}
GlobalState.application.showToast(sharedState.stopTip)
handleStopService()
}
}
fun handleStartService() {
val appPlugin = flutterEngine?.plugin<AppPlugin>()
if (appPlugin != null) {
appPlugin.requestNotificationsPermission {
startService()
}
return
}
startService()
}
private fun startServiceWithPref() {
GlobalState.launch {
runLock.withLock {
if (runStateFlow.value != RunState.STOP) {
return@launch
}
sharedState = GlobalState.application.sharedState
setupAndStart()
}
}
}
suspend fun syncState() {
GlobalState.setCrashlytics(sharedState.crashlytics)
Service.updateNotificationParams(
NotificationParams(
title = sharedState.currentProfileName,
stopText = sharedState.stopText,
onlyStatisticsProxy = sharedState.onlyStatisticsProxy
)
)
Service.setCrashlytics(sharedState.crashlytics)
}
private suspend fun setupAndStart() {
Service.bind()
syncState()
GlobalState.application.showToast(sharedState.startTip)
val initParams = mutableMapOf<String, Any>()
initParams["home-dir"] = GlobalState.application.filesDir.path
initParams["version"] = android.os.Build.VERSION.SDK_INT
val initParamsString = Gson().toJson(initParams)
val setupParamsString = Gson().toJson(sharedState.setupParams)
Service.quickSetup(
initParamsString,
setupParamsString,
onStarted = {
startService()
},
onResult = {
if (it.isNotEmpty()) {
GlobalState.application.showToast(it)
}
},
)
}
private fun startService() {
GlobalState.launch {
runLock.withLock {
if (runStateFlow.value != RunState.STOP) {
return@launch
}
try {
runStateFlow.tryEmit(RunState.PENDING)
val options = sharedState.vpnOptions ?: return@launch
appPlugin?.let {
it.prepare(options.enable) {
runTime = Service.startService(options, runTime)
runStateFlow.tryEmit(RunState.START)
}
} ?: run {
val intent = VpnService.prepare(GlobalState.application)
if (intent != null) {
return@launch
}
runTime = Service.startService(options, runTime)
runStateFlow.tryEmit(RunState.START)
}
} finally {
if (runStateFlow.value == RunState.PENDING) {
runStateFlow.tryEmit(RunState.STOP)
}
}
}
}
}
fun handleStopService() {
GlobalState.launch {
runLock.withLock {
if (runStateFlow.value != RunState.START) {
return@launch
}
try {
runStateFlow.tryEmit(RunState.PENDING)
runTime = Service.stopService()
runStateFlow.tryEmit(RunState.STOP)
} finally {
if (runStateFlow.value == RunState.PENDING) {
runStateFlow.tryEmit(RunState.START)
}
}
}
}
}
}

View File

@@ -2,10 +2,36 @@ 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() {
class TempActivity : Activity(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finishAndRemoveTask()
when (intent.action) {
QuickAction.START.action -> {
launch {
State.handleStartServiceAction()
}
}
QuickAction.STOP.action -> {
launch {
State.handleStopServiceAction()
}
}
QuickAction.TOGGLE.action -> {
launch {
State.handleToggleAction()
}
}
}
finish()
}
}

View File

@@ -0,0 +1,61 @@
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.handleSyncState()
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

@@ -1,37 +0,0 @@
package com.follow.clash.extensions
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 java.net.URL
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.models.Metadata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.net.InetAddress
import java.net.InetSocketAddress
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
}
fun String.getInetSocketAddress(): InetSocketAddress {
val url = URL("https://$this")
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
package com.follow.clash.models
import com.follow.clash.service.models.VpnOptions
import com.google.gson.annotations.SerializedName
data class SharedState(
val startTip: String = "Starting VPN...",
val stopTip: String = "Stopping VPN...",
val crashlytics: Boolean = true,
val currentProfileName: String = "FlClash",
val stopText: String = "Stop",
val onlyStatisticsProxy: Boolean = false,
val vpnOptions: VpnOptions? = null,
val setupParams: SetupParams? = null,
)
data class SetupParams(
@SerializedName("test-url")
val testUrl: String,
@SerializedName("selected-map")
val selectedMap: Map<String, String>,
)

View File

@@ -3,20 +3,29 @@ package com.follow.clash.plugins
import android.Manifest
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.VpnService
import android.os.Build
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.getSystemService
import com.follow.clash.GlobalState
import com.follow.clash.extensions.getBase64
import com.follow.clash.extensions.getProtocol
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 com.follow.clash.models.Package
import com.follow.clash.models.Process
import com.follow.clash.showToast
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
@@ -28,134 +37,118 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.InetSocketAddress
import java.io.File
import java.lang.ref.WeakReference
import java.util.zip.ZipFile
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private var activity: Activity? = null
companion object {
const val VPN_PERMISSION_REQUEST_CODE = 1001
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
}
private var toast: Toast? = null
private var context: Context? = null
private var activityRef: WeakReference<Activity>? = null
private lateinit var channel: MethodChannel
private lateinit var scope: CoroutineScope
private var connectivity: ConnectivityManager? = null
private var vpnPrepareCallback: (suspend () -> Unit)? = null
private val iconMap = mutableMapOf<String, String?>()
private var requestNotificationCallback: (() -> Unit)? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this)
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 onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
scope.cancel()
}
private fun tip(message: String?) {
if(GlobalState.flutterEngine == null){
if (toast != null) {
toast!!.cancel()
}
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
toast!!.show()
}
}
private var isBlockNotification: Boolean = false
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"moveTaskToBack" -> {
activity?.moveTaskToBack(true)
result.success(true);
activityRef?.get()?.moveTaskToBack(true)
result.success(true)
}
"updateExcludeFromRecents" -> {
val value = call.argument<Boolean>("value")
updateExcludeFromRecents(value)
result.success(true);
result.success(true)
}
"initShortcuts" -> {
initShortcuts(call.arguments as String)
result.success(true)
}
"getPackages" -> {
scope.launch {
result.success(getPackages())
result.success(getPackagesToJson())
}
}
"getChinaPackageNames" -> {
scope.launch {
result.success(getChinaPackageNames())
}
}
"getPackageIcon" -> {
scope.launch {
val packageName = call.argument<String>("packageName")
if (packageName == null) {
result.success(null)
return@launch
}
val packageIcon = getPackageIcon(packageName)
packageIcon.let {
if (it != null) {
result.success(it)
return@launch
}
if (iconMap["default"] == null) {
iconMap["default"] =
context?.packageManager?.defaultActivityIcon?.getBase64()
}
result.success(iconMap["default"])
return@launch
}
}
}
"resolverProcess" -> {
val data = call.argument<String>("data")
val process =
if (data != null) Gson().fromJson(
data,
Process::class.java
) else null
val metadata = process?.metadata
val protocol = metadata?.getProtocol()
if (protocol == null) {
result.success(null)
return
}
scope.launch {
withContext(Dispatchers.Default) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
result.success(null)
return@withContext
}
if (context == null) {
result.success(null)
return@withContext
}
if (connectivity == null) {
connectivity = context!!.getSystemService<ConnectivityManager>()
}
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
metadata.destinationIP.ifEmpty { metadata.host },
metadata.destinationPort
)
val uid = try {
connectivity?.getConnectionOwnerUid(protocol, src, dst)
} catch (_: Exception) {
null
}
if (uid == null || uid == -1) {
result.success(null)
return@withContext
}
val packages = context?.packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
handleGetPackageIcon(call, result)
}
"tip" -> {
@@ -165,19 +158,52 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
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 fun initShortcuts(label: String) {
val shortcut = with(ShortcutInfoCompat.Builder(GlobalState.application, "toggle")) {
setShortLabel(label)
setIcon(
IconCompat.createWithResource(
GlobalState.application,
R.mipmap.ic_launcher_round,
)
)
setIntent(QuickAction.TOGGLE.quickIntent)
build()
}
ShortcutManagerCompat.setDynamicShortcuts(
GlobalState.application, listOf(shortcut)
)
}
private fun tip(message: String?) {
GlobalState.application.showToast(message)
}
@Suppress("DEPRECATION")
private fun updateExcludeFromRecents(value: Boolean?) {
if (context == null) return
val am = getSystemService(context!!, ActivityManager::class.java)
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 == activity?.taskId
it.taskInfo.taskId == activityRef?.get()?.taskId
} else {
it.taskInfo.id == activity?.taskId
it.taskInfo.id == activityRef?.get()?.taskId
}
}
@@ -188,57 +214,205 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}
private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context?.packageManager
if (iconMap[packageName] == null) {
iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64()
} catch (_: Exception) {
null
}
}
return iconMap[packageName]
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)) != 0,
lastUpdateTime = it.lastUpdateTime,
internet = it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
)
}?.let { packages.addAll(it) }
return packages
}
private suspend fun getPackages(): String {
private suspend fun getPackagesToJson(): 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"
Gson().toJson(getPackages())
}
}
}?.map {
Package(
packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1
)
}
private suspend fun getChinaPackageNames(): String {
return withContext(Dispatchers.Default) {
val packages: List<String> =
getPackages().map { it.packageName }.filter { isChinaPackage(it) }
Gson().toJson(packages)
}
}
fun requestGc() {
channel.invokeMethod("gc", null)
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) {
activity = binding.activity;
activityRef = WeakReference(binding.activity)
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
activityRef = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity;
activityRef = WeakReference(binding.activity)
}
override fun onDetachedFromActivity() {
channel.invokeMethod("exit", null)
activity = 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
}
}

View File

@@ -1,213 +0,0 @@
package com.follow.clash.plugins
import android.Manifest
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
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.Props
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
class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private lateinit var flutterMethodChannel: MethodChannel
val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
private var activity: Activity? = null
private var context: Context? = null
private var flClashVpnService: FlClashVpnService? = null
private var port: Int = 7890
private var props: Props? = null
private var isBlockNotification: Boolean = false
private var isStart: Boolean = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as FlClashVpnService.LocalBinder
flClashVpnService = binder.getService()
if (isStart) {
startVpn()
} else {
flClashVpnService?.initServiceEngine()
}
}
override fun onServiceDisconnected(arg: ComponentName) {
flClashVpnService = null
}
}
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) {
"initService" -> {
isStart = false
initService()
requestNotificationsPermission()
result.success(true)
}
"startProxy" -> {
isStart = true
port = call.argument<Int>("port")!!
val args = call.argument<String>("args")
props =
if (args != null) Gson().fromJson(args, Props::class.java) else null
startVpn()
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)
}
}
"startForeground" -> {
val title = call.argument<String>("title") as String
val content = call.argument<String>("content") as String
startForeground(title, content)
result.success(true)
}
else -> {
result.notImplemented()
}
}
private fun initService() {
val intent = VpnService.prepare(context)
if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
} else {
if (flClashVpnService != null) {
flClashVpnService!!.initServiceEngine()
} else {
bindService()
}
}
}
private fun startVpn() {
if (flClashVpnService == null) {
bindService()
return
}
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
flutterMethodChannel.invokeMethod("started", flClashVpnService?.start(port, props))
}
private fun stopVpn() {
if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
flClashVpnService?.stop()
GlobalState.destroyServiceEngine()
}
private fun startForeground(title: String, content: String) {
if (GlobalState.runState.value != RunState.START) return
flClashVpnService?.startForeground(title, content)
}
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) {
isBlockNotification = true
}
return false
}
private fun requestNotificationsPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = context?.let {
ContextCompat.checkSelfPermission(
it,
Manifest.permission.POST_NOTIFICATIONS
)
}
if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return
if (activity == null) return
ActivityCompat.requestPermissions(
activity!!,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
}
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
}
private fun bindService() {
val intent = Intent(context, FlClashVpnService::class.java)
context?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}

View File

@@ -0,0 +1,139 @@
package com.follow.clash.plugins
import com.follow.clash.RunState
import com.follow.clash.Service
import com.follow.clash.State
import com.follow.clash.common.Components
import com.follow.clash.invokeMethodOnMainThread
import com.follow.clash.models.SharedState
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)
}
"shutdown" -> {
handleShutdown(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>()!!
Service.invokeAction(data) {
result.success(it)
}
}
}
private fun handleShutdown(result: MethodChannel.Result) {
Service.unbind()
result.success(true)
}
private fun handleStart(result: MethodChannel.Result) {
State.handleStartService()
result.success(true)
}
private fun handleStop(result: MethodChannel.Result) {
State.handleStopService()
result.success(true)
}
val semaphore = Semaphore(10)
fun handleSendEvent(value: String?) {
launch(Dispatchers.Main) {
semaphore.withPermit {
flutterMethodChannel.invokeMethod("event", value)
}
}
}
private fun onServiceDisconnected(message: String) {
State.runStateFlow.tryEmit(RunState.STOP)
flutterMethodChannel.invokeMethodOnMainThread<Any>("crash", message)
}
private fun handleSyncState(call: MethodCall, result: MethodChannel.Result) {
val data = call.arguments<String>()!!
State.sharedState = Gson().fromJson(data, SharedState::class.java)
launch {
State.syncState()
result.success("")
}
}
fun handleInit(result: MethodChannel.Result) {
Service.bind()
launch {
Service.setEventListener {
handleSendEvent(it)
}.onSuccess {
result.success("")
}.onFailure {
result.success(it.message)
}
}
Service.onServiceDisconnected = ::onServiceDisconnected
}
private fun handleGetRunTime(result: MethodChannel.Result) {
launch {
State.handleSyncState()
result.success(State.runTime)
}
}
}

View File

@@ -1,36 +1,31 @@
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(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin,
MethodChannel.MethodCallHandler {
class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
channel =
MethodChannel(flutterPluginBinding.binaryMessenger, "${Components.PACKAGE_NAME}/tile")
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
handleDetached()
channel.setMethodCallHandler(null)
}
fun handleStart() {
onStart?.let { it() }
channel.invokeMethod("start", null)
channel.invokeMethodOnMainThread<Any>("start", null)
}
fun handleStop() {
channel.invokeMethod("stop", null)
onStop?.let { it() }
}
private fun handleDetached() {
channel.invokeMethod("detached", null)
channel.invokeMethodOnMainThread<Any>("stop", null)
}

View File

@@ -1,87 +0,0 @@
package com.follow.clash.services
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.TempActivity
@RequiresApi(Build.VERSION_CODES.N)
class FlClashTileService : TileService() {
private val observer = Observer<RunState> { runState ->
updateTile(runState)
}
private fun updateTile(runState: RunState) {
if (qsTile != null) {
qsTile.state = when (runState) {
RunState.START -> Tile.STATE_ACTIVE
RunState.PENDING -> Tile.STATE_UNAVAILABLE
RunState.STOP -> Tile.STATE_INACTIVE
}
qsTile.updateTile()
}
}
override fun onStartListening() {
super.onStartListening()
GlobalState.runState.value?.let { updateTile(it) }
GlobalState.runState.observeForever(observer)
}
private fun activityTransfer() {
val intent = Intent(this, TempActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent)
} else {
startActivityAndCollapse(intent)
}
}
override fun onClick() {
super.onClick()
activityTransfer()
if (GlobalState.runState.value == RunState.STOP) {
GlobalState.runState.value = RunState.PENDING
val titlePlugin = GlobalState.getCurrentTitlePlugin()
if (titlePlugin != null) {
titlePlugin.handleStart()
} else {
GlobalState.initServiceEngine(applicationContext)
}
} else if (GlobalState.runState.value == RunState.START) {
GlobalState.runState.value = RunState.PENDING
GlobalState.getCurrentTitlePlugin()?.handleStop()
}
}
override fun onDestroy() {
GlobalState.runState.removeObserver(observer)
super.onDestroy()
}
}

View File

@@ -1,205 +0,0 @@
package com.follow.clash.services
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.os.RemoteException
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.Props
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FlClashVpnService : VpnService() {
private val CHANNEL = "FlClash"
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 onCreate() {
super.onCreate()
initServiceEngine()
}
fun start(port: Int, props: Props?): Int? {
return with(Builder()) {
addAddress("172.16.0.1", 30)
setMtu(9000)
addRoute("0.0.0.0", 0)
props?.accessControl?.let { accessControl ->
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
}
AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it)
}
}
}
}
addDnsServer("172.16.0.2")
setSession("FlClash")
setBlocking(false)
if (Build.VERSION.SDK_INT >= 29) {
setMetered(false)
}
if (props?.allowBypass == true) {
allowBypass()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && props?.systemProxy == true) {
setHttpProxy(
ProxyInfo.buildDirectProxy(
"127.0.0.1",
port,
passList
)
)
}
establish()?.detachFd()
}
}
fun stop() {
stopSelf()
stopForeground()
}
private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this, CHANNEL)) {
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_MIN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
}
}
fun initServiceEngine() {
GlobalState.initServiceEngine(applicationContext)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentAppPlugin()?.requestGc()
}
fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
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(STOP_FOREGROUND_REMOVE)
}
}
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): FlClashVpnService = this@FlClashVpnService
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
try {
val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) {
CoroutineScope(Dispatchers.Main).launch {
GlobalState.getCurrentTitlePlugin()?.handleStop()
}
}
return isSuccess
} catch (e: RemoteException) {
throw e
}
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
}
override fun onDestroy() {
stop()
super.onDestroy()
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,25 @@
<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.

Before

Width:  |  Height:  |  Size: 118 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="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/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="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.1 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">@mipmap/ic_launcher_foreground</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
</style>
</resources>

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">@mipmap/ic_launcher_foreground</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/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">#EFEFEF</color>
<color name="ic_launcher_background">#FAFAFA</color>
</resources>

View File

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

View File

@@ -1,32 +0,0 @@
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
}

24
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

1
android/common/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,45 @@
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)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics.ndk)
implementation(libs.firebase.analytics)
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<permission
android:name="${applicationId}.permission.RECEIVE_BROADCASTS"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.RECEIVE_BROADCASTS" />
</manifest>

View File

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,23 @@
package com.follow.clash.common
import com.google.gson.annotations.SerializedName
enum class QuickAction {
STOP,
START,
TOGGLE,
}
enum class BroadcastAction {
SERVICE_CREATED,
SERVICE_DESTROYED,
}
enum class AccessControlMode {
@SerializedName("acceptSelected")
ACCEPT_SELECTED,
@SerializedName("rejectSelected")
REJECT_SELECTED,
}

View File

@@ -0,0 +1,250 @@
package com.follow.clash.common
import android.annotation.SuppressLint
import android.app.ActivityManager
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_SPECIAL_USE
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.RemoteException
import android.util.Log
import androidx.core.content.getSystemService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.withContext
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_SPECIAL_USE)
} else {
startForeground(id, notification)
}
}
val ComponentName.intent: Intent
get() = Intent().apply {
setComponent(this@intent)
setPackage(GlobalState.packageName)
}
val QuickAction.action: String
get() = "${GlobalState.application.packageName}.action.${this.name}"
val QuickAction.quickIntent: Intent
get() = Components.TEMP_ACTIVITY.intent.apply {
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 Context.processName: String?
get() {
val pid = android.os.Process.myPid()
val activityManager = getSystemService<ActivityManager>()
activityManager?.runningAppProcesses?.find { it.pid == pid }?.let {
return it.processName
}
return null
}
val BroadcastAction.quickIntent: Intent
get() = Components.BROADCAST_RECEIVER.intent.apply {
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) }
}
inline fun <reified T : IBinder> Context.bindServiceFlow(
intent: Intent,
flags: Int = Context.BIND_AUTO_CREATE,
maxRetries: Int = 10,
retryDelayMillis: Long = 200L
): Flow<Pair<IBinder?, String>> = callbackFlow {
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
if (binder != null) {
try {
@Suppress("UNCHECKED_CAST") val casted = binder as? T
if (casted != null) {
trySend(Pair(casted, ""))
} else {
trySend(Pair(null, "Binder is not of type ${T::class.java}"))
}
} catch (e: RemoteException) {
trySend(Pair(null, "Failed to link to death: ${e.message}"))
}
} else {
trySend(Pair(null, "Binder empty"))
}
}
override fun onServiceDisconnected(name: ComponentName?) {
trySend(Pair(null, "Service disconnected"))
}
}
val success = withContext(Dispatchers.Main) {
bindService(intent, connection, flags)
}
if (!success) {
throw IllegalStateException("bindService() failed, will retry")
}
awaitClose {
Handler(Looper.getMainLooper()).post {
unbindService(connection)
trySend(Pair(null, ""))
}
}
}.retryWhen { cause, attempt ->
if (attempt < maxRetries && cause is Exception) {
delay(retryDelayMillis)
true
} else {
false
}
}
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
forEach { byteArray ->
byteArray.copyInto(combined, offset)
offset += byteArray.size
}
return String(combined, charset)
}

View File

@@ -0,0 +1,47 @@
package com.follow.clash.common
import android.app.Application
import android.util.Log
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
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 var _application: Application? = null
val application: Application
get() = _application!!
fun log(text: String) {
Log.d("[FlClash]", text)
}
fun init(application: Application) {
_application = application
}
fun setCrashlytics(enable: Boolean) {
_application?.let {
FirebaseApp.initializeApp(it)
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = enable
if (enable) {
log("init crashlytics ${it.processName}")
}
}
}
}

View File

@@ -0,0 +1,78 @@
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.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import java.util.concurrent.atomic.AtomicBoolean
class ServiceDelegate<T>(
private val intent: Intent,
private val onServiceDisconnected: ((String) -> Unit)? = null,
private val interfaceCreator: (IBinder) -> T,
) : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
private val _bindingState = AtomicBoolean(false)
private var _serviceState = MutableStateFlow<Pair<T?, String>?>(null)
val serviceState: StateFlow<Pair<T?, String>?> = _serviceState
private var job: Job? = null
private fun handleBind(data: Pair<IBinder?, String>) {
data.first?.let {
_serviceState.value = Pair(interfaceCreator(it), data.second)
} ?: run {
_serviceState.value = Pair(null, data.second)
unbind()
onServiceDisconnected?.invoke(data.second)
_bindingState.set(false)
}
}
fun bind() {
if (_bindingState.compareAndSet(false, true)) {
job?.cancel()
job = null
_serviceState.value = null
job = launch {
runCatching {
GlobalState.application.bindServiceFlow<IBinder>(intent)
.collect { handleBind(it) }
}
}
}
}
suspend inline fun <R> useService(
timeoutMillis: Long = 5000, crossinline block: suspend (T) -> R
): Result<R> {
return runCatching {
withTimeout(timeoutMillis) {
val state = serviceState.filterNotNull().first()
state.first?.let {
withContext(Dispatchers.Default) {
block(it)
}
} ?: throw Exception(state.second)
}
}
}
fun unbind() {
if (_bindingState.compareAndSet(true, false)) {
job?.cancel()
job = null
_serviceState.value = null
}
}
}

View File

@@ -0,0 +1,14 @@
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,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fl_clash">FlClash</string>
<string name="FlClash">FlClash</string>
</resources>

1
android/core/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,81 @@
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

@@ -0,0 +1,5 @@
<?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

@@ -0,0 +1,35 @@
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 -flto -g0 -fno-exceptions -fno-rtti)
add_link_options(-flto -Wl,--gc-sections,--strip-all)
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

@@ -0,0 +1,208 @@
#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);
startTUN(interface, fd, get_string(stack), get_string(address), get_string(dns));
}
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) {
updateDns(get_string(dns));
}
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);
invokeAction(interface, get_string(data));
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_setEventListener(JNIEnv *env, jobject thiz, jobject cb) {
if (cb != nullptr) {
const auto interface = new_global(cb);
setEventListener(interface);
} else {
setEventListener(nullptr);
}
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_follow_clash_core_Core_getTraffic(JNIEnv *env, jobject thiz,
const jboolean only_statistics_proxy) {
return new_string(getTraffic(only_statistics_proxy));
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_follow_clash_core_Core_getTotalTraffic(JNIEnv *env, jobject thiz,
const jboolean only_statistics_proxy) {
return new_string(getTotalTraffic(only_statistics_proxy));
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean suspended) {
suspend(suspended);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_quickSetup(JNIEnv *env, jobject thiz, jstring init_params_string,
jstring setup_params_string, jobject cb) {
const auto interface = new_global(cb);
quickSetup(interface, get_string(init_params_string), get_string(setup_params_string));
}
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 free_string_impl(char *str) {
free(str);
}
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));
return get_string(packageName);
}
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;
free_string_func = &free_string_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_setEventListener(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) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_quickSetup(JNIEnv *env, jobject thiz, jstring init_params_string,
jstring setup_params_string, jobject cb) {
}
#endif

View File

@@ -0,0 +1,71 @@
#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

@@ -0,0 +1,36 @@
#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

@@ -0,0 +1,136 @@
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 setEventListener(cb: InvokeInterface?)
fun callSetEventListener(
cb: ((result: String?) -> Unit)?
) {
when (cb != null) {
true -> setEventListener(
object : InvokeInterface {
override fun onResult(result: String?) {
cb(result)
}
},
)
false -> setEventListener(null)
}
}
fun quickSetup(
initParamsString: String,
setupParamsString: String,
cb: (result: String?) -> Unit,
) {
quickSetup(
initParamsString,
setupParamsString,
object : InvokeInterface {
override fun onResult(result: String?) {
cb(result)
}
},
)
}
private external fun quickSetup(
initParamsString: String,
setupParamsString: String,
cb: InvokeInterface
)
external fun stopTun()
external fun getTraffic(onlyStatisticsProxy: Boolean): String
external fun getTotalTraffic(onlyStatisticsProxy: Boolean): String
init {
System.loadLibrary("core")
}
}

View File

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

View File

@@ -0,0 +1,9 @@
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,5 +1,3 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
kotlin_version=1.9.22
agp_version=8.2.1

View File

@@ -0,0 +1,28 @@
[versions]
#agp = "8.10.1"
firebaseBom = "34.2.0"
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"
firebaseCrashlyticsKtx = "20.0.1"
firebaseCommonKtx = "22.0.0"
[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" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-crashlytics-ndk = { module = "com.google.firebase:firebase-crashlytics-ndk" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
smali-dexlib2 = { module = "com.android.tools.smali:smali-dexlib2", version.ref = "smaliDexlib2" }
firebase-crashlytics-ktx = { group = "com.google.firebase", name = "firebase-crashlytics-ktx", version.ref = "firebaseCrashlyticsKtx" }
firebase-common-ktx = { group = "com.google.firebase", name = "firebase-common-ktx", version.ref = "firebaseCommonKtx" }

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.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip

1
android/service/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,48 @@
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

@@ -0,0 +1,49 @@
<?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=".VpnService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE"
android:process=":remote">
<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=".CommonService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:process=":remote">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="proxy" />
</service>
<service
android:name=".RemoteService"
android:enabled="true"
android:exported="false"
android:process=":remote" />
<provider
android:name=".FilesProvider"
android:authorities="${applicationId}.files"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS"
android:process=":remote">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,8 @@
// IAckInterface.aidl
package com.follow.clash.service;
import com.follow.clash.service.IAckInterface;
interface IAckInterface {
oneway void onAck();
}

View File

@@ -0,0 +1,8 @@
// ICallbackInterface.aidl
package com.follow.clash.service;
import com.follow.clash.service.IAckInterface;
interface ICallbackInterface {
oneway void onResult(in byte[] data,in boolean isSuccess, in IAckInterface ack);
}

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