初始化提交

This commit is contained in:
Rust 2025-09-23 16:23:15 +08:00
commit 877b18a70f
590 changed files with 53617 additions and 0 deletions

45
.github/workflows/build-windows.yml vendored Executable file
View File

@ -0,0 +1,45 @@
name: Build Windows
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
- name: Enable Windows desktop
run: flutter config --enable-windows-desktop
- name: Get dependencies
run: flutter pub get
- name: Build Windows Debug
run: flutter build windows
- name: Build Windows Release
run: flutter build windows --release
- name: Upload Debug build artifacts
uses: actions/upload-artifact@v3
with:
name: windows-debug-build
path: build/windows/runner/Debug/
- name: Upload Release build artifacts
uses: actions/upload-artifact@v3
with:
name: windows-release-build
path: build/windows/runner/Release/

175
.gitignore vendored Executable file
View File

@ -0,0 +1,175 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
.github/help
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# 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/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Generated files
**/*.g.dart
**/*.freezed.dart
**/*.mapper.dart
**/*.gen.dart
**/*.dll
**/*.dylib
**/*.xcframework
/dist/
# Assets
/assets/core/*
!/assets/core/.gitkeep
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Additional Flutter ignores
**/ios/Flutter/flutter_assets/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral/
**/ios/Flutter/Flutter-Debug.xcconfig
**/ios/Flutter/Flutter-Release.xcconfig
**/ios/Flutter/Flutter-Profile.xcconfig
# macOS specific
**/macos/Flutter/ephemeral/
**/macos/Flutter/Flutter-Debug.xcconfig
**/macos/Flutter/Flutter-Release.xcconfig
**/macos/Flutter/Flutter-Profile.xcconfig
**/macos/Flutter/GeneratedPluginRegistrant.swift
# Windows specific
**/windows/flutter/ephemeral/
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Linux specific
**/linux/flutter/ephemeral/
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
# Web specific
**/web/flutter_service_worker.js
# CocoaPods
**/ios/Pods/
**/macos/Pods/
**/ios/Podfile.lock
**/macos/Podfile.lock
# Gradle
**/android/.gradle/
**/android/gradle/
**/android/local.properties
**/android/key.properties
**/android/upload-keystore.jks
**/android/app/upload-keystore.jks
**/android/app/libs/
# Build outputs
**/build/
**/dist/
**/out/
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
*.log
logs/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Temporary folders
tmp/
temp/
# Data directories
/data

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

View File

@ -0,0 +1,2 @@
#Tue Sep 23 15:58:20 CST 2025
gradle.version=8.10

View File

@ -0,0 +1,2 @@
#Tue Sep 23 15:58:17 CST 2025
java.home=/Users/mac/Applications/Android Studio.app/Contents/jbr/Contents/Home

View File

45
.metadata Executable file
View File

@ -0,0 +1,45 @@
# 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 and should not be manually edited.
version:
revision: "80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
- platform: android
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
- platform: ios
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
- platform: linux
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
- platform: macos
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
- platform: web
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
- platform: windows
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

35
Dockerfile Executable file
View File

@ -0,0 +1,35 @@
FROM ubuntu:latest
# 使用阿里云源
RUN sed -i 's/ports.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list && \
sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list && \
sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list
# 安装必要的依赖
RUN apt-get update && apt-get install -y \
git \
wget \
unzip \
xz-utils \
zip \
libglu1-mesa \
curl \
sudo \
&& rm -rf /var/lib/apt/lists/*
# 创建非root用户
RUN useradd -ms /bin/bash flutter_user
RUN adduser flutter_user sudo
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
# 克隆Flutter并设置权限
RUN git clone https://github.com/flutter/flutter.git /flutter && \
chown -R flutter_user:flutter_user /flutter
# 设置环境变量
ENV PATH="/flutter/bin:${PATH}"
# 切换用户并配置Flutter
USER flutter_user
WORKDIR /home/flutter_user
RUN flutter config --enable-windows-desktop

52
Dockerfile.windows Executable file
View File

@ -0,0 +1,52 @@
FROM ubuntu:22.04
# 设置环境变量
ENV DEBIAN_FRONTEND=noninteractive
ENV FLUTTER_VERSION=3.24.0
ENV FLUTTER_HOME=/flutter
ENV PATH=$PATH:$FLUTTER_HOME/bin
# 安装必要的依赖
RUN apt-get update && apt-get install -y \
curl \
git \
unzip \
xz-utils \
zip \
libglu1-mesa \
cmake \
ninja-build \
pkg-config \
libgtk-3-dev \
liblzma-dev \
libstdc++-12-dev \
mingw-w64 \
gcc-mingw-w64 \
g++-mingw-w64 \
&& rm -rf /var/lib/apt/lists/*
# 下载并安装Flutter
RUN cd /tmp && curl -O https://mirrors-i.tuna.tsinghua.edu.cn/flutter/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz \
&& tar xf flutter_linux_${FLUTTER_VERSION}-stable.tar.xz \
&& ls -la \
&& rm -rf /flutter \
&& mv flutter /flutter \
&& rm flutter_linux_${FLUTTER_VERSION}-stable.tar.xz
# 预下载Flutter依赖
RUN flutter precache
# 修复Git权限问题
RUN git config --global --add safe.directory /flutter
# 设置工作目录
WORKDIR /app
# 复制项目文件
COPY . .
# 获取依赖
RUN flutter pub get
# 构建Windows版本
CMD ["flutter", "build", "windows"]

53
Dockerfile.windows-cross Executable file
View File

@ -0,0 +1,53 @@
FROM ubuntu:22.04
# 设置环境变量
ENV DEBIAN_FRONTEND=noninteractive
ENV FLUTTER_VERSION=3.24.0
ENV FLUTTER_HOME=/flutter
ENV PATH=$PATH:$FLUTTER_HOME/bin
# 安装必要的依赖
RUN apt-get update && apt-get install -y \
curl \
git \
unzip \
xz-utils \
zip \
libglu1-mesa \
cmake \
ninja-build \
pkg-config \
libgtk-3-dev \
liblzma-dev \
libstdc++-12-dev \
mingw-w64 \
gcc-mingw-w64 \
g++-mingw-w64 \
wine \
wine64 \
&& rm -rf /var/lib/apt/lists/*
# 下载并安装Flutter
RUN cd /tmp && curl -O https://mirrors-i.tuna.tsinghua.edu.cn/flutter/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz \
&& tar xf flutter_linux_${FLUTTER_VERSION}-stable.tar.xz \
&& rm -rf /flutter \
&& mv flutter /flutter \
&& rm flutter_linux_${FLUTTER_VERSION}-stable.tar.xz
# 预下载Flutter依赖
RUN flutter precache
# 修复Git权限问题
RUN git config --global --add safe.directory /flutter
# 设置工作目录
WORKDIR /app
# 复制项目文件
COPY . .
# 获取依赖
RUN flutter pub get
# 尝试构建Windows版本使用交叉编译
CMD ["flutter", "build", "windows", "--release"]

62
INSTALLATION_GUIDE.md Normal file
View File

@ -0,0 +1,62 @@
# BearVPN macOS 安装指南
## 🚨 如果遇到"应用程序无法打开"的问题
### 问题原因
macOS 的安全机制Gatekeeper可能会阻止未签名的应用运行。
### 解决方案
#### 方法 1右键打开推荐
1. 右键点击 `BearVPN.app`
2. 选择"打开"
3. 在弹出的对话框中点击"打开"
#### 方法 2系统偏好设置
1. 打开"系统偏好设置" > "安全性与隐私"
2. 在"通用"标签页中,找到被阻止的应用
3. 点击"仍要打开"
#### 方法 3终端命令高级用户
```bash
# 移除隔离属性
sudo xattr -rd com.apple.quarantine /Applications/BearVPN.app
# 或者完全禁用 Gatekeeper不推荐
sudo spctl --master-disable
```
### 验证应用完整性
```bash
# 检查签名状态
codesign -dv --verbose=4 /Applications/BearVPN.app
# 验证签名
codesign --verify --verbose /Applications/BearVPN.app
```
## 📋 系统要求
- macOS 10.15 或更高版本
- 支持 Intel 和 Apple Silicon (M1/M2) 芯片
- 至少 200MB 可用磁盘空间
## 🔧 故障排除
### 如果应用仍然无法打开
1. 确保 macOS 版本符合要求
2. 检查系统时间是否正确
3. 尝试重新下载应用
4. 联系技术支持
### 性能优化
1. 将应用添加到"登录项"以自动启动
2. 在"系统偏好设置"中允许应用访问网络
3. 确保防火墙没有阻止应用
## 📞 技术支持
如果遇到问题,请联系:
- 邮箱support@bearvpn.com
- 网站https://bearvpn.com
---
**注意**:本应用已通过 Apple 开发者证书签名,确保安全性和完整性。

182
IOS_BUILD_README.md Normal file
View File

@ -0,0 +1,182 @@
# iOS 自动化构建指南
本指南将帮助您使用自动化脚本构建和签名 iOS 应用,并创建 DMG 安装包。
## 🎯 目标
创建经过签名的 iOS 应用 DMG 文件,用于分发和安装。
## 📋 前提条件
### 1. Apple Developer 账户
- 需要有效的 Apple Developer 账户
- 需要 **iOS Development** 证书
- 需要 **Provisioning Profile**
### 2. 获取证书和配置文件
1. 登录 [Apple Developer Portal](https://developer.apple.com)
2. 进入 "Certificates, Identifiers & Profiles"
3. 创建以下证书:
- **iOS Development** (用于应用签名)
4. 创建 App ID 和 Provisioning Profile
5. 下载并安装证书和配置文件
## 🚀 快速开始
### 步骤 1: 配置签名信息
```bash
# 运行配置脚本
./update_team_id.sh
```
按照提示输入您的 Team ID脚本会自动更新配置文件。
### 步骤 2: 加载配置
```bash
# 加载签名配置
source ios_signing_config.sh
```
### 步骤 3: 构建 DMG
```bash
# 构建发布版本
./build_ios_dmg.sh
# 或构建调试版本
./build_ios_dmg.sh debug
```
## 📁 输出文件
构建完成后,文件将位于:
```
build/ios/
├── BearVPN-1.0.0.ipa # 签名的 IPA 文件
└── BearVPN-1.0.0-iOS.dmg # DMG 安装包
```
## 🛠️ 可用的构建脚本
### 1. `build_ios_dmg.sh` - 主要构建脚本
- 构建签名的 iOS 应用
- 创建 DMG 安装包
- 支持调试和发布版本
```bash
./build_ios_dmg.sh [debug|release]
```
### 2. `build_ios_simple.sh` - 简化构建脚本
- 构建未签名的版本
- 仅用于测试和开发
```bash
./build_ios_simple.sh [debug|release]
```
### 3. `build_ios_appstore.sh` - App Store 构建脚本
- 构建用于 App Store 分发的版本
- 支持自动上传到 App Store Connect
```bash
./build_ios_appstore.sh [upload|build]
```
## 🔧 配置文件
### `ios_signing_config.sh`
包含所有签名配置信息:
```bash
# Apple Developer 账户信息
export APPLE_ID="your-apple-id@example.com"
export APPLE_PASSWORD="your-app-password"
export TEAM_ID="YOUR_TEAM_ID"
# 应用信息
export APP_NAME="BearVPN"
export BUNDLE_ID="com.bearvpn.app"
export VERSION="1.0.0"
export BUILD_NUMBER="1"
# 签名身份
export SIGNING_IDENTITY="iPhone Developer: Your Name (YOUR_TEAM_ID)"
export DISTRIBUTION_IDENTITY="iPhone Distribution: Your Name (YOUR_TEAM_ID)"
```
## 🔍 验证构建结果
构建完成后,您可以验证结果:
```bash
# 验证 IPA 文件
unzip -l build/ios/BearVPN-1.0.0.ipa
# 验证 DMG 文件
hdiutil verify build/ios/BearVPN-1.0.0-iOS.dmg
# 查看 DMG 内容
hdiutil mount build/ios/BearVPN-1.0.0-iOS.dmg
```
## 🛠️ 故障排除
### 1. 证书问题
```bash
# 查看可用证书
security find-identity -v -p codesigning
# 如果看到 "0 valid identities found",说明没有安装证书
```
### 2. 配置文件问题
- 确保 Provisioning Profile 已正确安装
- 检查 Bundle ID 是否匹配
- 确保证书和配置文件匹配
### 3. 构建失败
- 检查 Xcode 是否正确安装
- 确保 Flutter 环境正确配置
- 查看构建日志中的具体错误信息
### 4. 签名失败
- 确保证书已正确安装
- 检查签名身份名称是否正确
- 确保证书未过期
## 📚 相关文档
- [Apple 代码签名指南](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)
- [Flutter iOS 部署指南](https://docs.flutter.dev/deployment/ios)
- [Xcode 构建指南](https://developer.apple.com/documentation/xcode)
## ⚠️ 重要提醒
1. **安全性**:请妥善保管您的开发者证书和密码
2. **测试**:在分发前,请在真实的 iOS 设备上测试
3. **更新**:定期更新证书,避免过期
4. **备份**:建议备份您的签名配置
## 🎉 成功标志
如果构建成功,您应该看到:
- ✅ 应用构建成功
- ✅ 应用签名成功
- ✅ IPA 文件创建成功
- ✅ DMG 文件创建成功
- ✅ 最终验证通过
## 📞 支持
如果遇到问题,请检查:
1. 构建日志中的错误信息
2. 证书和配置文件是否正确安装
3. 网络连接是否正常
4. Xcode 和 Flutter 版本是否兼容

241
Makefile Executable file
View File

@ -0,0 +1,241 @@
# .ONESHELL:
include dependencies.properties
MKDIR := mkdir -p
RM := rm -rf
SEP :=/
ifeq ($(OS),Windows_NT)
ifeq ($(IS_GITHUB_ACTIONS),)
MKDIR := -mkdir
RM := rmdir /s /q
SEP:=\\
endif
endif
BINDIR=libcore$(SEP)bin
ANDROID_OUT=android$(SEP)app$(SEP)libs
IOS_OUT=ios$(SEP)Frameworks
DESKTOP_OUT=libcore$(SEP)bin
GEO_ASSETS_DIR=assets$(SEP)core
CORE_PRODUCT_NAME=hiddify-core
CORE_NAME=$(CORE_PRODUCT_NAME)
LIB_NAME=libcore
# libcore始终下载正式版本.
ifeq ($(CHANNEL),prod)
CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version)
else
CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version)
endif
# ifeq ($(CHANNEL),prod)
# CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version)
# else
# CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft
# endif
ifeq ($(CHANNEL),prod)
TARGET=lib/main_prod.dart
else
TARGET=lib/main.dart
endif
BUILD_ARGS=--dart-define sentry_dsn=$(SENTRY_DSN)
DISTRIBUTOR_ARGS=--skip-clean --build-target $(TARGET) --build-dart-define sentry_dsn=$(SENTRY_DSN)
get:
flutter pub get
gen:
dart run build_runner build --delete-conflicting-outputs
translate:
dart run slang
prepare:
@echo use the following commands to prepare the library for each platform:
@echo make android-prepare
@echo make windows-prepare
@echo make linux-prepare
@echo make macos-prepare
@echo make ios-prepare
windows-prepare: get gen translate windows-libs
ios-prepare: get-geo-assets get gen translate ios-libs
cd ios; pod repo update; pod install;echo "done ios prepare"
macos-prepare: get-geo-assets get gen translate macos-libs
linux-prepare: get-geo-assets get gen translate linux-libs
linux-appimage-prepare:linux-prepare
linux-rpm-prepare:linux-prepare
linux-deb-prepare:linux-prepare
android-prepare: get-geo-assets get gen translate android-libs
android-apk-prepare:android-prepare
android-aab-prepare:android-prepare
.PHONY: protos
protos:
make -C libcore -f Makefile protos
protoc --dart_out=grpc:lib/singbox/generated --proto_path=libcore/protos libcore/protos/*.proto
macos-install-dependencies:
brew install create-dmg tree
npm install -g appdmg
dart pub global activate flutter_distributor
ios-install-dependencies:
if [ "$(flutter)" = "true" ]; then \
curl -L -o ~/Downloads/flutter_macos_3.19.3-stable.zip https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.22.3-stable.zip; \
mkdir -p ~/develop; \
cd ~/develop; \
unzip ~/Downloads/flutter_macos_3.22.3-stable.zip; \
export PATH="$$PATH:$$HOME/develop/flutter/bin"; \
echo 'export PATH="$$PATH:$$HOME/develop/flutter/bin"' >> ~/.zshrc; \
export PATH="$PATH:$HOME/develop/flutter/bin"; \
echo 'export PATH="$PATH:$HOME/develop/flutter/bin"' >> ~/.zshrc; \
curl -sSL https://rvm.io/mpapis.asc | gpg --import -; \
curl -sSL https://rvm.io/pkuczynski.asc | gpg --import -; \
curl -sSL https://get.rvm.io | bash -s stable; \
brew install openssl@1.1; \
PKG_CONFIG_PATH=$(brew --prefix openssl@1.1)/lib/pkgconfig rvm install 2.7.5; \
sudo gem install cocoapods -V; \
fi
brew install create-dmg tree
npm install -g appdmg
dart pub global activate flutter_distributor
android-install-dependencies:
echo "nothing yet"
android-apk-install-dependencies: android-install-dependencies
android-aab-install-dependencies: android-install-dependencies
linux-install-dependencies:
if [ "$(flutter)" = "true" ]; then \
mkdir -p ~/develop; \
cd ~/develop; \
wget -O flutter_linux-stable.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.19.4-stable.tar.xz; \
tar xf flutter_linux-stable.tar.xz; \
rm flutter_linux-stable.tar.xz;\
export PATH="$$PATH:$$HOME/develop/flutter/bin"; \
echo 'export PATH="$$PATH:$$HOME/develop/flutter/bin"' >> ~/.bashrc; \
fi
PATH="$$PATH":"$$HOME/.pub-cache/bin"
echo 'export PATH="$$PATH:$$HOME/.pub-cache/bin"' >>~/.bashrc
sudo apt-get update
sudo apt install -y clang ninja-build pkg-config cmake libgtk-3-dev locate ninja-build pkg-config libglib2.0-dev libgio2.0-cil-dev libayatana-appindicator3-dev fuse rpm patchelf file appstream
sudo modprobe fuse
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool
sudo mv appimagetool /usr/local/bin/
dart pub global activate --source git https://github.com/hiddify/flutter_distributor --git-path packages/flutter_distributor
windows-install-dependencies:
dart pub global activate flutter_distributor
gen_translations: #generating missing translations using google translate
cd .github && bash sync_translate.sh
make translate
android-release: android-apk-release
android-apk-release:
echo flutter build apk --target $(TARGET) $(BUILD_ARGS) --target-platform android-arm,android-arm64,android-x64 --split-per-abi --verbose
flutter build apk --target $(TARGET) $(BUILD_ARGS) --target-platform android-arm,android-arm64,android-x64 --verbose
ls -R build/app/outputs
android-aab-release:
flutter build appbundle --target $(TARGET) $(BUILD_ARGS) --dart-define release=google-play
ls -R build/app/outputs
windows-release:
dart pub global activate flutter_distributor
flutter_distributor package --flutter-build-args=verbose --platform windows --targets exe,msix $(DISTRIBUTOR_ARGS)
linux-release:
flutter_distributor package --flutter-build-args=verbose --platform linux --targets deb,rpm,appimage $(DISTRIBUTOR_ARGS)
macos-release:
flutter_distributor package --platform macos --targets dmg,pkg $(DISTRIBUTOR_ARGS)
ios-release: #not tested
flutter_distributor package --platform ios --targets ipa --build-export-options-plist ios/exportOptions.plist $(DISTRIBUTOR_ARGS)
android-libs:
@$(MKDIR) $(ANDROID_OUT) || echo Folder already exists. Skipping...
curl -L $(CORE_URL)/$(CORE_NAME)-android.tar.gz | tar xz -C $(ANDROID_OUT)/
android-apk-libs: android-libs
android-aab-libs: android-libs
windows-libs:
$(MKDIR) $(DESKTOP_OUT) || echo Folder already exists. Skipping...
curl -L $(CORE_URL)/$(CORE_NAME)-windows-amd64.tar.gz | tar xz -C $(DESKTOP_OUT)$(SEP)
ls $(DESKTOP_OUT) || dir $(DESKTOP_OUT)$(SEP)
linux-libs:
mkdir -p $(DESKTOP_OUT)
curl -L $(CORE_URL)/$(CORE_NAME)-linux-amd64.tar.gz | tar xz -C $(DESKTOP_OUT)/
macos-libs:
mkdir -p $(DESKTOP_OUT)
curl -L $(CORE_URL)/$(CORE_NAME)-macos-universal.tar.gz | tar xz -C $(DESKTOP_OUT)
ios-libs: #not tested
mkdir -p $(IOS_OUT)
rm -rf $(IOS_OUT)/Libcore.xcframework
curl -L $(CORE_URL)/$(CORE_NAME)-ios.tar.gz | tar xz -C "$(IOS_OUT)"
get-geo-assets:
echo ""
# curl -L https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db -o $(GEO_ASSETS_DIR)/geoip.db
# curl -L https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db -o $(GEO_ASSETS_DIR)/geosite.db
build-headers:
make -C libcore -f Makefile headers && mv $(BINDIR)/$(CORE_NAME)-headers.h $(BINDIR)/libcore.h
build-android-libs:
make -C libcore -f Makefile android
mv $(BINDIR)/$(LIB_NAME).aar $(ANDROID_OUT)/
build-windows-libs:
make -C libcore -f Makefile windows-amd64
build-linux-libs:
make -C libcore -f Makefile linux-amd64
build-macos-libs:
make -C libcore -f Makefile macos-universal
build-ios-libs:
rf -rf $(IOS_OUT)/Libcore.xcframework
make -C libcore -f Makefile ios
mv $(BINDIR)/Libcore.xcframework $(IOS_OUT)/Libcore.xcframework
release: # Create a new tag for release.
@CORE_VERSION=$(core.version) bash -c ".github/change_version.sh "
ios-temp-prepare:
make ios-prepare
flutter build ios-framework
cd ios
pod install

141
README.md Executable file
View File

@ -0,0 +1,141 @@
# OmnTech - Instant Cloud Services
## Hi there 👋
Welcome to **OmnTech**, where innovation meets freedom in the cloud computing space!
### 🌟 About Us
We are a passionate team of technology enthusiasts at **OmnTech**, a cutting-edge technology company specializing in **instant cloud services**. Our mission is to deliver lightning-fast, reliable, and scalable cloud solutions that empower businesses and individuals worldwide.
### 🌍 Our Locations
- **Headquarters**: Tallinn, Estonia 🇪🇪
- **US Office**: San Jose, California 🇺🇸
### 💻 Our Culture
We are a team of **technology lovers** and **freedom seekers** who believe in the power of remote work. Our distributed workforce spans across different time zones, bringing together diverse perspectives and innovative ideas. We embrace the flexibility of remote collaboration while maintaining the highest standards of technical excellence.
### 🚀 What We Do
**OmnTech** specializes in:
- **Instant Cloud Services** - Rapid deployment and scaling solutions
- **Real-time Infrastructure** - Low-latency cloud computing platforms
- **Global Network Solutions** - Worldwide connectivity and optimization
- **Developer Tools** - APIs and SDKs for seamless integration
- **Enterprise Solutions** - Custom cloud architectures for businesses
### 🌈 How to Get Involved
We welcome contributions from the global developer community! Here's how you can get involved:
- **Open Source Projects** - Contribute to our public repositories
- **Documentation** - Help improve our technical documentation
- **Bug Reports** - Report issues and help us improve
- **Feature Requests** - Suggest new features and improvements
- **Community Discussions** - Join our technical discussions
### 👩‍💻 Useful Resources
- **Documentation**: [docs.omntech.com](https://docs.omntech.com)
- **API Reference**: [api.omntech.com](https://api.omntech.com)
- **Community Forum**: [community.omntech.com](https://community.omntech.com)
- **Support**: [support@omntech.com](mailto:support@omntech.com)
### 🍿 Fun Facts About Our Team
- **Breakfast of Champions**: Our remote team enjoys everything from traditional Estonian black bread to California avocado toast
- **Coffee Culture**: We have a virtual coffee break every day at 3 PM EST
- **Global Perspectives**: Our team speaks 12+ languages fluently
- **Innovation Time**: Every Friday is dedicated to personal projects and innovation
- **Remote First**: We've been remote-first since day one, long before it became mainstream
### 🧙 Our Philosophy
We believe that **technology should serve humanity**, not the other way around. Our commitment to freedom, innovation, and excellence drives everything we do. We're not just building cloud services we're building the future of how people work, collaborate, and create.
### 📞 Contact Us
- **Website**: [omntech.com](https://omntech.com)
- **Email**: [hello@omntech.com](mailto:hello@omntech.com)
- **LinkedIn**: [OmnTech](https://linkedin.com/company/omntech)
- **Twitter**: [@OmnTech](https://twitter.com/omntech)
---
*Remember, you can do mighty things with the power of [Markdown](https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) and the right team!*
---
## 中文翻译
# OmnTech - 即时云服务
## 你好 👋
欢迎来到 **OmnTech**,在这里创新与自由在云计算领域相遇!
### 🌟 关于我们
我们是 **OmnTech** 充满激情的科技爱好者团队,这是一家专注于**即时云服务**的前沿科技公司。我们的使命是为全球企业和个人提供闪电般快速、可靠且可扩展的云解决方案。
### 🌍 我们的办公地点
- **总部**: 爱沙尼亚塔林 🇪🇪
- **美国办公室**: 加利福尼亚圣何塞 🇺🇸
### 💻 我们的文化
我们是一群**技术爱好者**和**自由追求者**,相信远程工作的力量。我们的分布式团队跨越不同时区,汇聚了多样化的观点和创新理念。我们拥抱远程协作的灵活性,同时保持最高的技术卓越标准。
### 🚀 我们的业务
**OmnTech** 专注于:
- **即时云服务** - 快速部署和扩展解决方案
- **实时基础设施** - 低延迟云计算平台
- **全球网络解决方案** - 全球连接和优化
- **开发者工具** - 无缝集成的API和SDK
- **企业解决方案** - 定制化云架构
### 🌈 如何参与
我们欢迎全球开发者社区的贡献!以下是您可以参与的方式:
- **开源项目** - 为我们的公共仓库做出贡献
- **文档** - 帮助改进我们的技术文档
- **错误报告** - 报告问题并帮助我们改进
- **功能请求** - 建议新功能和改进
- **社区讨论** - 加入我们的技术讨论
### 👩‍💻 有用资源
- **文档**: [docs.omntech.com](https://docs.omntech.com)
- **API参考**: [api.omntech.com](https://api.omntech.com)
- **社区论坛**: [community.omntech.com](https://community.omntech.com)
- **支持**: [support@omntech.com](mailto:support@omntech.com)
### 🍿 关于我们团队的有趣事实
- **冠军早餐**: 我们的远程团队享受从传统爱沙尼亚黑面包到加州鳄梨吐司的一切
- **咖啡文化**: 我们每天下午3点EST都有虚拟咖啡时间
- **全球视野**: 我们的团队流利掌握12+种语言
- **创新时间**: 每个周五都致力于个人项目和创新
- **远程优先**: 我们从第一天起就是远程优先,远在它成为主流之前
### 🧙 我们的理念
我们相信**技术应该为人类服务**,而不是相反。我们对自由、创新和卓越的承诺驱动着我们所做的一切。我们不仅仅是在构建云服务——我们正在构建人们工作、协作和创造未来的方式。
### 📞 联系我们
- **网站**: [omntech.com](https://omntech.com)
- **邮箱**: [hello@omntech.com](mailto:hello@omntech.com)
- **LinkedIn**: [OmnTech](https://linkedin.com/company/omntech)
- **Twitter**: [@OmnTech](https://twitter.com/omntech)
---
*记住,有了[Markdown](https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax)的力量和正确的团队,你可以做强大的事情!*

134
TROJAN_SERVER_NAME_FIX.md Executable file
View File

@ -0,0 +1,134 @@
# Trojan 配置中 server_name 参数修复
## 🔍 问题分析
### **原始问题**
你的 Trojan 配置中存在 `server_name` 设置错误:
```json
{
"server": "156.224.78.176",
"server_name": "baidu.com" // ❌ 错误:服务器 IP 与 SNI 不匹配
}
```
### **问题原因**
1. **TLS 握手失败** - 服务器没有为 `baidu.com` 配置证书
2. **SNI 不匹配** - 客户端请求 `baidu.com`,但服务器只支持 IP 地址
3. **代理无法工作** - TLS 验证失败导致连接中断
## 🛠️ 修复方案
### **智能 server_name 设置**
已修改配置生成逻辑,现在会:
1. **优先使用配置的 SNI** - 如果服务器配置了 `sni` 参数
2. **回退到服务器地址** - 如果没有配置 SNI使用服务器 IP/域名
3. **避免不匹配** - 确保 `server_name` 与服务器实际配置一致
### **修复后的逻辑**
```dart
// 智能设置 server_name
String serverName = securityConfig["sni"] ?? "";
if (serverName.isEmpty) {
// 如果没有配置 SNI使用服务器地址
serverName = nodeListItem.serverAddr;
}
```
### **修复后的配置**
```json
{
"server": "156.224.78.176",
"server_name": "156.224.78.176" // ✅ 正确:使用服务器 IP
}
```
## 📋 server_name 参数说明
### **应该填什么值**
#### **1. 服务器实际域名** ✅ **最佳选择**
```json
{
"server_name": "your-server-domain.com"
}
```
#### **2. 服务器 IP 地址** ✅ **推荐**
```json
{
"server_name": "156.224.78.176"
}
```
#### **3. 空字符串** ✅ **某些情况下**
```json
{
"server_name": ""
}
```
### **不应该填什么值**
#### **❌ 随机域名**
```json
{
"server_name": "baidu.com" // 错误:服务器没有这个域名的证书
}
```
#### **❌ 不相关的域名**
```json
{
"server_name": "google.com" // 错误:与服务器不匹配
}
```
## 🔧 其他协议修复
已同时修复了以下协议的 `server_name` 设置:
- **VLESS** - 智能 SNI 设置
- **VMess** - 智能 SNI 设置
- **Trojan** - 智能 SNI 设置
## 🧪 测试步骤
1. **重新运行应用**
2. **检查新的配置** - 应该看到 `server_name` 使用服务器 IP
3. **测试连接** - 应该能正常通过代理访问网络
4. **验证延迟** - 延迟测试应该能正常工作
## 📊 预期结果
修复后应该看到:
```json
{
"type": "trojan",
"tag": "香港",
"server": "156.224.78.176",
"server_port": 27639,
"password": "cf6dc0d8-4997-4fc3-b790-1a54e38c6e8c",
"tls": {
"enabled": true,
"server_name": "156.224.78.176", // ✅ 修复后
"insecure": false,
"utls": {
"enabled": true,
"fingerprint": "chrome"
}
}
}
```
## 💡 关键要点
1. **`server_name` 必须与服务器配置匹配**
2. **优先使用服务器实际域名**
3. **IP 地址也是有效的选择**
4. **避免使用不相关的域名**
5. **TLS 验证失败会导致代理无法工作**
这个修复应该能解决你的 Trojan 连接问题!

4
analysis_options.yaml Executable file
View File

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:

16
android/.gitignore vendored Executable file
View File

@ -0,0 +1,16 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
/app/libs/*
!/app/libs/.gitkeep

11
android/.stignore Executable file
View File

@ -0,0 +1,11 @@
gradle-wrapper.jar
.gradle
captures/
gradlew
gradlew.bat
local.properties
GeneratedPluginRegistrant.java
key.properties
**.keystore
**.jks

158
android/app/build.gradle Executable file
View File

@ -0,0 +1,158 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterRoot = properties.getProperty("flutter.sdk")
assert flutterRoot != null, "flutter.sdk not set in local.properties"
boolean hasKeyStore = false
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
hasKeyStore = true
} else {
println "+++"
println "No keystore defined. The app will not be signed."
println "Create a android/key.properties file with the following properties:"
println "storePassword"
println "keyPassword"
println "keyAlias"
println "storeFile"
println "+++"
}
def flutterVersionCode = properties.getProperty('flutter.versionCode')?: '1'
def flutterVersionName = properties.getProperty('flutter.versionName') ?: '1.0'
android {
namespace 'com.hiddify.hiddify'
testNamespace "test.com.hiddify.hiddify"
compileSdk 36
ndkVersion "26.1.10909125"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "app.brAccelerator.com"
minSdkVersion flutter.minSdkVersion
targetSdkVersion 36
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
manifestPlaceholders = [
'android.permission.ACCESS_NETWORK_STATE': true
]
android.defaultConfig.manifestPlaceholders += [
'android:screenOrientation': "portrait"
]
}
splits {
abi {
enable true
reset()
include "armeabi-v7a", "arm64-v8a"
universalApk true
}
}
if (hasKeyStore) {
signingConfigs {
release {
/* keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']*/
storeFile file("upload-keystore.jks")
storePassword "123456"
keyAlias "upload"
keyPassword "123456"
}
}
}
buildTypes {
release {
if (hasKeyStore) {
signingConfig signingConfigs.release
} else {
signingConfig signingConfigs.debug
}
ndk {
abiFilters "armeabi-v7a", "arm64-v8a"
debugSymbolLevel 'FULL'
}
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
aidl true
}
configurations.all {
resolutionStrategy {
force "org.jetbrains.kotlin:kotlin-stdlib:2.1.0"
force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0"
force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0"
force "androidx.annotation:annotation-jvm:1.7.0"
}
}
lintOptions {
checkReleaseBuilds false
}
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.versionCodeOverride = android.defaultConfig.versionCode
}
}
flutter {
source '../..'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.0"
implementation "androidx.annotation:annotation:1.7.1"
constraints {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0") {
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
}
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0") {
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
}
}
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 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.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,149 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Crisp 聊天所需权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".Application"
android:banner="@mipmap/ic_banner"
android:icon="@mipmap/ic_launcher"
android:label="BearVPN"
android:roundIcon="@mipmap/ic_launcher_round"
tools:targetApi="31">
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<activity
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:screenOrientation="portrait"
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" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="sing-box" />
<data android:host="import-remote-profile" />
<data android:scheme="clash" />
<data android:host="install-config" />
<data android:scheme="clashmeta" />
<data android:scheme="hiddify" />
<data android:host="install-sub" />
<data android:scheme="hiddify" />
<data android:host="import" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
<activity
android:name=".ShortcutActivity"
android:excludeFromRecents="true"
android:exported="true"
android:label="@string/quick_toggle"
android:launchMode="singleTask"
android:taskAffinity="">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
</activity>
<service
android:name=".bg.TileService"
android:directBootAware="true"
android:exported="true"
android:icon="@drawable/ic_stat_logo"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="n">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
<service
android:name=".bg.VPNService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE">
<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=".bg.ProxyService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="proxy" />
</service>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -0,0 +1,9 @@
package com.hiddify.hiddify;
import com.hiddify.hiddify.IServiceCallback;
interface IService {
int getStatus();
void registerCallback(in IServiceCallback callback);
oneway void unregisterCallback(in IServiceCallback callback);
}

View File

@ -0,0 +1,8 @@
package com.hiddify.hiddify;
interface IServiceCallback {
void onServiceStatusChanged(int status);
void onServiceAlert(int type, String message);
void onServiceWriteLog(String message);
void onServiceResetLogs(in List<String> messages);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,60 @@
package com.hiddify.hiddify
import android.util.Log
import com.google.gson.Gson
import com.hiddify.hiddify.utils.CommandClient
import com.hiddify.hiddify.utils.ParsedOutboundGroup
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.nekohasekai.libbox.OutboundGroup
import kotlinx.coroutines.CoroutineScope
class ActiveGroupsChannel(private val scope: CoroutineScope) : FlutterPlugin,
CommandClient.Handler {
companion object {
const val TAG = "A/ActiveGroupsChannel"
const val CHANNEL = "com.baer.app/active-groups"
val gson = Gson()
}
private val client =
CommandClient(scope, CommandClient.ConnectionType.GroupOnly, this)
private var channel: EventChannel? = null
private var event: EventChannel.EventSink? = null
override fun updateGroups(groups: List<OutboundGroup>) {
MainActivity.instance.runOnUiThread {
val parsedGroups = groups.map { group -> ParsedOutboundGroup.fromOutbound(group) }
event?.success(gson.toJson(parsedGroups))
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = EventChannel(
flutterPluginBinding.binaryMessenger,
CHANNEL
)
channel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
event = events
Log.d(TAG, "connecting active groups command client")
client.connect()
}
override fun onCancel(arguments: Any?) {
event = null
Log.d(TAG, "disconnecting active groups command client")
client.disconnect()
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
event = null
client.disconnect()
channel?.setStreamHandler(null)
}
}

View File

@ -0,0 +1,43 @@
package com.hiddify.hiddify
import android.app.Application
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.PowerManager
import androidx.core.content.getSystemService
import com.hiddify.hiddify.bg.AppChangeReceiver
import go.Seq
import com.hiddify.hiddify.Application as BoxApplication
class Application : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
application = this
}
override fun onCreate() {
super.onCreate()
Seq.setContext(this)
registerReceiver(AppChangeReceiver(), IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addDataScheme("package")
})
}
companion object {
lateinit var application: BoxApplication
val notification by lazy { application.getSystemService<NotificationManager>()!! }
val connectivity by lazy { application.getSystemService<ConnectivityManager>()!! }
val packageManager by lazy { application.packageManager }
val powerManager by lazy { application.getSystemService<PowerManager>()!! }
val notificationManager by lazy { application.getSystemService<NotificationManager>()!! }
}
}

View File

@ -0,0 +1,82 @@
package com.hiddify.hiddify
import android.util.Log
import androidx.lifecycle.Observer
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.JSONMethodCodec
class EventHandler : FlutterPlugin {
companion object {
const val TAG = "A/EventHandler"
const val SERVICE_STATUS = "com.baer.app/service.status"
const val SERVICE_ALERTS = "com.baer.app/service.alerts"
}
private var statusChannel: EventChannel? = null
private var alertsChannel: EventChannel? = null
private var statusObserver: Observer<Status>? = null
private var alertsObserver: Observer<ServiceEvent?>? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
statusChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_STATUS, JSONMethodCodec.INSTANCE)
alertsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_ALERTS, JSONMethodCodec.INSTANCE)
statusChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
statusObserver = Observer {
Log.d(TAG, "new status: $it")
val map = listOf(
Pair("status", it.name)
)
.toMap()
events?.success(map)
}
MainActivity.instance.serviceStatus.observeForever(statusObserver!!)
}
override fun onCancel(arguments: Any?) {
if (statusObserver != null)
MainActivity.instance.serviceStatus.removeObserver(statusObserver!!)
}
})
alertsChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
alertsObserver = Observer {
if (it == null) return@Observer
Log.d(TAG, "new alert: $it")
val map = listOf(
Pair("status", it.status.name),
Pair("alert", it.alert?.name),
Pair("message", it.message)
)
.mapNotNull { p -> p.second?.let { Pair(p.first, p.second) } }
.toMap()
events?.success(map)
}
MainActivity.instance.serviceAlerts.observeForever(alertsObserver!!)
}
override fun onCancel(arguments: Any?) {
if (alertsObserver != null)
MainActivity.instance.serviceAlerts.removeObserver(alertsObserver!!)
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
if (statusObserver != null)
MainActivity.instance.serviceStatus.removeObserver(statusObserver!!)
statusChannel?.setStreamHandler(null)
if (alertsObserver != null)
MainActivity.instance.serviceAlerts.removeObserver(alertsObserver!!)
alertsChannel?.setStreamHandler(null)
}
}
data class ServiceEvent(val status: Status, val alert: Alert? = null, val message: String? = null)

View File

@ -0,0 +1,58 @@
package com.hiddify.hiddify
import android.util.Log
import com.google.gson.Gson
import com.hiddify.hiddify.utils.CommandClient
import com.hiddify.hiddify.utils.ParsedOutboundGroup
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.nekohasekai.libbox.OutboundGroup
import kotlinx.coroutines.CoroutineScope
class GroupsChannel(private val scope: CoroutineScope) : FlutterPlugin, CommandClient.Handler {
companion object {
const val TAG = "A/GroupsChannel"
const val CHANNEL = "com.baer.app/groups"
val gson = Gson()
}
private val client =
CommandClient(scope, CommandClient.ConnectionType.Groups, this)
private var channel: EventChannel? = null
private var event: EventChannel.EventSink? = null
override fun updateGroups(groups: List<OutboundGroup>) {
MainActivity.instance.runOnUiThread {
val parsedGroups = groups.map { group -> ParsedOutboundGroup.fromOutbound(group) }
event?.success(gson.toJson(parsedGroups))
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = EventChannel(
flutterPluginBinding.binaryMessenger,
CHANNEL
)
channel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
event = events
Log.d(TAG, "connecting groups command client")
client.connect()
}
override fun onCancel(arguments: Any?) {
event = null
Log.d(TAG, "disconnecting groups command client")
client.disconnect()
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
event = null
client.disconnect()
channel?.setStreamHandler(null)
}
}

View File

@ -0,0 +1,37 @@
package com.hiddify.hiddify
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
class LogHandler : FlutterPlugin {
companion object {
const val TAG = "A/LogHandler"
const val SERVICE_LOGS = "com.baer.app/service.logs"
}
private lateinit var logsChannel: EventChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
logsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_LOGS)
logsChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
val activity = MainActivity.instance
events?.success(activity.logList)
activity.logCallback = {
events?.success(activity.logList)
}
}
override fun onCancel(arguments: Any?) {
MainActivity.instance.logCallback = null
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
}
}

View File

@ -0,0 +1,156 @@
package com.hiddify.hiddify
import android.annotation.SuppressLint
import android.content.Intent
import android.Manifest
import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import com.hiddify.hiddify.bg.ServiceConnection
import com.hiddify.hiddify.bg.ServiceNotification
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.ServiceMode
import com.hiddify.hiddify.constant.Status
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.LinkedList
class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
companion object {
private const val TAG = "ANDROID/MyActivity"
lateinit var instance: MainActivity
const val VPN_PERMISSION_REQUEST_CODE = 1001
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1010
}
private val connection = ServiceConnection(this, this)
val logList = LinkedList<String>()
var logCallback: ((Boolean) -> Unit)? = null
val serviceStatus = MutableLiveData(Status.Stopped)
val serviceAlerts = MutableLiveData<ServiceEvent?>(null)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
instance = this
reconnect()
flutterEngine.plugins.add(MethodHandler(lifecycleScope))
flutterEngine.plugins.add(PlatformSettingsHandler())
flutterEngine.plugins.add(EventHandler())
flutterEngine.plugins.add(LogHandler())
flutterEngine.plugins.add(GroupsChannel(lifecycleScope))
flutterEngine.plugins.add(ActiveGroupsChannel(lifecycleScope))
flutterEngine.plugins.add(StatsChannel(lifecycleScope))
}
fun reconnect() {
connection.reconnect()
}
fun startService() {
// 暂时跳过通知权限检查
lifecycleScope.launch(Dispatchers.IO) {
if (Settings.rebuildServiceMode()) {
reconnect()
}
if (Settings.serviceMode == ServiceMode.VPN) {
if (prepare()) {
Log.d(TAG, "VPN permission required")
return@launch
}
}
val intent = Intent(Application.application, Settings.serviceClass())
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(Application.application, intent)
}
}
}
private suspend fun prepare() = withContext(Dispatchers.Main) {
try {
val intent = VpnService.prepare(this@MainActivity)
if (intent != null) {
startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
true
} else {
false
}
} catch (e: Exception) {
onServiceAlert(Alert.RequestVPNPermission, e.message)
false
}
}
override fun onServiceStatusChanged(status: Status) {
serviceStatus.postValue(status)
}
override fun onServiceAlert(type: Alert, message: String?) {
serviceAlerts.postValue(ServiceEvent(Status.Stopped, type, message))
}
override fun onServiceWriteLog(message: String?) {
if (logList.size > 300) {
logList.removeFirst()
}
logList.addLast(message ?: "")
logCallback?.invoke(false)
}
override fun onServiceResetLogs(messages: MutableList<String>) {
logList.clear()
logList.addAll(messages)
logCallback?.invoke(true)
}
override fun onDestroy() {
connection.disconnect()
super.onDestroy()
}
@SuppressLint("NewApi")
private fun grantNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startService()
} else onServiceAlert(Alert.RequestNotificationPermission, null)
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == RESULT_OK) startService()
else onServiceAlert(Alert.RequestVPNPermission, null)
} else if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
if (resultCode == RESULT_OK) startService()
else onServiceAlert(Alert.RequestNotificationPermission, null)
}
}
}

View File

@ -0,0 +1,227 @@
package com.hiddify.hiddify
import android.util.Log
import com.hiddify.hiddify.bg.BoxService
import com.hiddify.hiddify.constant.Status
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.mobile.Mobile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
MethodChannel.MethodCallHandler {
private var channel: MethodChannel? = null
companion object {
const val TAG = "A/MethodHandler"
const val channelName = "com.baer.app/method"
enum class Trigger(val method: String) {
Setup("setup"),
ParseConfig("parse_config"),
changeHiddifyOptions("change_hiddify_options"),
GenerateConfig("generate_config"),
Start("start"),
Stop("stop"),
Restart("restart"),
SelectOutbound("select_outbound"),
UrlTest("url_test"),
ClearLogs("clear_logs"),
GenerateWarpConfig("generate_warp_config"),
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(
flutterPluginBinding.binaryMessenger,
channelName,
)
channel!!.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel?.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
Trigger.Setup.method -> {
GlobalScope.launch {
result.runCatching {
val baseDir = Application.application.filesDir
baseDir.mkdirs()
val workingDir = Application.application.getExternalFilesDir(null)
workingDir?.mkdirs()
val tempDir = Application.application.cacheDir
tempDir.mkdirs()
Log.d(TAG, "base dir: ${baseDir.path}")
Log.d(TAG, "working dir: ${workingDir?.path}")
Log.d(TAG, "temp dir: ${tempDir.path}")
Mobile.setup(baseDir.path, workingDir?.path, tempDir.path, false)
Libbox.redirectStderr(File(workingDir, "stderr2.log").path)
success("")
}
}
}
Trigger.ParseConfig.method -> {
scope.launch(Dispatchers.IO) {
result.runCatching {
val args = call.arguments as Map<*, *>
val path = args["path"] as String
val tempPath = args["tempPath"] as String
val debug = args["debug"] as Boolean
val msg = BoxService.parseConfig(path, tempPath, debug)
success(msg)
}
}
}
Trigger.changeHiddifyOptions.method -> {
scope.launch {
result.runCatching {
val args = call.arguments as String
Settings.configOptions = args
success(true)
}
}
}
Trigger.GenerateConfig.method -> {
scope.launch {
result.runCatching {
val args = call.arguments as Map<*, *>
val path = args["path"] as String
val options = Settings.configOptions
if (options.isBlank() || path.isBlank()) {
error("blank properties")
}
val config = BoxService.buildConfig(path, options)
success(config)
}
}
}
Trigger.Start.method -> {
scope.launch {
result.runCatching {
val args = call.arguments as Map<*, *>
Settings.activeConfigPath = args["path"] as String? ?: ""
Settings.activeProfileName = args["name"] as String? ?: ""
val mainActivity = MainActivity.instance
val started = mainActivity.serviceStatus.value == Status.Started
if (started) {
Log.w(TAG, "service is already running")
return@launch success(true)
}
mainActivity.startService()
success(true)
}
}
}
Trigger.Stop.method -> {
scope.launch {
result.runCatching {
val mainActivity = MainActivity.instance
val started = mainActivity.serviceStatus.value == Status.Started
if (!started) {
Log.w(TAG, "service is not running")
return@launch success(true)
}
BoxService.stop()
success(true)
}
}
}
Trigger.Restart.method -> {
scope.launch(Dispatchers.IO) {
result.runCatching {
val args = call.arguments as Map<*, *>
Settings.activeConfigPath = args["path"] as String? ?: ""
Settings.activeProfileName = args["name"] as String? ?: ""
val mainActivity = MainActivity.instance
val started = mainActivity.serviceStatus.value == Status.Started
if (!started) return@launch success(true)
val restart = Settings.rebuildServiceMode()
if (restart) {
mainActivity.reconnect()
BoxService.stop()
delay(1000L)
mainActivity.startService()
return@launch success(true)
}
runCatching {
Libbox.newStandaloneCommandClient().serviceReload()
success(true)
}.onFailure {
error(it)
}
}
}
}
Trigger.SelectOutbound.method -> {
scope.launch {
result.runCatching {
val args = call.arguments as Map<*, *>
Libbox.newStandaloneCommandClient()
.selectOutbound(
args["groupTag"] as String,
args["outboundTag"] as String
)
success(true)
}
}
}
Trigger.UrlTest.method -> {
scope.launch {
result.runCatching {
val args = call.arguments as Map<*, *>
Libbox.newStandaloneCommandClient()
.urlTest(
args["groupTag"] as String
)
success(true)
}
}
}
Trigger.ClearLogs.method -> {
scope.launch {
result.runCatching {
MainActivity.instance.onServiceResetLogs(mutableListOf())
success(true)
}
}
}
Trigger.GenerateWarpConfig.method -> {
scope.launch(Dispatchers.IO) {
result.runCatching {
val args = call.arguments as Map<*, *>
val warpConfig = Mobile.generateWarpConfig(
args["license-key"] as String,
args["previous-account-id"] as String,
args["previous-access-token"] as String,
)
success(warpConfig)
}
}
}
else -> result.notImplemented()
}
}
}

View File

@ -0,0 +1,189 @@
package com.hiddify.hiddify
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri
import android.os.Build
import android.util.Base64
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.hiddify.hiddify.Application.Companion.packageManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugin.common.StandardMethodCodec
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware,
PluginRegistry.ActivityResultListener {
private var channel: MethodChannel? = null
private var activity: Activity? = null
private lateinit var ignoreRequestResult: MethodChannel.Result
companion object {
const val channelName = "com.baer.app/platform"
const val REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = 44
val gson = Gson()
enum class Trigger(val method: String) {
IsIgnoringBatteryOptimizations("is_ignoring_battery_optimizations"),
RequestIgnoreBatteryOptimizations("request_ignore_battery_optimizations"),
GetInstalledPackages("get_installed_packages"),
GetPackagesIcon("get_package_icon"),
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val taskQueue = flutterPluginBinding.binaryMessenger.makeBackgroundTaskQueue()
channel = MethodChannel(
flutterPluginBinding.binaryMessenger,
channelName,
StandardMethodCodec.INSTANCE,
taskQueue
)
channel!!.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel?.setMethodCallHandler(null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivity() {
activity = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) {
ignoreRequestResult.success(resultCode == Activity.RESULT_OK)
return true
}
return false
}
data class AppItem(
@SerializedName("package-name") val packageName: String,
@SerializedName("name") val name: String,
@SerializedName("is-system-app") val isSystemApp: Boolean
)
@SuppressLint("BatteryLife")
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
Trigger.IsIgnoringBatteryOptimizations.method -> {
result.runCatching {
success(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Application.powerManager.isIgnoringBatteryOptimizations(Application.application.packageName)
} else {
true
}
)
}
}
Trigger.RequestIgnoreBatteryOptimizations.method -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return result.success(true)
}
val intent = Intent(
android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:${Application.application.packageName}")
)
ignoreRequestResult = result
activity?.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
}
Trigger.GetInstalledPackages.method -> {
GlobalScope.launch {
result.runCatching {
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES
} else {
@Suppress("DEPRECATION")
PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES
}
val installedPackages =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getInstalledPackages(
PackageManager.PackageInfoFlags.of(
flag.toLong()
)
)
} else {
@Suppress("DEPRECATION")
packageManager.getInstalledPackages(flag)
}
val list = mutableListOf<AppItem>()
installedPackages.forEach {
if (it.packageName != Application.application.packageName &&
(it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android")
) {
list.add(
AppItem(
it.packageName,
it.applicationInfo?.loadLabel(packageManager)?.toString() ?: "",
(it.applicationInfo?.flags ?: 0) and ApplicationInfo.FLAG_SYSTEM == 1
)
)
}
}
list.sortBy { it.name }
success(gson.toJson(list))
}
}
}
Trigger.GetPackagesIcon.method -> {
result.runCatching {
val args = call.arguments as Map<*, *>
val packageName =
args["packageName"] as String
val drawable = packageManager.getApplicationIcon(packageName)
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
val base64: String =
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
success(base64)
}
}
else -> result.notImplemented()
}
}
}

View File

@ -0,0 +1,126 @@
package com.hiddify.hiddify
import android.content.Context
import android.util.Base64
import com.hiddify.hiddify.bg.ProxyService
import com.hiddify.hiddify.bg.VPNService
import com.hiddify.hiddify.constant.PerAppProxyMode
import com.hiddify.hiddify.constant.ServiceMode
import com.hiddify.hiddify.constant.SettingsKey
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.File
import java.io.ObjectInputStream
object Settings {
private val preferences by lazy {
val context = Application.application.applicationContext
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
}
private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"
var perAppProxyMode: String
get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF)!!
set(value) = preferences.edit().putString(SettingsKey.PER_APP_PROXY_MODE, value).apply()
val perAppProxyEnabled: Boolean
get() = perAppProxyMode != PerAppProxyMode.OFF
val perAppProxyList: List<String>
get() {
val stringValue = if (perAppProxyMode == PerAppProxyMode.INCLUDE) {
preferences.getString(SettingsKey.PER_APP_PROXY_INCLUDE_LIST, "")!!
} else {
preferences.getString(SettingsKey.PER_APP_PROXY_EXCLUDE_LIST, "")!!
}
if (!stringValue.startsWith(LIST_IDENTIFIER)) {
return emptyList()
}
return decodeListString(stringValue.substring(LIST_IDENTIFIER.length))
}
private fun decodeListString(listString: String): List<String> {
val stream = ObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0)))
return stream.readObject() as List<String>
}
var activeConfigPath: String
get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!!
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_CONFIG_PATH, value).apply()
var activeProfileName: String
get() = preferences.getString(SettingsKey.ACTIVE_PROFILE_NAME, "")!!
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_PROFILE_NAME, value).apply()
var serviceMode: String
get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.VPN)!!
set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply()
var configOptions: String
get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "")!!
set(value) = preferences.edit().putString(SettingsKey.CONFIG_OPTIONS, value).apply()
var debugMode: Boolean
get() = preferences.getBoolean(SettingsKey.DEBUG_MODE, false)
set(value) = preferences.edit().putBoolean(SettingsKey.DEBUG_MODE, value).apply()
var disableMemoryLimit: Boolean
get() = preferences.getBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, false)
set(value) =
preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply()
var dynamicNotification: Boolean
get() = preferences.getBoolean(SettingsKey.DYNAMIC_NOTIFICATION, true)
set(value) =
preferences.edit().putBoolean(SettingsKey.DYNAMIC_NOTIFICATION, value).apply()
var systemProxyEnabled: Boolean
get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true)
set(value) =
preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply()
var startedByUser: Boolean
get() = preferences.getBoolean(SettingsKey.STARTED_BY_USER, false)
set(value) = preferences.edit().putBoolean(SettingsKey.STARTED_BY_USER, value).apply()
fun serviceClass(): Class<*> {
return when (serviceMode) {
ServiceMode.VPN -> VPNService::class.java
else -> ProxyService::class.java
}
}
private var currentServiceMode : String? = null
suspend fun rebuildServiceMode(): Boolean {
var newMode = ServiceMode.NORMAL
try {
if (serviceMode == ServiceMode.VPN) {
newMode = ServiceMode.VPN
}
} catch (_: Exception) {
}
if (currentServiceMode == newMode) {
return false
}
currentServiceMode = newMode
return true
}
private suspend fun needVPNService(): Boolean {
val filePath = activeConfigPath
if (filePath.isBlank()) return false
val content = JSONObject(File(filePath).readText())
val inbounds = content.getJSONArray("inbounds")
for (index in 0 until inbounds.length()) {
val inbound = inbounds.getJSONObject(index)
if (inbound.getString("type") == "tun") {
return true
}
}
return false
}
}

View File

@ -0,0 +1,67 @@
package com.hiddify.hiddify
import android.app.Activity
import android.content.Intent
import android.content.pm.ShortcutManager
import android.os.Build
import android.os.Bundle
import androidx.core.content.getSystemService
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.hiddify.hiddify.bg.BoxService
import com.hiddify.hiddify.bg.ServiceConnection
import com.hiddify.hiddify.constant.Status
class ShortcutActivity : Activity(), ServiceConnection.Callback {
private val connection = ServiceConnection(this, this, false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
setResult(
RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(
this,
ShortcutInfoCompat.Builder(this, "toggle")
.setIntent(
Intent(
this,
ShortcutActivity::class.java
).setAction(Intent.ACTION_MAIN)
)
.setIcon(
IconCompat.createWithResource(
this,
R.mipmap.ic_launcher
)
)
.setShortLabel(getString(R.string.quick_toggle))
.build()
)
)
finish()
} else {
connection.connect()
if (Build.VERSION.SDK_INT >= 25) {
getSystemService<ShortcutManager>()?.reportShortcutUsed("toggle")
}
}
moveTaskToBack(true)
}
override fun onServiceStatusChanged(status: Status) {
when (status) {
Status.Started -> BoxService.stop()
Status.Stopped -> BoxService.start()
else -> {}
}
finish()
}
override fun onDestroy() {
connection.disconnect()
super.onDestroy()
}
}

View File

@ -0,0 +1,64 @@
package com.hiddify.hiddify
import android.util.Log
import com.hiddify.hiddify.utils.CommandClient
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.JSONMethodCodec
import io.nekohasekai.libbox.StatusMessage
import kotlinx.coroutines.CoroutineScope
class StatsChannel(private val scope: CoroutineScope) : FlutterPlugin, CommandClient.Handler{
companion object {
const val TAG = "A/StatsChannel"
const val STATS_CHANNEL = "com.baer.app/stats"
}
private val commandClient =
CommandClient(scope, CommandClient.ConnectionType.Status, this)
private var statsChannel: EventChannel? = null
private var statsEvent: EventChannel.EventSink? = null
override fun updateStatus(status: StatusMessage) {
MainActivity.instance.runOnUiThread {
val map = listOf(
Pair("connections-in", status.connectionsIn),
Pair("connections-out", status.connectionsOut),
Pair("uplink", status.uplink),
Pair("downlink", status.downlink),
Pair("uplink-total", status.uplinkTotal),
Pair("downlink-total", status.downlinkTotal)
).associate { it.first to it.second }
statsEvent?.success(map)
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
statsChannel = EventChannel(
flutterPluginBinding.binaryMessenger,
STATS_CHANNEL,
JSONMethodCodec.INSTANCE
)
statsChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
statsEvent = events
Log.d(TAG, "connecting stats command client")
commandClient.connect()
}
override fun onCancel(arguments: Any?) {
statsEvent = null
Log.d(TAG, "disconnecting stats command client")
commandClient.disconnect()
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
statsEvent = null
commandClient.disconnect()
statsChannel?.setStreamHandler(null)
}
}

View File

@ -0,0 +1,37 @@
package com.hiddify.hiddify.bg
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.hiddify.hiddify.Settings
class AppChangeReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "A/AppChangeReceiver"
}
override fun onReceive(context: Context, intent: Intent) {
checkUpdate(context, intent)
}
private fun checkUpdate(context: Context, intent: Intent) {
// if (!Settings.perAppProxyEnabled) {
// return
// }
// val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
// if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
// return
// }
// val packageName = intent.dataString?.substringAfter("package:")
// if (packageName.isNullOrBlank()) {
// return
// }
// if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) {
// Settings.perAppProxyList = Settings.perAppProxyList + packageName
// } else {
// Settings.perAppProxyList = Settings.perAppProxyList - packageName
// }
}
}

View File

@ -0,0 +1,30 @@
package com.hiddify.hiddify.bg
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.hiddify.hiddify.Settings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
}
else -> return
}
GlobalScope.launch(Dispatchers.IO) {
if (Settings.startedByUser) {
withContext(Dispatchers.Main) {
BoxService.start()
}
}
}
}
}

View File

@ -0,0 +1,364 @@
package com.hiddify.hiddify.bg
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.os.PowerManager
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.Application
import com.hiddify.hiddify.R
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import go.Seq
import io.nekohasekai.libbox.BoxService
import io.nekohasekai.libbox.CommandServer
import io.nekohasekai.libbox.CommandServerHandler
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.SystemProxyStatus
import io.nekohasekai.mobile.Mobile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
class BoxService(
private val service: Service,
private val platformInterface: PlatformInterface
) : CommandServerHandler {
companion object {
private const val TAG = "A/BoxService"
private var initializeOnce = false
private lateinit var workingDir: File
private fun initialize() {
if (initializeOnce) return
val baseDir = Application.application.filesDir
baseDir.mkdirs()
workingDir = Application.application.getExternalFilesDir(null) ?: return
workingDir.mkdirs()
val tempDir = Application.application.cacheDir
tempDir.mkdirs()
Log.d(TAG, "base dir: ${baseDir.path}")
Log.d(TAG, "working dir: ${workingDir.path}")
Log.d(TAG, "temp dir: ${tempDir.path}")
Mobile.setup(baseDir.path, workingDir.path, tempDir.path, false)
Libbox.redirectStderr(File(workingDir, "stderr.log").path)
initializeOnce = true
return
}
fun parseConfig(path: String, tempPath: String, debug: Boolean): String {
return try {
Mobile.parse(path, tempPath, debug)
""
} catch (e: Exception) {
Log.w(TAG, e)
e.message ?: "invalid config"
}
}
fun buildConfig(path: String, options: String): String {
return Mobile.buildConfig(path, options)
}
fun start() {
val intent = runBlocking {
withContext(Dispatchers.IO) {
Intent(Application.application, Settings.serviceClass())
}
}
ContextCompat.startForegroundService(Application.application, intent)
}
fun stop() {
Application.application.sendBroadcast(
Intent(Action.SERVICE_CLOSE).setPackage(
Application.application.packageName
)
)
}
fun reload() {
Application.application.sendBroadcast(
Intent(Action.SERVICE_RELOAD).setPackage(
Application.application.packageName
)
)
}
}
var fileDescriptor: ParcelFileDescriptor? = null
private val status = MutableLiveData(Status.Stopped)
private val binder = ServiceBinder(status)
private val notification = ServiceNotification(status, service)
private var boxService: BoxService? = null
private var commandServer: CommandServer? = null
private var receiverRegistered = false
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Action.SERVICE_CLOSE -> {
stopService()
}
Action.SERVICE_RELOAD -> {
serviceReload()
}
PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
serviceUpdateIdleMode()
}
}
}
}
}
private fun startCommandServer() {
val commandServer =
CommandServer(this, 300)
commandServer.start()
this.commandServer = commandServer
}
private var activeProfileName = ""
private suspend fun startService(delayStart: Boolean = false) {
try {
Log.d(TAG, "starting service")
// 暂时禁用通知显示
// withContext(Dispatchers.Main) {
// notification.show(activeProfileName, R.string.status_starting)
// }
val selectedConfigPath = Settings.activeConfigPath
if (selectedConfigPath.isBlank()) {
stopAndAlert(Alert.EmptyConfiguration)
return
}
activeProfileName = Settings.activeProfileName
val configOptions = Settings.configOptions
if (configOptions.isBlank()) {
stopAndAlert(Alert.EmptyConfiguration)
return
}
val content = try {
Mobile.buildConfig(selectedConfigPath, configOptions)
} catch (e: Exception) {
Log.w(TAG, e)
stopAndAlert(Alert.EmptyConfiguration)
return
}
if (Settings.debugMode) {
File(workingDir, "current-config.json").writeText(content)
}
withContext(Dispatchers.Main) {
notification.show(activeProfileName, R.string.status_starting)
binder.broadcast {
it.onServiceResetLogs(listOf())
}
}
DefaultNetworkMonitor.start()
Libbox.registerLocalDNSTransport(LocalResolver)
Libbox.setMemoryLimit(!Settings.disableMemoryLimit)
val newService = try {
Libbox.newService(content, platformInterface)
} catch (e: Exception) {
stopAndAlert(Alert.CreateService, e.message)
return
}
if (delayStart) {
delay(1000L)
}
newService.start()
boxService = newService
commandServer?.setService(boxService)
status.postValue(Status.Started)
withContext(Dispatchers.Main) {
notification.show(activeProfileName, R.string.status_started)
}
notification.start()
} catch (e: Exception) {
stopAndAlert(Alert.StartService, e.message)
return
}
}
override fun serviceReload() {
notification.close()
status.postValue(Status.Starting)
val pfd = fileDescriptor
if (pfd != null) {
pfd.close()
fileDescriptor = null
}
commandServer?.setService(null)
boxService?.apply {
runCatching {
close()
}.onFailure {
writeLog("service: error when closing: $it")
}
Seq.destroyRef(refnum)
}
boxService = null
runBlocking {
startService(true)
}
}
override fun getSystemProxyStatus(): SystemProxyStatus {
val status = SystemProxyStatus()
if (service is VPNService) {
status.available = service.systemProxyAvailable
status.enabled = service.systemProxyEnabled
}
return status
}
override fun setSystemProxyEnabled(isEnabled: Boolean) {
serviceReload()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun serviceUpdateIdleMode() {
if (Application.powerManager.isDeviceIdleMode) {
boxService?.pause()
} else {
boxService?.wake()
}
}
private fun stopService() {
if (status.value != Status.Started) return
status.value = Status.Stopping
if (receiverRegistered) {
service.unregisterReceiver(receiver)
receiverRegistered = false
}
notification.close()
GlobalScope.launch(Dispatchers.IO) {
val pfd = fileDescriptor
if (pfd != null) {
pfd.close()
fileDescriptor = null
}
commandServer?.setService(null)
boxService?.apply {
runCatching {
close()
}.onFailure {
writeLog("service: error when closing: $it")
}
Seq.destroyRef(refnum)
}
boxService = null
Libbox.registerLocalDNSTransport(null)
DefaultNetworkMonitor.stop()
commandServer?.apply {
close()
Seq.destroyRef(refnum)
}
commandServer = null
Settings.startedByUser = false
withContext(Dispatchers.Main) {
status.value = Status.Stopped
service.stopSelf()
}
}
}
override fun postServiceClose() {
// Not used on Android
}
private suspend fun stopAndAlert(type: Alert, message: String? = null) {
Settings.startedByUser = false
withContext(Dispatchers.Main) {
if (receiverRegistered) {
service.unregisterReceiver(receiver)
receiverRegistered = false
}
notification.close()
binder.broadcast { callback ->
callback.onServiceAlert(type.ordinal, message)
}
status.value = Status.Stopped
}
}
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (status.value != Status.Stopped) return Service.START_NOT_STICKY
status.value = Status.Starting
if (!receiverRegistered) {
ContextCompat.registerReceiver(service, receiver, IntentFilter().apply {
addAction(Action.SERVICE_CLOSE)
addAction(Action.SERVICE_RELOAD)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
}
}, ContextCompat.RECEIVER_NOT_EXPORTED)
receiverRegistered = true
}
GlobalScope.launch(Dispatchers.IO) {
Settings.startedByUser = true
initialize()
try {
startCommandServer()
} catch (e: Exception) {
stopAndAlert(Alert.StartCommandServer, e.message)
return@launch
}
startService()
}
return Service.START_NOT_STICKY
}
fun onBind(intent: Intent): IBinder {
return binder
}
fun onDestroy() {
binder.close()
}
fun onRevoke() {
stopService()
}
fun writeLog(message: String) {
binder.broadcast {
it.onServiceWriteLog(message)
}
}
}

View File

@ -0,0 +1,180 @@
package com.hiddify.hiddify.bg
import android.annotation.TargetApi
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import android.os.Looper
import com.hiddify.hiddify.Application
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.runBlocking
import java.net.UnknownHostException
object DefaultNetworkListener {
private sealed class NetworkMessage {
class Start(val key: Any, val listener: (Network?) -> Unit) : NetworkMessage()
class Get : NetworkMessage() {
val response = CompletableDeferred<Network>()
}
class Stop(val key: Any) : NetworkMessage()
class Put(val network: Network) : NetworkMessage()
class Update(val network: Network) : NetworkMessage()
class Lost(val network: Network) : NetworkMessage()
}
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
var network: Network? = null
val pendingRequests = arrayListOf<NetworkMessage.Get>()
for (message in channel) when (message) {
is NetworkMessage.Start -> {
if (listeners.isEmpty()) register()
listeners[message.key] = message.listener
if (network != null) message.listener(network)
}
is NetworkMessage.Get -> {
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
if (network == null) pendingRequests += message else message.response.complete(
network
)
}
is NetworkMessage.Stop -> if (listeners.isNotEmpty() && // was not empty
listeners.remove(message.key) != null && listeners.isEmpty()
) {
network = null
unregister()
}
is NetworkMessage.Put -> {
network = message.network
pendingRequests.forEach { it.response.complete(message.network) }
pendingRequests.clear()
listeners.values.forEach { it(network) }
}
is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach {
it(
network
)
}
is NetworkMessage.Lost -> if (network == message.network) {
network = null
listeners.values.forEach { it(null) }
}
}
}
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(
NetworkMessage.Start(
key,
listener
)
)
suspend fun get() = if (fallback) @TargetApi(23) {
Application.connectivity.activeNetwork
?: throw UnknownHostException() // failed to listen, return current if available
} else NetworkMessage.Get().run {
networkActor.send(this)
response.await()
}
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
private object Callback : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) = runBlocking {
networkActor.send(
NetworkMessage.Put(
network
)
)
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
// it's a good idea to refresh capabilities
runBlocking { networkActor.send(NetworkMessage.Update(network)) }
}
override fun onLost(network: Network) = runBlocking {
networkActor.send(
NetworkMessage.Lost(
network
)
)
}
}
private var fallback = false
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
if (Build.VERSION.SDK_INT == 23) { // workarounds for OEM bugs
removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
}
}.build()
private val mainHandler = Handler(Looper.getMainLooper())
/**
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
*
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
* satisfies default network capabilities but only THE default network. Unfortunately, we need to have
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
*/
private fun register() {
when (Build.VERSION.SDK_INT) {
in 31..Int.MAX_VALUE -> @TargetApi(31) {
Application.connectivity.registerBestMatchingNetworkCallback(
request,
Callback,
mainHandler
)
}
in 28 until 31 -> @TargetApi(28) { // we want REQUEST here instead of LISTEN
Application.connectivity.requestNetwork(request, Callback, mainHandler)
}
in 26 until 28 -> @TargetApi(26) {
Application.connectivity.registerDefaultNetworkCallback(Callback, mainHandler)
}
in 24 until 26 -> @TargetApi(24) {
Application.connectivity.registerDefaultNetworkCallback(Callback)
}
else -> try {
fallback = false
Application.connectivity.requestNetwork(request, Callback)
} catch (e: RuntimeException) {
fallback =
true // known bug on API 23: https://stackoverflow.com/a/33509180/2245107
}
}
}
private fun unregister() {
runCatching {
Application.connectivity.unregisterNetworkCallback(Callback)
}
}
}

View File

@ -0,0 +1,67 @@
package com.hiddify.hiddify.bg
import android.net.Network
import android.os.Build
import com.hiddify.hiddify.Application
import io.nekohasekai.libbox.InterfaceUpdateListener
import java.net.NetworkInterface
object DefaultNetworkMonitor {
var defaultNetwork: Network? = null
private var listener: InterfaceUpdateListener? = null
suspend fun start() {
DefaultNetworkListener.start(this) {
defaultNetwork = it
checkDefaultInterfaceUpdate(it)
}
defaultNetwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Application.connectivity.activeNetwork
} else {
DefaultNetworkListener.get()
}
}
suspend fun stop() {
DefaultNetworkListener.stop(this)
}
suspend fun require(): Network {
val network = defaultNetwork
if (network != null) {
return network
}
return DefaultNetworkListener.get()
}
fun setListener(listener: InterfaceUpdateListener?) {
this.listener = listener
checkDefaultInterfaceUpdate(defaultNetwork)
}
private fun checkDefaultInterfaceUpdate(
newNetwork: Network?
) {
val listener = listener ?: return
if (newNetwork != null) {
val interfaceName =
(Application.connectivity.getLinkProperties(newNetwork) ?: return).interfaceName
for (times in 0 until 10) {
var interfaceIndex: Int
try {
interfaceIndex = NetworkInterface.getByName(interfaceName).index
} catch (e: Exception) {
Thread.sleep(100)
continue
}
listener.updateDefaultInterface(interfaceName, interfaceIndex)
}
} else {
listener.updateDefaultInterface("", -1)
}
}
}

View File

@ -0,0 +1,134 @@
package com.hiddify.hiddify.bg
import android.net.DnsResolver
import android.os.Build
import android.os.CancellationSignal
import android.system.ErrnoException
import androidx.annotation.RequiresApi
import com.hiddify.hiddify.ktx.tryResumeWithException
import io.nekohasekai.libbox.ExchangeContext
import io.nekohasekai.libbox.LocalDNSTransport
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.runBlocking
import java.net.InetAddress
import java.net.UnknownHostException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object LocalResolver : LocalDNSTransport {
private const val RCODE_NXDOMAIN = 3
override fun raw(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
@RequiresApi(Build.VERSION_CODES.Q)
override fun exchange(ctx: ExchangeContext, message: ByteArray) {
return runBlocking {
val defaultNetwork = DefaultNetworkMonitor.require()
suspendCoroutine { continuation ->
val signal = CancellationSignal()
ctx.onCancel(signal::cancel)
val callback = object : DnsResolver.Callback<ByteArray> {
override fun onAnswer(answer: ByteArray, rcode: Int) {
if (rcode == 0) {
ctx.rawSuccess(answer)
} else {
ctx.errorCode(rcode)
}
continuation.resume(Unit)
}
override fun onError(error: DnsResolver.DnsException) {
when (val cause = error.cause) {
is ErrnoException -> {
ctx.errnoCode(cause.errno)
continuation.resume(Unit)
return
}
}
continuation.tryResumeWithException(error)
}
}
DnsResolver.getInstance().rawQuery(
defaultNetwork,
message,
DnsResolver.FLAG_NO_RETRY,
Dispatchers.IO.asExecutor(),
signal,
callback
)
}
}
}
override fun lookup(ctx: ExchangeContext, network: String, domain: String) {
return runBlocking {
val defaultNetwork = DefaultNetworkMonitor.require()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
suspendCoroutine { continuation ->
val signal = CancellationSignal()
ctx.onCancel(signal::cancel)
val callback = object : DnsResolver.Callback<Collection<InetAddress>> {
@Suppress("ThrowableNotThrown")
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) {
if (rcode == 0) {
ctx.success((answer as Collection<InetAddress?>).mapNotNull { it?.hostAddress }
.joinToString("\n"))
} else {
ctx.errorCode(rcode)
}
continuation.resume(Unit)
}
override fun onError(error: DnsResolver.DnsException) {
when (val cause = error.cause) {
is ErrnoException -> {
ctx.errnoCode(cause.errno)
continuation.resume(Unit)
return
}
}
continuation.tryResumeWithException(error)
}
}
val type = when {
network.endsWith("4") -> DnsResolver.TYPE_A
network.endsWith("6") -> DnsResolver.TYPE_AAAA
else -> null
}
if (type != null) {
DnsResolver.getInstance().query(
defaultNetwork,
domain,
type,
DnsResolver.FLAG_NO_RETRY,
Dispatchers.IO.asExecutor(),
signal,
callback
)
} else {
DnsResolver.getInstance().query(
defaultNetwork,
domain,
DnsResolver.FLAG_NO_RETRY,
Dispatchers.IO.asExecutor(),
signal,
callback
)
}
}
} else {
val answer = try {
defaultNetwork.getAllByName(domain)
} catch (e: UnknownHostException) {
ctx.errorCode(RCODE_NXDOMAIN)
return@runBlocking
}
ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n"))
}
}
}
}

View File

@ -0,0 +1,156 @@
package com.hiddify.hiddify.bg
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
import androidx.annotation.RequiresApi
import com.hiddify.hiddify.Application
import io.nekohasekai.libbox.InterfaceUpdateListener
import io.nekohasekai.libbox.NetworkInterfaceIterator
import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.StringIterator
import io.nekohasekai.libbox.TunOptions
import io.nekohasekai.libbox.WIFIState
import java.net.Inet6Address
import java.net.InetSocketAddress
import java.net.InterfaceAddress
import java.net.NetworkInterface
import java.util.Enumeration
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
interface PlatformInterfaceWrapper : PlatformInterface {
override fun usePlatformAutoDetectInterfaceControl(): Boolean {
return true
}
override fun autoDetectInterfaceControl(fd: Int) {
}
override fun openTun(options: TunOptions): Int {
error("invalid argument")
}
override fun useProcFS(): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
}
@RequiresApi(Build.VERSION_CODES.Q)
override fun findConnectionOwner(
ipProtocol: Int,
sourceAddress: String,
sourcePort: Int,
destinationAddress: String,
destinationPort: Int
): Int {
val uid = Application.connectivity.getConnectionOwnerUid(
ipProtocol,
InetSocketAddress(sourceAddress, sourcePort),
InetSocketAddress(destinationAddress, destinationPort)
)
if (uid == Process.INVALID_UID) error("android: connection owner not found")
return uid
}
override fun packageNameByUid(uid: Int): String {
val packages = Application.packageManager.getPackagesForUid(uid)
if (packages.isNullOrEmpty()) error("android: package not found")
return packages[0]
}
@Suppress("DEPRECATION")
override fun uidByPackageName(packageName: String): Int {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Application.packageManager.getPackageUid(
packageName, PackageManager.PackageInfoFlags.of(0)
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Application.packageManager.getPackageUid(packageName, 0)
} else {
Application.packageManager.getApplicationInfo(packageName, 0).uid
}
} catch (e: PackageManager.NameNotFoundException) {
error("android: package not found")
}
}
override fun usePlatformDefaultInterfaceMonitor(): Boolean {
return true
}
override fun startDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
DefaultNetworkMonitor.setListener(listener)
}
override fun closeDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
DefaultNetworkMonitor.setListener(null)
}
override fun usePlatformInterfaceGetter(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
override fun getInterfaces(): NetworkInterfaceIterator {
return InterfaceArray(NetworkInterface.getNetworkInterfaces())
}
override fun underNetworkExtension(): Boolean {
return false
}
override fun includeAllNetworks(): Boolean {
return false
}
override fun clearDNSCache() {
}
override fun readWIFIState(): WIFIState? {
return null
}
private class InterfaceArray(private val iterator: Enumeration<NetworkInterface>) :
NetworkInterfaceIterator {
override fun hasNext(): Boolean {
return iterator.hasMoreElements()
}
override fun next(): LibboxNetworkInterface {
val element = iterator.nextElement()
return LibboxNetworkInterface().apply {
name = element.name
index = element.index
runCatching {
mtu = element.mtu
}
addresses =
StringArray(
element.interfaceAddresses.mapTo(mutableListOf()) { it.toPrefix() }
.iterator()
)
}
}
private fun InterfaceAddress.toPrefix(): String {
return if (address is Inet6Address) {
"${Inet6Address.getByAddress(address.address).hostAddress}/${networkPrefixLength}"
} else {
"${address.hostAddress}/${networkPrefixLength}"
}
}
}
private class StringArray(private val iterator: Iterator<String>) : StringIterator {
override fun hasNext(): Boolean {
return iterator.hasNext()
}
override fun next(): String {
return iterator.next()
}
}
}

View File

@ -0,0 +1,17 @@
package com.hiddify.hiddify.bg
import android.app.Service
import android.content.Intent
class ProxyService : Service(), PlatformInterfaceWrapper {
private val service = BoxService(this, this)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) =
service.onStartCommand(intent, flags, startId)
override fun onBind(intent: Intent) = service.onBind(intent)
override fun onDestroy() = service.onDestroy()
override fun writeLog(message: String) = service.writeLog(message)
}

View File

@ -0,0 +1,59 @@
package com.hiddify.hiddify.bg
import android.os.RemoteCallbackList
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.IService
import com.hiddify.hiddify.IServiceCallback
import com.hiddify.hiddify.constant.Status
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class ServiceBinder(private val status: MutableLiveData<Status>) : IService.Stub() {
private val callbacks = RemoteCallbackList<IServiceCallback>()
private val broadcastLock = Mutex()
init {
status.observeForever {
broadcast { callback ->
callback.onServiceStatusChanged(it.ordinal)
}
}
}
fun broadcast(work: (IServiceCallback) -> Unit) {
GlobalScope.launch(Dispatchers.Main) {
broadcastLock.withLock {
val count = callbacks.beginBroadcast()
try {
repeat(count) {
try {
work(callbacks.getBroadcastItem(it))
} catch (_: Exception) {
}
}
} finally {
callbacks.finishBroadcast()
}
}
}
}
override fun getStatus(): Int {
return (status.value ?: Status.Stopped).ordinal
}
override fun registerCallback(callback: IServiceCallback) {
callbacks.register(callback)
}
override fun unregisterCallback(callback: IServiceCallback?) {
callbacks.unregister(callback)
}
fun close() {
callbacks.kill()
}
}

View File

@ -0,0 +1,109 @@
package com.hiddify.hiddify.bg
import com.hiddify.hiddify.IService
import com.hiddify.hiddify.IServiceCallback
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class ServiceConnection(
private val context: Context,
callback: Callback,
private val register: Boolean = true,
) : ServiceConnection {
companion object {
private const val TAG = "ServiceConnection"
}
private val callback = ServiceCallback(callback)
private var service: IService? = null
val status get() = service?.status?.let { Status.values()[it] } ?: Status.Stopped
fun connect() {
val intent = runBlocking {
withContext(Dispatchers.IO) {
Intent(context, Settings.serviceClass()).setAction(Action.SERVICE)
}
}
context.bindService(intent, this, AppCompatActivity.BIND_AUTO_CREATE)
}
fun disconnect() {
try {
context.unbindService(this)
} catch (_: IllegalArgumentException) {
}
}
fun reconnect() {
try {
context.unbindService(this)
} catch (_: IllegalArgumentException) {
}
val intent = runBlocking {
withContext(Dispatchers.IO) {
Intent(context, Settings.serviceClass()).setAction(Action.SERVICE)
}
}
context.bindService(intent, this, AppCompatActivity.BIND_AUTO_CREATE)
}
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
val service = IService.Stub.asInterface(binder)
this.service = service
try {
if (register) service.registerCallback(callback)
callback.onServiceStatusChanged(service.status)
} catch (e: RemoteException) {
Log.e(TAG, "initialize service connection", e)
}
}
override fun onServiceDisconnected(name: ComponentName?) {
try {
service?.unregisterCallback(callback)
} catch (e: RemoteException) {
Log.e(TAG, "cleanup service connection", e)
}
}
override fun onBindingDied(name: ComponentName?) {
reconnect()
}
interface Callback {
fun onServiceStatusChanged(status: Status)
fun onServiceAlert(type: Alert, message: String?) {}
fun onServiceWriteLog(message: String?) {}
fun onServiceResetLogs(messages: MutableList<String>) {}
}
class ServiceCallback(private val callback: Callback) : IServiceCallback.Stub() {
override fun onServiceStatusChanged(status: Int) {
callback.onServiceStatusChanged(Status.values()[status])
}
override fun onServiceAlert(type: Int, message: String?) {
callback.onServiceAlert(Alert.values()[type], message)
}
override fun onServiceWriteLog(message: String?) = callback.onServiceWriteLog(message)
override fun onServiceResetLogs(messages: MutableList<String>) =
callback.onServiceResetLogs(messages)
}
}

View File

@ -0,0 +1,143 @@
package com.hiddify.hiddify.bg
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.Application
import com.hiddify.hiddify.MainActivity
import com.hiddify.hiddify.R
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Status
import com.hiddify.hiddify.utils.CommandClient
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.StatusMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.withContext
class ServiceNotification(private val status: MutableLiveData<Status>, private val service: Service) : BroadcastReceiver(), CommandClient.Handler {
companion object {
private const val notificationId = 1
private const val notificationChannel = "service"
private val flags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
fun checkPermission(): Boolean {
// 暂时禁用通知权限检查
return true
}
}
private val commandClient =
CommandClient(GlobalScope, CommandClient.ConnectionType.Status, this)
private var receiverRegistered = false
private val notificationBuilder by lazy {
NotificationCompat.Builder(service, notificationChannel)
.setShowWhen(false)
.setOngoing(true)
.setContentTitle("BearVPN")
.setOnlyAlertOnce(true)
.setSmallIcon(R.drawable.ic_stat_logo)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setContentIntent(
PendingIntent.getActivity(
service,
0,
Intent(
service,
MainActivity::class.java
).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
flags
)
)
.setPriority(NotificationCompat.PRIORITY_LOW).apply {
addAction(
NotificationCompat.Action.Builder(
0, service.getText(R.string.stop), PendingIntent.getBroadcast(
service,
0,
Intent(Action.SERVICE_CLOSE).setPackage(service.packageName),
flags
)
).build()
)
}
}
fun show(profileName: String, @StringRes contentTextId: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Application.notification.createNotificationChannel(
NotificationChannel(
notificationChannel, "hiddify service", NotificationManager.IMPORTANCE_LOW
)
)
}
service.startForeground(
notificationId, notificationBuilder
.setContentTitle(profileName.takeIf { it.isNotBlank() } ?: "Hiddify")
.setContentText(service.getString(contentTextId)).build()
)
}
suspend fun start() {
if (Settings.dynamicNotification) {
commandClient.connect()
withContext(Dispatchers.Main) {
registerReceiver()
}
}
}
private fun registerReceiver() {
service.registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
})
receiverRegistered = true
}
override fun updateStatus(status: StatusMessage) {
val content =
Libbox.formatBytes(status.uplink) + "/s ↑\t" + Libbox.formatBytes(status.downlink) + "/s ↓"
Application.notificationManager.notify(
notificationId,
notificationBuilder.setContentText(content).build()
)
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_SCREEN_ON -> {
commandClient.connect()
}
Intent.ACTION_SCREEN_OFF -> {
commandClient.disconnect()
}
}
}
fun close() {
commandClient.disconnect()
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE)
if (receiverRegistered) {
service.unregisterReceiver(this)
receiverRegistered = false
}
}
}

View File

@ -0,0 +1,48 @@
package com.hiddify.hiddify.bg
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import com.hiddify.hiddify.constant.Status
@RequiresApi(24)
class TileService : TileService(), ServiceConnection.Callback {
private val connection = ServiceConnection(this, this)
override fun onServiceStatusChanged(status: Status) {
qsTile?.apply {
state = when (status) {
Status.Started -> Tile.STATE_ACTIVE
Status.Stopped -> Tile.STATE_INACTIVE
else -> Tile.STATE_UNAVAILABLE
}
updateTile()
}
}
override fun onStartListening() {
super.onStartListening()
connection.connect()
}
override fun onStopListening() {
connection.disconnect()
super.onStopListening()
}
override fun onClick() {
when (connection.status) {
Status.Stopped -> {
BoxService.start()
}
Status.Started -> {
BoxService.stop()
}
else -> {}
}
}
}

View File

@ -0,0 +1,199 @@
package com.hiddify.hiddify.bg
import android.util.Log
import com.hiddify.hiddify.Settings
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import com.hiddify.hiddify.constant.PerAppProxyMode
import com.hiddify.hiddify.ktx.toIpPrefix
import io.nekohasekai.libbox.TunOptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class VPNService : VpnService(), PlatformInterfaceWrapper {
companion object {
private const val TAG = "A/VPNService"
}
private val service = BoxService(this, this)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) =
service.onStartCommand(intent, flags, startId)
override fun onBind(intent: Intent): IBinder {
val binder = super.onBind(intent)
if (binder != null) {
return binder
}
return service.onBind(intent)
}
override fun onDestroy() {
service.onDestroy()
}
override fun onRevoke() {
runBlocking {
withContext(Dispatchers.Main) {
service.onRevoke()
}
}
}
override fun autoDetectInterfaceControl(fd: Int) {
protect(fd)
}
var systemProxyAvailable = false
var systemProxyEnabled = false
fun addIncludePackage(builder: Builder, packageName: String) {
if (packageName == this.packageName) {
Log.d("VpnService","Cannot include myself: $packageName")
return
}
try {
Log.d("VpnService","Including $packageName")
builder.addAllowedApplication(packageName)
} catch (e: NameNotFoundException) {
}
}
fun addExcludePackage(builder: Builder, packageName: String) {
try {
Log.d("VpnService","Excluding $packageName")
builder.addDisallowedApplication(packageName)
} catch (e: NameNotFoundException) {
}
}
override fun openTun(options: TunOptions): Int {
if (prepare(this) != null) error("android: missing vpn permission")
val builder = Builder()
.setSession("sing-box")
.setMtu(options.mtu)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setMetered(false)
}
val inet4Address = options.inet4Address
while (inet4Address.hasNext()) {
val address = inet4Address.next()
builder.addAddress(address.address(), address.prefix())
}
val inet6Address = options.inet6Address
while (inet6Address.hasNext()) {
val address = inet6Address.next()
builder.addAddress(address.address(), address.prefix())
}
if (options.autoRoute) {
builder.addDnsServer(options.dnsServerAddress)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val inet4RouteAddress = options.inet4RouteAddress
if (inet4RouteAddress.hasNext()) {
while (inet4RouteAddress.hasNext()) {
builder.addRoute(inet4RouteAddress.next().toIpPrefix())
}
} else {
builder.addRoute("0.0.0.0", 0)
}
val inet6RouteAddress = options.inet6RouteAddress
if (inet6RouteAddress.hasNext()) {
while (inet6RouteAddress.hasNext()) {
builder.addRoute(inet6RouteAddress.next().toIpPrefix())
}
} else {
builder.addRoute("::", 0)
}
val inet4RouteExcludeAddress = options.inet4RouteExcludeAddress
while (inet4RouteExcludeAddress.hasNext()) {
builder.excludeRoute(inet4RouteExcludeAddress.next().toIpPrefix())
}
val inet6RouteExcludeAddress = options.inet6RouteExcludeAddress
while (inet6RouteExcludeAddress.hasNext()) {
builder.excludeRoute(inet6RouteExcludeAddress.next().toIpPrefix())
}
} else {
val inet4RouteAddress = options.inet4RouteRange
if (inet4RouteAddress.hasNext()) {
while (inet4RouteAddress.hasNext()) {
val address = inet4RouteAddress.next()
builder.addRoute(address.address(), address.prefix())
}
}
val inet6RouteAddress = options.inet6RouteRange
if (inet6RouteAddress.hasNext()) {
while (inet6RouteAddress.hasNext()) {
val address = inet6RouteAddress.next()
builder.addRoute(address.address(), address.prefix())
}
}
}
if (Settings.perAppProxyEnabled) {
val appList = Settings.perAppProxyList
if (Settings.perAppProxyMode == PerAppProxyMode.INCLUDE) {
appList.forEach {
addIncludePackage(builder,it)
}
addIncludePackage(builder,packageName)
} else {
appList.forEach {
addExcludePackage(builder,it)
}
//addExcludePackage(builder,packageName)
}
} else {
val includePackage = options.includePackage
if (includePackage.hasNext()) {
while (includePackage.hasNext()) {
addIncludePackage(builder,includePackage.next())
}
}
val excludePackage = options.excludePackage
if (excludePackage.hasNext()) {
while (excludePackage.hasNext()) {
addExcludePackage(builder,excludePackage.next())
}
}
//addExcludePackage(builder,packageName)
}
}
if (options.isHTTPProxyEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
systemProxyAvailable = true
systemProxyEnabled = Settings.systemProxyEnabled
if (systemProxyEnabled) builder.setHttpProxy(
ProxyInfo.buildDirectProxy(
options.httpProxyServer, options.httpProxyServerPort
)
)
} else {
systemProxyAvailable = false
systemProxyEnabled = false
}
val pfd =
builder.establish() ?: error("android: the application is not prepared or is revoked")
service.fileDescriptor = pfd
return pfd.fd
}
override fun writeLog(message: String) = service.writeLog(message)
}

View File

@ -0,0 +1,7 @@
package com.hiddify.hiddify.constant
object Action {
const val SERVICE = "com.baer.app.SERVICE"
const val SERVICE_CLOSE = "com.baer.app.SERVICE_CLOSE"
const val SERVICE_RELOAD = "com.baer.app.sfa.SERVICE_RELOAD"
}

View File

@ -0,0 +1,10 @@
package com.hiddify.hiddify.constant
enum class Alert {
RequestVPNPermission,
RequestNotificationPermission,
EmptyConfiguration,
StartCommandServer,
CreateService,
StartService
}

View File

@ -0,0 +1,7 @@
package com.hiddify.hiddify.constant
object PerAppProxyMode {
const val OFF = "off"
const val INCLUDE = "include"
const val EXCLUDE = "exclude"
}

View File

@ -0,0 +1,6 @@
package com.hiddify.hiddify.constant
object ServiceMode {
const val NORMAL = "proxy"
const val VPN = "vpn"
}

View File

@ -0,0 +1,24 @@
package com.hiddify.hiddify.constant
object SettingsKey {
private const val KEY_PREFIX = "flutter."
const val SERVICE_MODE = "${KEY_PREFIX}service-mode"
const val ACTIVE_CONFIG_PATH = "${KEY_PREFIX}active_config_path"
const val ACTIVE_PROFILE_NAME = "${KEY_PREFIX}active_profile_name"
const val PER_APP_PROXY_MODE = "${KEY_PREFIX}per_app_proxy_mode"
const val PER_APP_PROXY_INCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_include_list"
const val PER_APP_PROXY_EXCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_exclude_list"
const val DEBUG_MODE = "${KEY_PREFIX}debug_mode"
const val DISABLE_MEMORY_LIMIT = "${KEY_PREFIX}disable_memory_limit"
const val DYNAMIC_NOTIFICATION = "${KEY_PREFIX}dynamic_notification"
const val SYSTEM_PROXY_ENABLED = "${KEY_PREFIX}system_proxy_enabled"
// cache
const val STARTED_BY_USER = "${KEY_PREFIX}started_by_user"
const val CONFIG_OPTIONS = "config_options_json"
}

View File

@ -0,0 +1,8 @@
package com.hiddify.hiddify.constant
enum class Status {
Stopped,
Starting,
Started,
Stopping,
}

View File

@ -0,0 +1,18 @@
package com.hiddify.hiddify.ktx
import kotlin.coroutines.Continuation
fun <T> Continuation<T>.tryResume(value: T) {
try {
resumeWith(Result.success(value))
} catch (ignored: IllegalStateException) {
}
}
fun <T> Continuation<T>.tryResumeWithException(exception: Throwable) {
try {
resumeWith(Result.failure(exception))
} catch (ignored: IllegalStateException) {
}
}

View File

@ -0,0 +1,19 @@
package com.hiddify.hiddify.ktx
import android.net.IpPrefix
import android.os.Build
import androidx.annotation.RequiresApi
import io.nekohasekai.libbox.RoutePrefix
import io.nekohasekai.libbox.StringIterator
import java.net.InetAddress
fun StringIterator.toList(): List<String> {
return mutableListOf<String>().apply {
while (hasNext()) {
add(next())
}
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun RoutePrefix.toIpPrefix() = IpPrefix(InetAddress.getByName(address()), prefix())

View File

@ -0,0 +1,139 @@
package com.hiddify.hiddify.utils
import go.Seq
import io.nekohasekai.libbox.CommandClient
import io.nekohasekai.libbox.CommandClientHandler
import io.nekohasekai.libbox.CommandClientOptions
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.OutboundGroup
import io.nekohasekai.libbox.OutboundGroupIterator
import io.nekohasekai.libbox.StatusMessage
import io.nekohasekai.libbox.StringIterator
import com.hiddify.hiddify.ktx.toList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
open class CommandClient(
private val scope: CoroutineScope,
private val connectionType: ConnectionType,
private val handler: Handler
) {
enum class ConnectionType {
Status, Groups, Log, ClashMode, GroupOnly
}
interface Handler {
fun onConnected() {}
fun onDisconnected() {}
fun updateStatus(status: StatusMessage) {}
fun updateGroups(groups: List<OutboundGroup>) {}
fun clearLog() {}
fun appendLog(message: String) {}
fun initializeClashMode(modeList: List<String>, currentMode: String) {}
fun updateClashMode(newMode: String) {}
}
private var commandClient: CommandClient? = null
private val clientHandler = ClientHandler()
fun connect() {
disconnect()
val options = CommandClientOptions()
options.command = when (connectionType) {
ConnectionType.Status -> Libbox.CommandStatus
ConnectionType.Groups -> Libbox.CommandGroup
ConnectionType.Log -> Libbox.CommandLog
ConnectionType.ClashMode -> Libbox.CommandClashMode
ConnectionType.GroupOnly -> Libbox.CommandGroupInfoOnly
}
options.statusInterval = 2 * 1000 * 1000 * 1000
val commandClient = CommandClient(clientHandler, options)
scope.launch(Dispatchers.IO) {
for (i in 1..10) {
delay(100 + i.toLong() * 50)
try {
commandClient.connect()
} catch (ignored: Exception) {
continue
}
if (!isActive) {
runCatching {
commandClient.disconnect()
}
return@launch
}
this@CommandClient.commandClient = commandClient
return@launch
}
runCatching {
commandClient.disconnect()
}
}
}
fun disconnect() {
commandClient?.apply {
runCatching {
disconnect()
}
Seq.destroyRef(refnum)
}
commandClient = null
}
private inner class ClientHandler : CommandClientHandler {
override fun connected() {
handler.onConnected()
}
override fun disconnected(message: String?) {
handler.onDisconnected()
}
override fun writeGroups(message: OutboundGroupIterator?) {
if (message == null) {
return
}
val groups = mutableListOf<OutboundGroup>()
while (message.hasNext()) {
groups.add(message.next())
}
handler.updateGroups(groups)
}
override fun clearLog() {
handler.clearLog()
}
override fun writeLog(message: String?) {
if (message == null) {
return
}
handler.appendLog(message)
}
override fun writeStatus(message: StatusMessage?) {
if (message == null) {
return
}
handler.updateStatus(message)
}
override fun initializeClashMode(modeList: StringIterator, currentMode: String) {
handler.initializeClashMode(modeList.toList(), currentMode)
}
override fun updateClashMode(newMode: String) {
handler.updateClashMode(newMode)
}
}
}

View File

@ -0,0 +1,31 @@
package com.hiddify.hiddify.utils
import com.google.gson.annotations.SerializedName
import io.nekohasekai.libbox.OutboundGroup
import io.nekohasekai.libbox.OutboundGroupItem
data class ParsedOutboundGroup(
@SerializedName("tag") val tag: String,
@SerializedName("type") val type: String,
@SerializedName("selected") val selected: String,
@SerializedName("items") val items: List<ParsedOutboundGroupItem>
) {
companion object {
fun fromOutbound(group: OutboundGroup): ParsedOutboundGroup {
val outboundItems = group.items
val items = mutableListOf<ParsedOutboundGroupItem>()
while (outboundItems.hasNext()) {
items.add(ParsedOutboundGroupItem(outboundItems.next()))
}
return ParsedOutboundGroup(group.tag, group.type, group.selected, items)
}
}
}
data class ParsedOutboundGroupItem(
@SerializedName("tag") val tag: String,
@SerializedName("type") val type: String,
@SerializedName("url-test-delay") val urlTestDelay: Int,
) {
constructor(item: OutboundGroupItem) : this(item.tag, item.type, item.urlTestDelay)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 背景色 -->
<!-- 启动图片 -->
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash"
android:width="50dp"
android:height="50dp" />
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,53 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="640dp"
android:height="360dp"
android:viewportWidth="640"
android:viewportHeight="360">
<group android:scaleX="0.6666667"
android:scaleY="0.6666667"
android:translateX="110"
android:translateY="65">
<group>
<clip-path
android:pathData="M60,115.28h114.5v113.28h-114.5z"/>
<path
android:pathData="M144.37,140.82C144.37,139.77 144.92,138.8 145.83,138.26L169.93,123.95C171.94,122.76 174.49,124.19 174.49,126.51V151.05C174.49,152.7 173.15,154.03 171.48,154.03H147.38C145.71,154.03 144.37,152.7 144.37,151.05V140.82Z"
android:fillColor="#455FE9"/>
<path
android:pathData="M102.18,164.67C102.18,163.62 102.74,162.65 103.64,162.11L127.75,147.8C129.76,146.61 132.31,148.04 132.31,150.36V192.79V195.77V222.6C132.31,224.25 130.96,225.58 129.3,225.58H105.2C103.53,225.58 102.18,224.25 102.18,222.6V195.77V192.79V164.67ZM61.46,188.94C60.56,189.48 60,190.45 60,191.5V222.6C60,224.25 61.35,225.58 63.01,225.58H87.12C88.78,225.58 90.13,224.25 90.13,222.6V177.19C90.13,174.87 87.58,173.44 85.57,174.63L61.46,188.94Z"
android:fillColor="#455FE9"
android:fillType="evenOdd"/>
<path
android:pathData="M90.13,195.77H72.05V219.62H90.13V213.66C90.13,212.01 91.48,210.68 93.14,210.68H99.17C100.83,210.68 102.18,212.01 102.18,213.66V219.62H126.29V195.77H102.18V201.73C102.18,203.38 100.83,204.71 99.17,204.71H93.14C91.48,204.71 90.13,203.38 90.13,201.73V195.77Z"
android:fillColor="#455FE9"
android:fillType="evenOdd"/>
<path
android:pathData="M144.37,162.98V222.6C144.37,224.25 145.71,225.58 147.38,225.58H171.48C173.15,225.58 174.49,224.25 174.49,222.6V162.98C174.49,161.33 173.15,160 171.48,160H147.38C145.71,160 144.37,161.33 144.37,162.98Z"
android:fillColor="#455FE9"/>
</group>
<path
android:pathData="M536.46,237.06C531.61,237.06 528.04,235.92 525.75,233.66C523.5,231.43 522.38,227.88 522.38,223.01C522.38,221.42 523.18,220.63 524.78,220.63C526.38,220.63 527.18,221.42 527.18,223.01C527.18,226.56 527.85,228.99 529.19,230.32C530.56,231.64 532.98,232.3 536.46,232.3H555.46C558.97,232.3 561.4,231.64 562.73,230.32C564.07,228.99 564.74,226.56 564.74,223.01V207.04C562.64,209.68 559.89,211.59 556.49,212.76C553.14,213.93 549.13,214.51 544.47,214.51H542.07C535.2,214.51 530.2,212.97 527.07,209.87C523.94,206.77 522.38,201.86 522.38,195.14V164.44C522.38,162.86 523.18,162.07 524.78,162.07C526.38,162.07 527.18,162.86 527.18,164.44V195.14C527.18,200.58 528.27,204.38 530.45,206.53C532.66,208.68 536.53,209.76 542.07,209.76H544.47C551.3,209.76 556.38,208.55 559.7,206.13C563.06,203.68 564.74,200.01 564.74,195.14V164.44C564.74,162.86 565.54,162.07 567.14,162.07C568.75,162.07 569.55,162.86 569.55,164.44V223.01C569.55,227.88 568.42,231.43 566.17,233.66C563.92,235.92 560.35,237.06 555.46,237.06H536.46Z"
android:fillColor="#495057"/>
<path
android:pathData="M491.05,214.4C489.45,214.4 488.64,213.61 488.64,212.02V166.94H483.61C482,166.94 481.2,166.15 481.2,164.56C481.2,162.97 482,162.18 483.61,162.18H488.64V155.72C488.64,148.93 490.21,143.98 493.34,140.88C496.47,137.79 501.47,136.24 508.34,136.24C510.02,136.24 510.86,137.03 510.86,138.62C510.86,140.2 510.02,141 508.34,141C502.84,141 498.99,142.09 496.77,144.28C494.56,146.47 493.45,150.29 493.45,155.72V162.18H508.45C510.05,162.18 510.86,162.97 510.86,164.56C510.86,166.15 510.05,166.94 508.45,166.94H493.45V212.02C493.45,213.61 492.65,214.4 491.05,214.4Z"
android:fillColor="#495057"/>
<path
android:pathData="M463.89,151.53C461.6,151.53 460.46,150.36 460.46,148.02V145.7C460.46,143.43 461.6,142.3 463.89,142.3H466.24C468.49,142.3 469.62,143.43 469.62,145.7V148.02C469.62,150.36 468.49,151.53 466.24,151.53H463.89ZM465.1,214.4C463.49,214.4 462.69,213.61 462.69,212.02V164.56C462.69,162.97 463.49,162.18 465.1,162.18C466.7,162.18 467.5,162.97 467.5,164.56V212.02C467.5,213.61 466.7,214.4 465.1,214.4Z"
android:fillColor="#495057"/>
<path
android:pathData="M413.71,214.4C406.73,214.4 401.63,212.84 398.43,209.7C395.26,206.53 393.68,201.49 393.68,194.58V182C393.68,175.09 395.26,170.07 398.43,166.94C401.63,163.77 406.73,162.18 413.71,162.18H438.33V138.62C438.33,137.03 439.13,136.24 440.73,136.24C442.34,136.24 443.14,137.03 443.14,138.62V212.02C443.14,213.61 442.34,214.4 440.73,214.4C439.13,214.4 438.33,213.61 438.33,212.02V205.79C436.73,208.63 434.28,210.78 431,212.25C427.76,213.68 423.45,214.4 418.06,214.4H413.71ZM413.71,209.64H418.06C424.32,209.64 429.27,208.63 432.89,206.59C436.52,204.51 438.33,201.41 438.33,197.3V166.94H413.71C408.1,166.94 404.15,168.07 401.86,170.34C399.61,172.56 398.48,176.45 398.48,182V194.58C398.48,200.13 399.61,204.04 401.86,206.3C404.15,208.53 408.1,209.64 413.71,209.64Z"
android:fillColor="#495057"/>
<path
android:pathData="M345.84,214.4C338.86,214.4 333.76,212.84 330.56,209.7C327.39,206.53 325.81,201.49 325.81,194.58V182C325.81,175.09 327.39,170.07 330.56,166.94C333.76,163.77 338.86,162.18 345.84,162.18H370.46V138.62C370.46,137.03 371.26,136.24 372.86,136.24C374.47,136.24 375.27,137.03 375.27,138.62V212.02C375.27,213.61 374.47,214.4 372.86,214.4C371.26,214.4 370.46,213.61 370.46,212.02V205.79C368.86,208.63 366.41,210.78 363.13,212.25C359.89,213.68 355.58,214.4 350.19,214.4H345.84ZM345.84,209.64H350.19C356.45,209.64 361.39,208.63 365.02,206.59C368.65,204.51 370.46,201.41 370.46,197.3V166.94H345.84C340.23,166.94 336.28,168.07 333.99,170.34C331.74,172.56 330.61,176.45 330.61,182V194.58C330.61,200.13 331.74,204.04 333.99,206.3C336.28,208.53 340.23,209.64 345.84,209.64Z"
android:fillColor="#495057"/>
<path
android:pathData="M302.77,151.53C300.48,151.53 299.34,150.36 299.34,148.02V145.7C299.34,143.43 300.48,142.3 302.77,142.3H305.12C307.37,142.3 308.5,143.43 308.5,145.7V148.02C308.5,150.36 307.37,151.53 305.12,151.53H302.77ZM303.97,214.4C302.37,214.4 301.57,213.61 301.57,212.02V164.56C301.57,162.97 302.37,162.18 303.97,162.18C305.58,162.18 306.38,162.97 306.38,164.56V212.02C306.38,213.61 305.58,214.4 303.97,214.4Z"
android:fillColor="#495057"/>
<path
android:pathData="M225.37,214.4C224.57,214.4 223.96,214.21 223.54,213.84C223.16,213.42 222.97,212.82 222.97,212.02V145.07C222.97,144.28 223.16,143.7 223.54,143.32C223.96,142.9 224.57,142.7 225.37,142.7C226.17,142.7 226.76,142.9 227.15,143.32C227.57,143.7 227.77,144.28 227.77,145.07V175.66H276.21V145.07C276.21,144.28 276.4,143.7 276.78,143.32C277.2,142.9 277.81,142.7 278.61,142.7C279.41,142.7 280,142.9 280.39,143.32C280.8,143.7 281.02,144.28 281.02,145.07V212.02C281.02,212.82 280.8,213.42 280.39,213.84C280,214.21 279.41,214.4 278.61,214.4C277.81,214.4 277.2,214.21 276.78,213.84C276.4,213.42 276.21,212.82 276.21,212.02V180.42H227.77V212.02C227.77,212.82 227.57,213.42 227.15,213.84C226.76,214.21 226.17,214.4 225.37,214.4Z"
android:fillColor="#495057"/>
</group>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="2048dp"
android:height="2048dp"
android:viewportWidth="2048"
android:viewportHeight="2048">
<group>
<!-- Filled rectangle -->
<path
android:pathData="M0,0 L2048,0 L2048,2048 L0,2048 Z"
android:fillColor="#FFF0F3FA" />
</group>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash"
android:width="50dp"
android:height="50dp" />
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_banner_background"/>
<foreground android:drawable="@drawable/ic_banner_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?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"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?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"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

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