初始化提交
45
.github/workflows/build-windows.yml
vendored
Executable 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
@ -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
|
||||
BIN
.gradle/8.10/checksums/checksums.lock
Normal file
0
.gradle/8.10/dependencies-accessors/gc.properties
Normal file
BIN
.gradle/8.10/executionHistory/executionHistory.lock
Normal file
BIN
.gradle/8.10/fileChanges/last-build.bin
Normal file
BIN
.gradle/8.10/fileHashes/fileHashes.lock
Normal file
0
.gradle/8.10/gc.properties
Normal file
BIN
.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
2
.gradle/buildOutputCleanup/cache.properties
Normal file
@ -0,0 +1,2 @@
|
||||
#Tue Sep 23 15:58:20 CST 2025
|
||||
gradle.version=8.10
|
||||
2
.gradle/config.properties
Normal 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
|
||||
0
.gradle/vcs-1/gc.properties
Normal file
45
.metadata
Executable 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,4 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
linter:
|
||||
rules:
|
||||
|
||||
16
android/.gitignore
vendored
Executable 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
@ -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
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
7
android/app/src/debug/AndroidManifest.xml
Executable 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>
|
||||
149
android/app/src/main/AndroidManifest.xml
Executable 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>
|
||||
9
android/app/src/main/aidl/com/hiddify/hiddify/IService.aidl
Executable 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);
|
||||
}
|
||||
8
android/app/src/main/aidl/com/hiddify/hiddify/IServiceCallback.aidl
Executable 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);
|
||||
}
|
||||
BIN
android/app/src/main/ic_launcher-playstore.png
Executable file
|
After Width: | Height: | Size: 27 KiB |
60
android/app/src/main/kotlin/com/hiddify/hiddify/ActiveGroupsChannel.kt
Executable 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)
|
||||
}
|
||||
}
|
||||
43
android/app/src/main/kotlin/com/hiddify/hiddify/Application.kt
Executable 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>()!! }
|
||||
}
|
||||
|
||||
}
|
||||
82
android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt
Executable 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)
|
||||
58
android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt
Executable 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)
|
||||
}
|
||||
}
|
||||
37
android/app/src/main/kotlin/com/hiddify/hiddify/LogHandler.kt
Executable 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) {
|
||||
}
|
||||
}
|
||||
156
android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt
Executable 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
227
android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt
Executable 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
189
android/app/src/main/kotlin/com/hiddify/hiddify/PlatformSettingsHandler.kt
Executable 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
126
android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt
Executable 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
|
||||
}
|
||||
}
|
||||
|
||||
67
android/app/src/main/kotlin/com/hiddify/hiddify/ShortcutActivity.kt
Executable 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()
|
||||
}
|
||||
|
||||
}
|
||||
64
android/app/src/main/kotlin/com/hiddify/hiddify/StatsChannel.kt
Executable 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)
|
||||
}
|
||||
}
|
||||
37
android/app/src/main/kotlin/com/hiddify/hiddify/bg/AppChangeReceiver.kt
Executable 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
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
30
android/app/src/main/kotlin/com/hiddify/hiddify/bg/BootReceiver.kt
Executable 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
364
android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt
Executable 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
180
android/app/src/main/kotlin/com/hiddify/hiddify/bg/DefaultNetworkListener.kt
Executable 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
67
android/app/src/main/kotlin/com/hiddify/hiddify/bg/DefaultNetworkMonitor.kt
Executable 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
134
android/app/src/main/kotlin/com/hiddify/hiddify/bg/LocalResolver.kt
Executable 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
156
android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt
Executable 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
17
android/app/src/main/kotlin/com/hiddify/hiddify/bg/ProxyService.kt
Executable 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)
|
||||
}
|
||||
59
android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceBinder.kt
Executable 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()
|
||||
}
|
||||
}
|
||||
109
android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceConnection.kt
Executable 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)
|
||||
}
|
||||
}
|
||||
143
android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceNotification.kt
Executable 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
|
||||
}
|
||||
}
|
||||
}
|
||||
48
android/app/src/main/kotlin/com/hiddify/hiddify/bg/TileService.kt
Executable 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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
199
android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt
Executable 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)
|
||||
|
||||
}
|
||||
7
android/app/src/main/kotlin/com/hiddify/hiddify/constant/Action.kt
Executable 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"
|
||||
}
|
||||
10
android/app/src/main/kotlin/com/hiddify/hiddify/constant/Alert.kt
Executable file
@ -0,0 +1,10 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
enum class Alert {
|
||||
RequestVPNPermission,
|
||||
RequestNotificationPermission,
|
||||
EmptyConfiguration,
|
||||
StartCommandServer,
|
||||
CreateService,
|
||||
StartService
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object PerAppProxyMode {
|
||||
const val OFF = "off"
|
||||
const val INCLUDE = "include"
|
||||
const val EXCLUDE = "exclude"
|
||||
}
|
||||
6
android/app/src/main/kotlin/com/hiddify/hiddify/constant/ServiceMode.kt
Executable file
@ -0,0 +1,6 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object ServiceMode {
|
||||
const val NORMAL = "proxy"
|
||||
const val VPN = "vpn"
|
||||
}
|
||||
24
android/app/src/main/kotlin/com/hiddify/hiddify/constant/SettingsKey.kt
Executable 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"
|
||||
|
||||
}
|
||||
8
android/app/src/main/kotlin/com/hiddify/hiddify/constant/Status.kt
Executable file
@ -0,0 +1,8 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
enum class Status {
|
||||
Stopped,
|
||||
Starting,
|
||||
Started,
|
||||
Stopping,
|
||||
}
|
||||
18
android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Continuations.kt
Executable 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) {
|
||||
}
|
||||
}
|
||||
19
android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Wrappers.kt
Executable 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())
|
||||
139
android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt
Executable 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
31
android/app/src/main/kotlin/com/hiddify/hiddify/utils/OutboundMapper.kt
Executable 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)
|
||||
}
|
||||
BIN
android/app/src/main/res/drawable-hdpi/ic_stat_logo.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-hdpi/splash.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-ldpi/ic_stat_logo.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-ldpi/splash.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-mdpi/ic_stat_logo.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-v21/background.png
Executable file
|
After Width: | Height: | Size: 69 B |
9
android/app/src/main/res/drawable-v21/launch_background.xml
Executable 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>
|
||||
BIN
android/app/src/main/res/drawable-xhdpi/ic_stat_logo.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_stat_logo.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_stat_logo.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
13
android/app/src/main/res/drawable/android12splash.xml
Executable 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>
|
||||
BIN
android/app/src/main/res/drawable/background.png
Executable file
|
After Width: | Height: | Size: 69 B |
53
android/app/src/main/res/drawable/ic_banner_foreground.xml
Executable 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>
|
||||
12
android/app/src/main/res/drawable/ic_launcher_background.xml
Executable 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>
|
||||
13
android/app/src/main/res/drawable/launch_background.xml
Executable 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>
|
||||
BIN
android/app/src/main/res/drawable/splash.png
Executable file
|
After Width: | Height: | Size: 7.0 KiB |
5
android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml
Executable 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>
|
||||
5
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Executable 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>
|
||||
5
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Executable 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>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Executable file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Executable file
|
After Width: | Height: | Size: 1008 B |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Executable file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Executable file
|
After Width: | Height: | Size: 794 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Executable file
|
After Width: | Height: | Size: 636 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Executable file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Executable file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Executable file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Executable file
|
After Width: | Height: | Size: 3.4 KiB |