1 Commits

Author SHA1 Message Date
admin c590135d68 Add CI/CD configuration and API documentation 2026-07-01 21:40:53 +08:00
4406 changed files with 740222 additions and 26163 deletions
Vendored
BIN
View File
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+254
View File
@@ -0,0 +1,254 @@
# CI/CD 配置检查报告
## ✅ 检查完成
已完成对 GitHub Actions CI/CD 配置文件的全面检查。
---
## 📁 已创建的文件
| 文件 | 路径 | 说明 |
|------|------|------|
| build.yml | `.github/workflows/build.yml` | 主工作流配置文件 |
| README.md | `.github/workflows/README.md` | 详细配置说明文档 |
| QUICKSTART.md | `.github/workflows/QUICKSTART.md` | 快速入门指南 |
| setup-secrets.sh | `.github/workflows/setup-secrets.sh` | macOS/Linux 配置脚本 |
| setup-secrets.ps1 | `.github/workflows/setup-secrets.ps1` | Windows 配置脚本 |
---
## 🔧 已修复的问题
### 问题 1: iOS 签名身份 Secret 名称不一致
- **位置**: build.yml 第 218 行
- **问题**: 使用了 `IOS_SIGNING_identity` (小写 identity)
- **修复**: 改为 `IOS_SIGNING_IDENTITY` (全大写,符合命名规范)
### 问题 2: 条件判断过于复杂
- **位置**: 多处
- **问题**: 使用了 `startsWith(github.ref, 'refs/tags/v') || github.event.inputs.release_type == 'release'` 复杂条件
- **修复**: 简化为 `startsWith(github.ref, 'refs/tags/v')`,只在创建标签时触发 Release 构建
---
## ✅ 配置验证清单
### 工作流触发条件
- ✅ Push 到 main 分支 - 触发构建
- ✅ Push 到 develop 分支 - 触发 Debug 构建
- ✅ Pull Request - 触发 Debug 构建
- ✅ 创建版本标签 (v*) - 触发 Release 构建
- ✅ 手动触发 - 可选择构建平台和类型
### Android 构建步骤
- ✅ 检出代码和子模块
- ✅ 安装 JDK 11
- ✅ 安装 Go 1.23
- ✅ 安装 Android SDK
- ✅ 创建 local.properties
- ✅ 配置签名密钥 (Release)
- ✅ 构建 APK
- ✅ 上传构建产物
### iOS 构建步骤
- ✅ 检出代码和子模块
- ✅ 安装 Xcode 15.0
- ✅ 安装开发者证书 (Release)
- ✅ 安装 Provisioning Profile (Release)
- ✅ 解析 Swift Package 依赖
- ✅ 构建 Debug 版本 (模拟器)
- ✅ 构建并归档 Release 版本
- ✅ 导出 IPA
- ✅ 上传构建产物
### 发布步骤
- ✅ 下载 Android APK
- ✅ 下载 iOS IPA
- ✅ 创建 GitHub Release
- ✅ 上传文件到 Release
- ✅ 上传到 TestFlight (Beta 和正式版)
- ✅ 发送 Telegram 通知
- ✅ 清理旧的构建产物
---
## ⚠️ 使用前必须配置的 Secrets
### Android 构建 (发布版本必需)
| Secret 名称 | 说明 | 如何获取 |
|------------|------|---------|
| `ANDROID_KEYSTORE_BASE64` | 签名密钥库的 Base64 编码 | 使用 base64 命令转换 |
| `ANDROID_KEYSTORE_PASSWORD` | 密钥库密码 | 创建密钥库时设置 |
| `ANDROID_KEY_ALIAS` | 密钥别名 | 创建密钥库时设置 |
| `ANDROID_KEY_PASSWORD` | 密钥密码 | 创建密钥库时设置 |
**注意**: Debug 构建不需要配置这些 Secrets。
### iOS 构建 (发布版本必需)
| Secret 名称 | 说明 | 如何获取 |
|------------|------|---------|
| `IOS_CERTIFICATES_P12` | 开发者证书的 Base64 编码 | 从钥匙串导出 .p12 文件 |
| `IOS_CERTIFICATES_PASSWORD` | 证书密码 | 导出时设置 |
| `IOS_TEAM_ID` | Apple Team ID | Apple Developer 账号 |
| `IOS_SIGNING_IDENTITY` | 签名身份 | 如: "Apple Distribution: Name (TEAM_ID)" |
| `IOS_PROVISIONING_PROFILE` | Provisioning Profile 的 Base64 编码 | Apple Developer 网站下载 |
**注意**: Debug 构建不需要配置这些 Secrets。
### TestFlight 上传 (可选)
| Secret 名称 | 说明 | 如何获取 |
|------------|------|---------|
| `APP_STORE_CONNECT_ISSUER_ID` | Issuer ID | App Store Connect API Keys |
| `APP_STORE_CONNECT_API_KEY_ID` | API Key ID | 创建 API Key 时获得 |
| `APP_STORE_CONNECT_API_PRIVATE_KEY` | API 私钥 | 下载的 .p8 文件内容 |
### 通知 (可选)
| Secret 名称 | 说明 | 如何获取 |
|------------|------|---------|
| `TELEGRAM_BOT_TOKEN` | Bot Token | 通过 @BotFather 创建 |
| `TELEGRAM_CHAT_ID` | Chat ID | 邀请 Bot 后获取 |
---
## 🚀 快速开始步骤
### 1. 配置 Secrets
**Windows 用户**:
```powershell
cd D:\Code\UUVPN\.github\workflows
.\setup-secrets.ps1
```
**macOS/Linux 用户**:
```bash
cd /path/to/UUVPN/.github/workflows
chmod +x setup-secrets.sh
./setup-secrets.sh
```
### 2. 推送代码测试
```bash
# 推送到 develop 分支触发 Debug 构建
git checkout -b develop
git push origin develop
# 查看 Actions 页面确认构建成功
```
### 3. 创建发布版本
```bash
# 创建 Alpha 版本
git tag v1.0.0-alpha
git push origin v1.0.0-alpha
# 创建 Beta 版本 (会自动上传到 TestFlight)
git tag v1.0.0-beta
git push origin v1.0.0-beta
# 创建正式版本
git tag v1.0.0
git push origin v1.0.0
```
---
## 📊 预期行为
### Debug 构建 (develop 分支或 PR)
**Android**:
- ✅ 使用 Debug 签名
- ✅ 构建 `app-meta-alpha-debug.apk`
- ✅ 上传到 Artifacts (保留 30 天)
**iOS**:
- ✅ 无签名
- ✅ 构建模拟器版本
- ✅ 上传 .app 文件到 Artifacts (保留 7 天)
### Release 构建 (创建标签)
**Android**:
- ✅ 使用自定义签名密钥
- ✅ 构建 Release APK
- ✅ 重命名为 `UUVPN-Android-v1.0.0.apk`
- ✅ 上传到 GitHub Release
**iOS**:
- ✅ 使用开发者证书签名
- ✅ 导出 IPA 文件
- ✅ 重命名为 `UUVPN-iOS-v1.0.0.ipa`
- ✅ 上传到 GitHub Release
- ✅ Beta 和正式版上传到 TestFlight
---
## ⏱️ 预计构建时间
| 平台 | Debug 构建 | Release 构建 |
|------|-----------|-------------|
| Android | 5-10 分钟 | 10-15 分钟 |
| iOS | 15-20 分钟 | 20-30 分钟 |
**总计**: 完整构建约 25-45 分钟
---
## 💰 成本估算
GitHub Actions 免费账户每月 2000 分钟:
| 构建类型 | 消耗时间 | 每月可构建次数 |
|---------|---------|--------------|
| 仅 Android | ~10 分钟 | ~200 次 |
| 仅 iOS | ~200 分钟 (按 10 倍计) | ~10 次 |
| Android + iOS | ~210 分钟 | ~9 次 |
**建议**: 节省 macOS runner 时间,只在需要时构建 iOS 版本。
---
## 🎯 下一步行动
1.**配置 Secrets**
- 使用配置脚本或手动配置
- 至少配置 Debug 构建所需的 Secrets
2.**测试 Debug 构建**
- 推送代码到 develop 分支
- 确认构建成功
3.**配置发布 Secrets**
- 配置 Android 签名密钥
- 配置 iOS 证书和 Profile
4.**创建第一个版本**
- 创建标签触发 Release 构建
- 验证 GitHub Release 创建成功
5.**配置 TestFlight** (可选)
- 配置 App Store Connect API Keys
- 验证自动上传到 TestFlight
---
## 📞 获取帮助
如遇到问题,请查看:
1. [GitHub Actions 日志](https://github.com/你的用户名/UUVPN/actions)
2. [配置文档](.github/workflows/README.md)
3. [快速入门](.github/workflows/QUICKSTART.md)
4. [项目 Issues](https://github.com/nicolastinkl/UUVPN/issues)
---
**配置检查完成! 🎉**
+363
View File
@@ -0,0 +1,363 @@
# GitHub Actions CI/CD 快速入门指南
本指南帮助你快速配置和使用 UUVPN 的自动构建系统。
---
## 🚀 5 分钟快速开始
### 第一步: 准备工作
确保你已拥有:
- ✅ GitHub 账号
- ✅ UUVPN 代码仓库的写权限
- ✅ Apple Developer 账号 (iOS 构建必需)
- ✅ Android 签名密钥库 (Android Release 构建必需)
### 第二步: 安装 GitHub CLI
**Windows**:
```powershell
winget install GitHub.cli
```
**macOS**:
```bash
brew install gh
```
**Linux**:
```bash
# Debian/Ubuntu
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh
```
### 第三步: 登录 GitHub
```bash
gh auth login
```
按照提示选择:
- GitHub.com
- HTTPS
- 使用浏览器登录或粘贴 token
### 第四步: 配置 Secrets
#### 方法一: 使用自动配置脚本 (推荐)
**Windows**:
```powershell
cd D:\Code\UUVPN\.github\workflows
.\setup-secrets.ps1
```
**macOS/Linux**:
```bash
cd /path/to/UUVPN/.github/workflows
chmod +x setup-secrets.sh
./setup-secrets.sh
```
#### 方法二: 手动配置
在 GitHub 仓库页面:
1. 进入 `Settings``Secrets and variables``Actions`
2. 点击 `New repository secret`
3. 添加所需的 secrets (详见下方)
### 第五步: 触发构建
```bash
# 推送代码触发构建
git push origin main
# 或创建标签触发发布
git tag v1.0.0-alpha
git push origin v1.0.0-alpha
```
---
## 📋 必需的 Secrets 清单
### Android 构建
| Secret | 必需 | 说明 |
|--------|------|------|
| `ANDROID_KEYSTORE_BASE64` | ✅ | 签名密钥库的 Base64 编码 |
| `ANDROID_KEYSTORE_PASSWORD` | ✅ | 密钥库密码 |
| `ANDROID_KEY_ALIAS` | ✅ | 密钥别名 |
| `ANDROID_KEY_PASSWORD` | ✅ | 密钥密码 |
### iOS 构建
| Secret | 必需 | 说明 |
|--------|------|------|
| `IOS_CERTIFICATES_P12` | ✅ | 开发者证书 Base64 编码 |
| `IOS_CERTIFICATES_PASSWORD` | ✅ | 证书密码 |
| `IOS_TEAM_ID` | ✅ | Apple Team ID |
| `IOS_SIGNING_IDENTITY` | ✅ | 签名身份 |
| `IOS_PROVISIONING_PROFILE` | ✅ | Provisioning Profile Base64 |
### TestFlight 上传 (可选)
| Secret | 必需 | 说明 |
|--------|------|------|
| `APP_STORE_CONNECT_ISSUER_ID` | ⭐ | App Store Connect Issuer ID |
| `APP_STORE_CONNECT_API_KEY_ID` | ⭐ | API Key ID |
| `APP_STORE_CONNECT_API_PRIVATE_KEY` | ⭐ | API 私钥 |
### 通知 (可选)
| Secret | 必需 | 说明 |
|--------|------|------|
| `TELEGRAM_BOT_TOKEN` | | Telegram Bot Token |
| `TELEGRAM_CHAT_ID` | | Telegram Chat ID |
---
## 🔧 详细配置步骤
### Android 签名密钥库
#### 1. 创建密钥库 (如果没有)
```bash
keytool -genkey -v \
-keystore release.keystore \
-alias uuvpn \
-keyalg RSA \
-keysize 2048 \
-validity 10000
```
按提示输入:
- 密钥库密码
- 个人信息
- 密钥密码
#### 2. 转换为 Base64
**macOS/Linux**:
```bash
base64 -i release.keystore -o release.keystore.base64
```
**Windows PowerShell**:
```powershell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Out-File release.keystore.base64
```
#### 3. 添加到 GitHub Secrets
复制 `release.keystore.base64` 文件内容,添加为 `ANDROID_KEYSTORE_BASE64` secret。
### iOS 开发者证书
#### 1. 导出证书
1. 打开 "钥匙串访问" (Keychain Access)
2. 找到 "Apple Distribution" 证书
3. 右键 → 导出
4. 选择 .p12 格式
5. 设置密码
#### 2. 转换为 Base64
**macOS**:
```bash
base64 -i certificate.p12 -o certificate.p12.base64
```
#### 3. 下载 Provisioning Profile
1. 登录 [Apple Developer](https://developer.apple.com/)
2. 进入 Certificates, Identifiers & Profiles
3. 下载对应的 Provisioning Profile
#### 4. 转换为 Base64
```bash
base64 -i uuvpn.mobileprovision -o uuvpn.mobileprovision.base64
```
### App Store Connect API Key
#### 1. 创建 API Key
1. 登录 [App Store Connect](https://appstoreconnect.apple.com/)
2. 进入 Users and Access → Keys
3. 点击 "Generate API Key"
4. 选择 "App Manager" 权限
5. 下载 .p8 文件
#### 2. 记录信息
- **Issuer ID**: 在 Keys 页面顶部
- **Key ID**: 创建后显示
- **Private Key**: 下载的 .p8 文件内容
---
## 🎯 构建类型说明
### Debug 构建
**触发条件**:
- Pull Request
- 推送到 develop 分支
**特点**:
- 无需签名配置
- 快速构建
- 不发布到 Release
### Release 构建
**触发条件**:
- 推送到 main 分支
- 创建版本标签
**特点**:
- 需要签名配置
- 自动发布到 GitHub Releases
- 可选上传到 TestFlight
### 版本标签格式
```bash
# Alpha 版本 (预发布)
git tag v1.0.0-alpha
git push origin v1.0.0-alpha
# Beta 版本 (预发布,上传 TestFlight)
git tag v1.0.0-beta
git push origin v1.0.0-beta
# 正式版本 (发布,上传 TestFlight)
git tag v1.0.0
git push origin v1.0.0
```
---
## 📥 下载构建产物
### 方法一: Actions Artifacts
1. 进入 GitHub 仓库
2. 点击 "Actions" 标签
3. 选择具体的 workflow 运行记录
4. 在页面底部 "Artifacts" 区域下载
### 方法二: GitHub Releases
1. 进入 GitHub 仓库
2. 点击 "Releases" 标签
3. 找到对应版本
4. 下载 APK 或 IPA 文件
---
## 🛠️ 故障排查
### 构建失败
1. **查看日志**:
- 进入 Actions 页面
- 点击失败的步骤
- 查看详细错误信息
2. **常见错误**:
- ❌ 签名错误: 检查 Secrets 配置
- ❌ 证书过期: 更新证书和 Profile
- ❌ 构建超时: 优化构建配置
### iOS 签名失败
检查清单:
- [ ] 证书未过期
- [ ] Provisioning Profile 包含正确 App ID
- [ ] Team ID 正确
- [ ] 证书和 Profile 匹配
### TestFlight 上传失败
检查清单:
- [ ] API Key 有 App Manager 权限
- [ ] App ID 在 App Store Connect 已创建
- [ ] 版本号未重复
---
## 💡 最佳实践
### 1. 分支策略
```
main (稳定版本)
develop (开发版本)
feature/* (功能分支)
```
### 2. 版本号规范
使用语义化版本:
- `v1.0.0-alpha`: 内部测试版本
- `v1.0.0-beta`: 公开测试版本
- `v1.0.0`: 正式发布版本
### 3. 节省构建时间
- 只在需要时构建 iOS (macOS runner 费用高)
- 使用缓存加速构建
- 手动触发选择性构建
### 4. 安全建议
- 定期更新签名密钥
- 使用最小权限原则
- 不要在代码中硬编码敏感信息
---
## 📞 获取帮助
### 文档
- [GitHub Actions 文档](https://docs.github.com/en/actions)
- [详细配置说明](./README.md)
- [API 配置指南](../../API配置指南.md)
### 支持
- 查看仓库 [Issues](https://github.com/nicolastinkl/UUVPN/issues)
- Telegram: @fastlink
---
## ✅ 配置验证清单
完成配置后,检查以下内容:
- [ ] 已安装 GitHub CLI
- [ ] 已登录 GitHub CLI
- [ ] 已配置 Android Secrets (如需 Android 构建)
- [ ] 已配置 iOS Secrets (如需 iOS 构建)
- [ ] 已配置 App Store Connect API (如需 TestFlight)
- [ ] 已推送代码触发构建
- [ ] 构建成功
- [ ] 可以下载构建产物
---
**祝你构建顺利! 🎉**
+525
View File
@@ -0,0 +1,525 @@
# GitHub Actions CI/CD 配置说明
本文档说明如何配置和使用 UUVPN 的 GitHub Actions 自动构建流程。
---
## 📋 目录
- [功能特性](#功能特性)
- [前置要求](#前置要求)
- [配置 Secrets](#配置-secrets)
- [使用方法](#使用方法)
- [构建产物](#构建产物)
- [自定义配置](#自定义配置)
- [常见问题](#常见问题)
---
## 功能特性
### ✅ 自动构建
- **Android APK**: 支持 Debug 和 Release 构建
- **iOS IPA**: 支持 Debug 和 Release 构建
- **多架构支持**: Android 支持 armeabi-v7a, arm64-v8a, x86, x86_64
### ✅ 自动发布
- **GitHub Releases**: 自动创建 Release 并上传构建产物
- **TestFlight**: 自动上传 iOS 版本到 TestFlight
- **Telegram 通知**: 构建完成后发送通知
### ✅ 触发条件
- 推送到 main/develop 分支
- 创建版本标签 (v*)
- Pull Request
- 手动触发
---
## 前置要求
### Android 构建
1. **JDK 11**: 已在 Actions 中自动配置
2. **Android SDK**: 已在 Actions 中自动配置
3. **Go 1.23**: 用于编译核心库
4. **签名密钥**: Release 构建需要签名密钥库
### iOS 构建
1. **Apple Developer 账号**: $99/年
2. **Xcode 15.0+**: 已在 Actions 中自动配置
3. **开发者证书**: .p12 格式
4. **Provisioning Profile**: .mobileprovision 文件
5. **App Store Connect API Key**: 用于上传到 TestFlight
---
## 配置 Secrets
在 GitHub 仓库中配置以下 Secrets:
### Android Secrets
| Secret 名称 | 描述 | 获取方式 |
|------------|------|---------|
| `ANDROID_KEYSTORE_BASE64` | 签名密钥库的 Base64 编码 | 见下方说明 |
| `ANDROID_KEYSTORE_PASSWORD` | 密钥库密码 | 创建密钥库时设置 |
| `ANDROID_KEY_ALIAS` | 密钥别名 | 创建密钥库时设置 |
| `ANDROID_KEY_PASSWORD` | 密钥密码 | 创建密钥库时设置 |
**生成 ANDROID_KEYSTORE_BASE64**:
```bash
# 在本地执行
keytool -genkey -v -keystore release.keystore -alias uuvpn -keyalg RSA -keysize 2048 -validity 10000
# 转换为 Base64
base64 -i release.keystore -o release.keystore.base64
# 或者使用 PowerShell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Out-File release.keystore.base64
```
### iOS Secrets
| Secret 名称 | 描述 | 获取方式 |
|------------|------|---------|
| `IOS_CERTIFICATES_P12` | 开发者证书的 Base64 编码 | 见下方说明 |
| `IOS_CERTIFICATES_PASSWORD` | 证书密码 | 导出证书时设置 |
| `IOS_SIGNING_IDENTITY` | 签名身份 | 如: "Apple Distribution: Your Name (TEAM_ID)" |
| `IOS_PROVISIONING_PROFILE` | Provisioning Profile 的 Base64 编码 | 见下方说明 |
| `IOS_TEAM_ID` | Apple Team ID | 在 Apple Developer 账号中查看 |
**生成 IOS_CERTIFICATES_P12**:
1. 在 Keychain Access 中导出证书
2. 选择 "Apple Distribution" 证书
3. 右键 → 导出 → 选择 .p12 格式
4. 设置密码
5. 转换为 Base64:
```bash
base64 -i certificate.p12 -o certificate.p12.base64
# 或者使用 PowerShell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.p12")) | Out-File certificate.p12.base64
```
**生成 IOS_PROVISIONING_PROFILE**:
1. 在 Apple Developer 网站下载 Provisioning Profile
2. 转换为 Base64:
```bash
base64 -i uuvpn.mobileprovision -o uuvpn.mobileprovision.base64
# 或者使用 PowerShell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("uuvpn.mobileprovision")) | Out-File uuvpn.mobileprovision.base64
```
### App Store Connect Secrets
| Secret 名称 | 描述 | 获取方式 |
|------------|------|---------|
| `APP_STORE_CONNECT_ISSUER_ID` | App Store Connect API Issuer ID | 在 App Store Connect 中创建 API Key |
| `APP_STORE_CONNECT_API_KEY_ID` | App Store Connect API Key ID | 创建 API Key 时获得 |
| `APP_STORE_CONNECT_API_PRIVATE_KEY` | App Store Connect API 私钥 | 创建 API Key 时下载的 .p8 文件内容 |
**获取方式**:
1. 登录 [App Store Connect](https://appstoreconnect.apple.com/)
2. 进入 Users and Access → Keys
3. 点击 "Generate API Key"
4. 选择 "App Manager" 权限
5. 下载 .p8 文件并保存私钥内容
### 通知 Secrets (可选)
| Secret 名称 | 描述 | 获取方式 |
|------------|------|---------|
| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token | 通过 @BotFather 创建 Bot |
| `TELEGRAM_CHAT_ID` | Telegram 群组/频道 ID | 邀请 Bot 到群组后获取 |
**获取方式**:
1. 在 Telegram 中找到 @BotFather
2. 发送 `/newbot` 创建新 Bot
3. 获取 Bot Token
4. 创建群组或频道,邀请 Bot
5. 获取 Chat ID: `https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates`
---
## 使用方法
### 方法一: 自动触发
#### 推送到分支
```bash
# 推送到 main 分支,触发构建
git push origin main
# 推送到 develop 分支,触发 Debug 构建
git push origin develop
```
#### 创建版本标签
```bash
# 创建 Alpha 版本
git tag v1.0.0-alpha
git push origin v1.0.0-alpha
# 创建 Beta 版本
git tag v1.0.0-beta
git push origin v1.0.0-beta
# 创建正式版本
git tag v1.0.0
git push origin v1.0.0
```
#### Pull Request
创建 Pull Request 到 main 分支会自动触发 Debug 构建。
### 方法二: 手动触发
1. 进入 GitHub 仓库
2. 点击 "Actions" 标签
3. 选择 "Build UUVPN" workflow
4. 点击 "Run workflow"
5. 选择构建选项:
- `build_android`: 是否构建 Android APK
- `build_ios`: 是否构建 iOS IPA
- `release_type`: 构建类型 (alpha/beta/release)
6. 点击 "Run workflow" 按钮
---
## 构建产物
### Android 构建产物
构建完成后,可以在以下位置找到 APK:
1. **Actions Artifacts**: 每个 workflow 运行都会保存 APK artifact
2. **GitHub Releases**: 创建标签后,APK 会自动上传到 Release
**APK 文件命名**:
- Debug: `app-meta-alpha-debug.apk`
- Release: `UUVPN-Android-v1.0.0.apk`
### iOS 构建产物
构建完成后,可以在以下位置找到 IPA:
1. **Actions Artifacts**: 每个 workflow 运行都会保存 IPA artifact
2. **GitHub Releases**: 创建标签后,IPA 会自动上传到 Release
3. **TestFlight**: Beta 和正式版本会自动上传到 TestFlight
**IPA 文件命名**:
- Release: `UUVPN-iOS-v1.0.0.ipa`
---
## 自定义配置
### 修改 Android 构建变体
编辑 `.github/workflows/build.yml` 中的构建命令:
```yaml
- name: Build Android APK
run: |
cd Android-kotlin-Code
# 修改为你需要的构建变体
./gradlew app:assembleMeta-BetaRelease --stacktrace
```
### 修改 iOS Scheme
编辑 `.github/workflows/build.yml` 中的 scheme:
```yaml
- name: Build iOS App
run: |
cd iOS-SwiftUI-Code
xcodebuild build \
-project uuvpn.xcodeproj \
-scheme SFT \ # 修改为你需要的 scheme
-sdk iphoneos \
...
```
### 添加构建缓存
在 workflow 中添加缓存以加速构建:
```yaml
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Cache Swift packages
uses: actions/cache@v4
with:
path: |
~/Library/Caches/org.swift.swiftpm
.build
key: ${{ runner.os }}-swift-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-swift-
```
### 添加自定义构建步骤
在 workflow 中添加自定义步骤:
```yaml
- name: Custom build step
run: |
# 你的自定义命令
echo "Running custom build step"
```
---
## 工作流程说明
### 完整构建流程
```
┌─────────────────┐
│ 触发构建 │
└────────┬────────┘
├─────────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 构建 Android │ │ 构建 iOS │
│ APK │ │ IPA │
└────────┬────────┘ └────────┬────────┘
│ │
└──────────┬──────────┘
┌─────────────────────┐
│ 创建 GitHub Release │
│ (仅标签触发) │
└──────────┬──────────┘
├─────────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 上传 APK 到 │ │ 上传 IPA 到 │
│ GitHub Release │ │ TestFlight │
└─────────────────┘ └─────────────────┘
┌─────────────────────┐
│ 发送通知 │
└─────────────────────┘
```
### 构建类型说明
| 触发条件 | Android | iOS | 发布 |
|---------|---------|-----|------|
| Push to develop | Debug APK | Debug App | 否 |
| Pull Request | Debug APK | Debug App | 否 |
| Push to main | Release APK | Release IPA | 否 |
| Tag v*-alpha | Release APK | Release IPA | GitHub Release (Pre-release) |
| Tag v*-beta | Release APK | Release IPA | GitHub Release + TestFlight |
| Tag v* | Release APK | Release IPA | GitHub Release + TestFlight |
---
## 常见问题
### Q1: Android 构建失败,提示找不到签名密钥?
**A**: 检查以下内容:
- 确认已配置 `ANDROID_KEYSTORE_BASE64` secret
- 确认 Base64 编码正确
- 确认密钥库密码、别名和密钥密码正确
### Q2: iOS 构建失败,提示签名错误?
**A**: 检查以下内容:
- 确认证书未过期
- 确认 Provisioning Profile 包含正确的 App ID
- 确认 Team ID 正确
- 确认证书和 Provisioning Profile 匹配
### Q3: iOS 构建时间过长?
**A**: iOS 构建通常需要 10-20 分钟,这是正常的。可以:
- 使用缓存加速构建
- 只在需要时构建 iOS 版本
- 使用手动触发选择性构建
### Q4: 如何跳过某个平台的构建?
**A**: 使用手动触发时,取消勾选对应平台的构建选项。
### Q5: 构建产物在哪里下载?
**A**:
1. 进入 Actions 页面
2. 点击具体的 workflow 运行记录
3. 在页面底部 "Artifacts" 区域下载
### Q6: 如何查看构建日志?
**A**:
1. 进入 Actions 页面
2. 点击具体的 workflow 运行记录
3. 点击左侧的 job 名称
4. 展开各个步骤查看详细日志
### Q7: TestFlight 上传失败?
**A**: 检查以下内容:
- 确认 App Store Connect API Key 配置正确
- 确认 API Key 有 App Manager 权限
- 确认 App ID 在 App Store Connect 中已创建
- 确认版本号未重复
### Q8: 如何修改构建触发条件?
**A**: 编辑 `.github/workflows/build.yml``on` 部分:
```yaml
on:
push:
branches:
- main
- develop
- feature/* # 添加更多分支
tags:
- 'v*'
pull_request:
branches:
- main
- develop
```
---
## 进阶配置
### 添加代码质量检查
在构建前添加代码检查:
```yaml
jobs:
lint:
name: Code Quality Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Android Lint
run: |
cd Android-kotlin-Code
./gradlew lint
- name: Swift Lint
run: |
brew install swiftlint
cd iOS-SwiftUI-Code
swiftlint
```
### 添加单元测试
```yaml
jobs:
test:
name: Run Tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Android Tests
run: |
cd Android-kotlin-Code
./gradlew test
- name: iOS Tests
run: |
cd iOS-SwiftUI-Code
xcodebuild test \
-project uuvpn.xcodeproj \
-scheme SFI \
-destination 'platform=iOS Simulator,name=iPhone 15'
```
### 添加多版本构建
```yaml
strategy:
matrix:
xcode-version: ['14.3', '15.0', '15.2']
include:
- xcode-version: '14.3'
ios-deployment-target: '14.0'
- xcode-version: '15.0'
ios-deployment-target: '15.0'
- xcode-version: '15.2'
ios-deployment-target: '16.0'
```
---
## 成本估算
### GitHub Actions 免费额度
- **总时长**: 每月 2000 分钟 (免费账户)
- **macOS runner**: 按 10 倍计算
- **Linux runner**: 按 1 倍计算
### 实际消耗估算
| 构建类型 | Android | iOS | 总计 |
|---------|---------|-----|------|
| Debug 构建 | ~5 分钟 | ~100 分钟 | ~105 分钟 |
| Release 构建 | ~10 分钟 | ~150 分钟 | ~160 分钟 |
| 完整流程 | ~15 分钟 | ~200 分钟 | ~215 分钟 |
### 建议
- 免费账户每月可进行约 **9-10 次完整构建**
- 建议只在必要时构建 iOS 版本
- 使用手动触发选择性构建
- 考虑升级 GitHub Pro 获得更多额度
---
## 总结
通过 GitHub Actions CI/CD 配置,你可以:
✅ 无需本地 Mac 设备即可构建 iOS 应用
✅ 自动化构建和发布流程
✅ 支持 Android 和 iOS 双平台
✅ 自动上传到 TestFlight 和 GitHub Releases
✅ 接收构建通知
配置好 Secrets 后,只需推送代码或创建标签,即可自动完成构建和发布!
+423
View File
@@ -0,0 +1,423 @@
name: Build UUVPN
on:
push:
branches:
- main
- develop
tags:
- 'v*'
pull_request:
branches:
- main
workflow_dispatch:
inputs:
build_android:
description: 'Build Android APK'
required: false
default: true
type: boolean
build_ios:
description: 'Build iOS IPA'
required: false
default: true
type: boolean
release_type:
description: 'Release type'
required: false
default: 'alpha'
type: choice
options:
- alpha
- beta
- release
env:
# Android 配置
ANDROID_COMPILE_SDK: "33"
ANDROID_BUILD_TOOLS: "33.0.0"
ANDROID_TARGET_SDK: "33"
ANDROID_JDK_VERSION: "11"
# iOS 配置
IOS_XCODE_VERSION: "15.0"
IOS_DEPLOYMENT_TARGET: "15.0"
jobs:
# ==================== Android 构建 ====================
build-android:
name: Build Android APK
runs-on: ubuntu-latest
if: ${{ github.event.inputs.build_android != 'false' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: ${{ env.ANDROID_JDK_VERSION }}
distribution: 'temurin'
cache: 'gradle'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache: true
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Grant execute permission for gradlew
run: |
cd Android-kotlin-Code
chmod +x gradlew
- name: Create local.properties
run: |
cd Android-kotlin-Code
echo "sdk.dir=$ANDROID_HOME" > local.properties
- name: Create signing.properties (Debug)
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop' }}
run: |
cd Android-kotlin-Code
echo "keystore.path=debug.keystore" > signing.properties
echo "keystore.password=android" >> signing.properties
echo "key.alias=androiddebugkey" >> signing.properties
echo "key.password=android" >> signing.properties
- name: Create signing.properties (Release)
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
cd Android-kotlin-Code
echo "keystore.path=release.keystore" > signing.properties
echo "keystore.password=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> signing.properties
echo "key.alias=${{ secrets.ANDROID_KEY_ALIAS }}" >> signing.properties
echo "key.password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> signing.properties
- name: Decode Android keystore
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
cd Android-kotlin-Code
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
- name: Build Android Debug APK
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop' }}
run: |
cd Android-kotlin-Code
./gradlew app:assembleMeta-AlphaDebug --stacktrace
- name: Build Android Release APK
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
cd Android-kotlin-Code
./gradlew app:assembleMeta-AlphaRelease --stacktrace
- name: Build Android APK (Default)
if: ${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') && github.ref != 'refs/heads/develop' }}
run: |
cd Android-kotlin-Code
./gradlew app:assembleMeta-AlphaRelease --stacktrace
- name: Upload Android APK Artifact
uses: actions/upload-artifact@v4
with:
name: android-apk-${{ github.sha }}
path: |
Android-kotlin-Code/app/build/outputs/apk/**/*.apk
Android-kotlin-Code/app/build/outputs/apk/**/*.json
retention-days: 30
- name: Get APK filename
id: apk_name
run: |
if [ "${{ startsWith(github.ref, 'refs/tags/v') }}" = "true" ]; then
APK_NAME="UUVPN-Android-${{ github.ref_name }}.apk"
else
APK_NAME="UUVPN-Android-${{ github.sha }}.apk"
fi
echo "apk_name=$APK_NAME" >> $GITHUB_OUTPUT
- name: Rename APK for release
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
cd Android-kotlin-Code/app/build/outputs/apk
find . -name "*.apk" -type f -exec mv {} "${{ steps.apk_name.outputs.apk_name }}" \;
# ==================== iOS 构建 ====================
build-ios:
name: Build iOS IPA
runs-on: macos-latest
if: ${{ github.event.inputs.build_ios != 'false' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ env.IOS_XCODE_VERSION }}
- name: Show Xcode version
run: |
xcodebuild -version
swift --version
- name: Install Apple Certificate
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.IOS_CERTIFICATES_P12 }}
p12-password: ${{ secrets.IOS_CERTIFICATES_PASSWORD }}
- name: Install Provisioning Profile
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "${{ secrets.IOS_PROVISIONING_PROFILE }}" | base64 -d > ~/Library/MobileDevice/Provisioning\ Profiles/uuvpn.mobileprovision
- name: Resolve Swift Package Dependencies
run: |
cd iOS-SwiftUI-Code
xcodebuild -resolvePackageDependencies -project uuvpn.xcodeproj -scheme SFI | xcpretty
- name: Build iOS App (Debug)
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop' }}
run: |
cd iOS-SwiftUI-Code
xcodebuild clean build \
-project uuvpn.xcodeproj \
-scheme SFI \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
-configuration Debug \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
| xcpretty
- name: Build and Archive iOS App (Release)
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
cd iOS-SwiftUI-Code
xcodebuild clean archive \
-project uuvpn.xcodeproj \
-scheme SFI \
-sdk iphoneos \
-configuration Release \
-archivePath build/UUVPN.xcarchive \
CODE_SIGN_IDENTITY="${{ secrets.IOS_SIGNING_IDENTITY }}" \
PROVISIONING_PROFILE_SPECIFIER="uuvpn" \
| xcpretty
- name: Create ExportOptions.plist
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
cd iOS-SwiftUI-Code
cat > ExportOptions.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>${{ secrets.IOS_TEAM_ID }}</string>
<key>uploadSymbols</key>
<true/>
<key>compileBitcode</key>
<false/>
</dict>
</plist>
EOF
- name: Export IPA
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
cd iOS-SwiftUI-Code
xcodebuild -exportArchive \
-archivePath build/UUVPN.xcarchive \
-exportPath build/ipa \
-exportOptionsPlist ExportOptions.plist \
| xcpretty
- name: Upload iOS IPA Artifact
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
uses: actions/upload-artifact@v4
with:
name: ios-ipa-${{ github.sha }}
path: iOS-SwiftUI-Code/build/ipa/*.ipa
retention-days: 30
- name: Upload iOS App Artifact (Debug)
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop' }}
uses: actions/upload-artifact@v4
with:
name: ios-app-debug-${{ github.sha }}
path: iOS-SwiftUI-Code/build/**/*.app
retention-days: 7
- name: Get IPA filename
id: ipa_name
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
IPA_NAME="UUVPN-iOS-${{ github.ref_name }}.ipa"
echo "ipa_name=$IPA_NAME" >> $GITHUB_OUTPUT
- name: Rename IPA for release
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
cd iOS-SwiftUI-Code/build/ipa
find . -name "*.ipa" -type f -exec mv {} "${{ steps.ipa_name.outputs.ipa_name }}" \;
# ==================== 发布到 GitHub Releases ====================
release:
name: Create GitHub Release
needs: [build-android, build-ios]
runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download Android APK
uses: actions/download-artifact@v4
with:
name: android-apk-${{ github.sha }}
path: artifacts/android
- name: Download iOS IPA
uses: actions/download-artifact@v4
with:
name: ios-ipa-${{ github.sha }}
path: artifacts/ios
- name: Get version from tag
id: get_version
run: |
VERSION=${{ github.ref_name }}
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create Release Notes
id: release_notes
run: |
cat > release_notes.md << EOF
# UUVPN ${{ steps.get_version.outputs.version }}
## 🎉 新版本发布
### 📱 Android 版本
- 支持 Android 5.0+ (推荐 Android 7.0+)
- 支持 armeabi-v7a, arm64-v8a, x86, x86_64 架构
- 基于 CLASH (mihomo) 核心
### 🍎 iOS 版本
- 支持 iOS 15.0+
- 基于 SINGBOX 核心
- 支持 iPhone 和 iPad
## 📥 下载说明
### Android
下载 APK 文件并安装,首次安装需要授予必要权限。
### iOS
IPA 文件需要通过 TestFlight 或第三方工具安装。
## 🔄 更新内容
请查看 [Commits](https://github.com/${{ github.repository }}/commits/${{ github.ref_name }}) 了解详细更新内容。
## 📝 问题反馈
如遇到问题,请在 [Issues](https://github.com/${{ github.repository }}/issues) 中反馈。
EOF
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref_name }}
name: UUVPN ${{ steps.get_version.outputs.version }}
body_path: release_notes.md
draft: false
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}
files: |
artifacts/android/*.apk
artifacts/ios/*.ipa
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ==================== 上传到 TestFlight (iOS) ====================
upload-testflight:
name: Upload to TestFlight
needs: [build-ios]
runs-on: macos-latest
if: ${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, 'alpha') }}
steps:
- name: Download iOS IPA
uses: actions/download-artifact@v4
with:
name: ios-ipa-${{ github.sha }}
path: artifacts/ios
- name: Upload to TestFlight
uses: apple-actions/upload-testflight-build@v1
with:
app-path: artifacts/ios/*.ipa
issuer-id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
api-key-id: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
api-private-key: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }}
# ==================== 清理旧构建产物 ====================
cleanup:
name: Cleanup old artifacts
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }}
steps:
- name: Delete old artifacts
uses: c-hive/gha-remove-artifacts@v1
with:
age: '30 days'
skip-recent: 5
# ==================== 通知 ====================
notify:
name: Send Notification
needs: [build-android, build-ios]
runs-on: ubuntu-latest
if: ${{ always() && github.event_name == 'push' }}
steps:
- name: Send Telegram Notification
uses: appleboy/telegram-action@master
if: ${{ secrets.TELEGRAM_BOT_TOKEN != '' }}
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
format: markdown
message: |
*UUVPN Build Notification*
📦 **Repository**: ${{ github.repository }}
🌿 **Branch**: ${{ github.ref_name }}
🔄 **Commit**: ${{ github.sha }}
✅ **Status**:
- Android: ${{ needs.build-android.result }}
- iOS: ${{ needs.build-ios.result }}
👤 **Author**: ${{ github.actor }}
💬 **Message**: ${{ github.event.head_commit.message }}
[View Build](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
-42
View File
@@ -1,42 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Dart
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Note: This workflow uses the latest stable version of the Dart SDK.
# You can specify other versions if desired, see documentation here:
# https://github.com/dart-lang/setup-dart/blob/main/README.md
# - uses: dart-lang/setup-dart@v1
- uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603
- name: Install dependencies
run: dart pub get
# Uncomment this step to verify the use of 'dart format' on each commit.
# - name: Verify formatting
# run: dart format --output=none --set-exit-if-changed .
# Consider passing '--fatal-infos' for slightly stricter analysis.
- name: Analyze project source
run: dart analyze
# Your project will need to have tests in test/ and a dependency on
# package:test for this step to succeed. Note that Flutter projects will
# want to change this to 'flutter test'.
- name: Run tests
run: dart test
+199
View File
@@ -0,0 +1,199 @@
# UUVPN GitHub Secrets 配置脚本 (PowerShell)
# 用于快速配置 GitHub Actions 所需的 Secrets
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "UUVPN GitHub Secrets 配置工具" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# 检查是否安装了 GitHub CLI
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
Write-Host "❌ 错误: 未安装 GitHub CLI" -ForegroundColor Red
Write-Host "请先安装 GitHub CLI: https://cli.github.com/"
exit 1
}
# 检查是否已登录 GitHub
try {
$authStatus = gh auth status 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ 错误: 未登录 GitHub CLI" -ForegroundColor Red
Write-Host "请先运行: gh auth login"
exit 1
}
} catch {
Write-Host "❌ 错误: 未登录 GitHub CLI" -ForegroundColor Red
Write-Host "请先运行: gh auth login"
exit 1
}
# 获取仓库信息
try {
$remoteUrl = git remote get-url origin
if ($remoteUrl -match "github\.com[:/](.+?)\.git") {
$repo = $matches[1]
} else {
Write-Host "❌ 错误: 无法获取仓库信息" -ForegroundColor Red
Write-Host "请确保在仓库目录中运行此脚本"
exit 1
}
} catch {
Write-Host "❌ 错误: 无法获取仓库信息" -ForegroundColor Red
Write-Host "请确保在仓库目录中运行此脚本"
exit 1
}
Write-Host "✅ 当前仓库: $repo" -ForegroundColor Green
Write-Host ""
# 配置 Android Secrets
Write-Host "📱 配置 Android Secrets" -ForegroundColor Yellow
Write-Host "----------------------------------------"
$configAndroid = Read-Host "是否配置 Android 签名密钥? (y/n)"
if ($configAndroid -eq "y") {
$keystorePath = Read-Host "密钥库文件路径 (默认: release.keystore)"
if ([string]::IsNullOrWhiteSpace($keystorePath)) {
$keystorePath = "release.keystore"
}
if (-not (Test-Path $keystorePath)) {
Write-Host "❌ 错误: 密钥库文件不存在: $keystorePath" -ForegroundColor Red
Write-Host "请先创建密钥库:"
Write-Host "keytool -genkey -v -keystore release.keystore -alias uuvpn -keyalg RSA -keysize 2048 -validity 10000"
exit 1
}
$keystorePassword = Read-Host "密钥库密码" -AsSecureString
$keystorePasswordPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($keystorePassword)
)
$keyAlias = Read-Host "密钥别名 (默认: uuvpn)"
if ([string]::IsNullOrWhiteSpace($keyAlias)) {
$keyAlias = "uuvpn"
}
$keyPassword = Read-Host "密钥密码" -AsSecureString
$keyPasswordPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($keyPassword)
)
# Base64 编码密钥库
$keystoreBytes = [IO.File]::ReadAllBytes($keystorePath)
$keystoreBase64 = [Convert]::ToBase64String($keystoreBytes)
Write-Host "正在设置 Android Secrets..." -ForegroundColor Cyan
$keystoreBase64 | gh secret set ANDROID_KEYSTORE_BASE64 --repo $repo
$keystorePasswordPlain | gh secret set ANDROID_KEYSTORE_PASSWORD --repo $repo
$keyAlias | gh secret set ANDROID_KEY_ALIAS --repo $repo
$keyPasswordPlain | gh secret set ANDROID_KEY_PASSWORD --repo $repo
Write-Host "✅ Android Secrets 配置完成" -ForegroundColor Green
}
Write-Host ""
# 配置 iOS Secrets
Write-Host "🍎 配置 iOS Secrets" -ForegroundColor Yellow
Write-Host "----------------------------------------"
$configIOS = Read-Host "是否配置 iOS 签名证书? (y/n)"
if ($configIOS -eq "y") {
$certPath = Read-Host "证书文件路径 (.p12)"
if (-not (Test-Path $certPath)) {
Write-Host "❌ 错误: 证书文件不存在: $certPath" -ForegroundColor Red
exit 1
}
$certPassword = Read-Host "证书密码" -AsSecureString
$certPasswordPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($certPassword)
)
$teamId = Read-Host "Team ID"
$signingIdentity = Read-Host "签名身份 (如: Apple Distribution: Your Name (TEAM_ID))"
$profilePath = Read-Host "Provisioning Profile 文件路径 (.mobileprovision)"
if (-not (Test-Path $profilePath)) {
Write-Host "❌ 错误: Provisioning Profile 文件不存在: $profilePath" -ForegroundColor Red
exit 1
}
# Base64 编码
$certBytes = [IO.File]::ReadAllBytes($certPath)
$certBase64 = [Convert]::ToBase64String($certBytes)
$profileBytes = [IO.File]::ReadAllBytes($profilePath)
$profileBase64 = [Convert]::ToBase64String($profileBytes)
Write-Host "正在设置 iOS Secrets..." -ForegroundColor Cyan
$certBase64 | gh secret set IOS_CERTIFICATES_P12 --repo $repo
$certPasswordPlain | gh secret set IOS_CERTIFICATES_PASSWORD --repo $repo
$teamId | gh secret set IOS_TEAM_ID --repo $repo
$signingIdentity | gh secret set IOS_SIGNING_IDENTITY --repo $repo
$profileBase64 | gh secret set IOS_PROVISIONING_PROFILE --repo $repo
Write-Host "✅ iOS Secrets 配置完成" -ForegroundColor Green
}
Write-Host ""
# 配置 App Store Connect Secrets
Write-Host "🚀 配置 App Store Connect Secrets" -ForegroundColor Yellow
Write-Host "----------------------------------------"
$configASC = Read-Host "是否配置 App Store Connect API? (y/n)"
if ($configASC -eq "y") {
$issuerId = Read-Host "Issuer ID"
$apiKeyId = Read-Host "API Key ID"
$p8Path = Read-Host "API Private Key 文件路径 (.p8)"
if (-not (Test-Path $p8Path)) {
Write-Host "❌ 错误: API Private Key 文件不存在: $p8Path" -ForegroundColor Red
exit 1
}
# 读取私钥内容
$apiKey = Get-Content $p8Path -Raw
Write-Host "正在设置 App Store Connect Secrets..." -ForegroundColor Cyan
$issuerId | gh secret set APP_STORE_CONNECT_ISSUER_ID --repo $repo
$apiKeyId | gh secret set APP_STORE_CONNECT_API_KEY_ID --repo $repo
$apiKey | gh secret set APP_STORE_CONNECT_API_PRIVATE_KEY --repo $repo
Write-Host "✅ App Store Connect Secrets 配置完成" -ForegroundColor Green
}
Write-Host ""
# 配置通知 Secrets
Write-Host "📢 配置通知 Secrets" -ForegroundColor Yellow
Write-Host "----------------------------------------"
$configTelegram = Read-Host "是否配置 Telegram 通知? (y/n)"
if ($configTelegram -eq "y") {
$botToken = Read-Host "Bot Token"
$chatId = Read-Host "Chat ID"
Write-Host "正在设置 Telegram Secrets..." -ForegroundColor Cyan
$botToken | gh secret set TELEGRAM_BOT_TOKEN --repo $repo
$chatId | gh secret set TELEGRAM_CHAT_ID --repo $repo
Write-Host "✅ Telegram Secrets 配置完成" -ForegroundColor Green
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "✅ 配置完成!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "当前配置的 Secrets:"
gh secret list --repo $repo
Write-Host ""
Write-Host "下一步:" -ForegroundColor Yellow
Write-Host "1. 推送代码到仓库触发构建"
Write-Host "2. 查看构建状态: https://github.com/$repo/actions"
Write-Host ""
+171
View File
@@ -0,0 +1,171 @@
#!/bin/bash
# UUVPN GitHub Secrets 配置脚本
# 用于快速配置 GitHub Actions 所需的 Secrets
set -e
echo "========================================"
echo "UUVPN GitHub Secrets 配置工具"
echo "========================================"
echo ""
# 检查是否安装了 GitHub CLI
if ! command -v gh &> /dev/null; then
echo "❌ 错误: 未安装 GitHub CLI"
echo "请先安装 GitHub CLI: https://cli.github.com/"
exit 1
fi
# 检查是否已登录 GitHub
if ! gh auth status &> /dev/null; then
echo "❌ 错误: 未登录 GitHub CLI"
echo "请先运行: gh auth login"
exit 1
fi
# 获取仓库信息
REPO=$(git remote get-url origin | sed -E 's|.*github\.com[:/](.*)\.git|\1|')
if [ -z "$REPO" ]; then
echo "❌ 错误: 无法获取仓库信息"
echo "请确保在仓库目录中运行此脚本"
exit 1
fi
echo "✅ 当前仓库: $REPO"
echo ""
# 配置 Android Secrets
echo "📱 配置 Android Secrets"
echo "----------------------------------------"
read -p "是否配置 Android 签名密钥? (y/n): " config_android
if [ "$config_android" = "y" ]; then
read -p "密钥库文件路径 (默认: release.keystore): " keystore_path
keystore_path=${keystore_path:-"release.keystore"}
if [ ! -f "$keystore_path" ]; then
echo "❌ 错误: 密钥库文件不存在: $keystore_path"
echo "请先创建密钥库:"
echo "keytool -genkey -v -keystore release.keystore -alias uuvpn -keyalg RSA -keysize 2048 -validity 10000"
exit 1
fi
read -sp "密钥库密码: " keystore_password
echo ""
read -p "密钥别名 (默认: uuvpn): " key_alias
key_alias=${key_alias:-"uuvpn"}
read -sp "密钥密码: " key_password
echo ""
# Base64 编码密钥库
keystore_base64=$(base64 -i "$keystore_path")
echo "正在设置 Android Secrets..."
gh secret set ANDROID_KEYSTORE_BASE64 --repo "$REPO" --body "$keystore_base64"
gh secret set ANDROID_KEYSTORE_PASSWORD --repo "$REPO" --body "$keystore_password"
gh secret set ANDROID_KEY_ALIAS --repo "$REPO" --body "$key_alias"
gh secret set ANDROID_KEY_PASSWORD --repo "$REPO" --body "$key_password"
echo "✅ Android Secrets 配置完成"
fi
echo ""
# 配置 iOS Secrets
echo "🍎 配置 iOS Secrets"
echo "----------------------------------------"
read -p "是否配置 iOS 签名证书? (y/n): " config_ios
if [ "$config_ios" = "y" ]; then
read -p "证书文件路径 (.p12): " cert_path
if [ ! -f "$cert_path" ]; then
echo "❌ 错误: 证书文件不存在: $cert_path"
exit 1
fi
read -sp "证书密码: " cert_password
echo ""
read -p "Team ID: " team_id
read -p "签名身份 (如: Apple Distribution: Your Name (TEAM_ID)): " signing_identity
read -p "Provisioning Profile 文件路径 (.mobileprovision): " profile_path
if [ ! -f "$profile_path" ]; then
echo "❌ 错误: Provisioning Profile 文件不存在: $profile_path"
exit 1
fi
# Base64 编码
cert_base64=$(base64 -i "$cert_path")
profile_base64=$(base64 -i "$profile_path")
echo "正在设置 iOS Secrets..."
gh secret set IOS_CERTIFICATES_P12 --repo "$REPO" --body "$cert_base64"
gh secret set IOS_CERTIFICATES_PASSWORD --repo "$REPO" --body "$cert_password"
gh secret set IOS_TEAM_ID --repo "$REPO" --body "$team_id"
gh secret set IOS_SIGNING_IDENTITY --repo "$REPO" --body "$signing_identity"
gh secret set IOS_PROVISIONING_PROFILE --repo "$REPO" --body "$profile_base64"
echo "✅ iOS Secrets 配置完成"
fi
echo ""
# 配置 App Store Connect Secrets
echo "🚀 配置 App Store Connect Secrets"
echo "----------------------------------------"
read -p "是否配置 App Store Connect API? (y/n): " config_asc
if [ "$config_asc" = "y" ]; then
read -p "Issuer ID: " issuer_id
read -p "API Key ID: " api_key_id
read -p "API Private Key 文件路径 (.p8): " p8_path
if [ ! -f "$p8_path" ]; then
echo "❌ 错误: API Private Key 文件不存在: $p8_path"
exit 1
fi
# 读取私钥内容
api_private_key=$(cat "$p8_path")
echo "正在设置 App Store Connect Secrets..."
gh secret set APP_STORE_CONNECT_ISSUER_ID --repo "$REPO" --body "$issuer_id"
gh secret set APP_STORE_CONNECT_API_KEY_ID --repo "$REPO" --body "$api_key_id"
gh secret set APP_STORE_CONNECT_API_PRIVATE_KEY --repo "$REPO" --body "$api_private_key"
echo "✅ App Store Connect Secrets 配置完成"
fi
echo ""
# 配置通知 Secrets
echo "📢 配置通知 Secrets"
echo "----------------------------------------"
read -p "是否配置 Telegram 通知? (y/n): " config_telegram
if [ "$config_telegram" = "y" ]; then
read -p "Bot Token: " bot_token
read -p "Chat ID: " chat_id
echo "正在设置 Telegram Secrets..."
gh secret set TELEGRAM_BOT_TOKEN --repo "$REPO" --body "$bot_token"
gh secret set TELEGRAM_CHAT_ID --repo "$REPO" --body "$chat_id"
echo "✅ Telegram Secrets 配置完成"
fi
echo ""
echo "========================================"
echo "✅ 配置完成!"
echo "========================================"
echo ""
echo "当前配置的 Secrets:"
gh secret list --repo "$REPO"
echo ""
echo "下一步:"
echo "1. 推送代码到仓库触发构建"
echo "2. 查看构建状态: https://github.com/$REPO/actions"
echo ""
+141
View File
@@ -1 +1,142 @@
app-profile.apk
.DS_Store
# Do not remove or rename entries in this file, only add new ones
# See https://github.com/flutter/flutter/issues/128635 for more context.
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# Visual Studio Code related
.classpath
.project
.settings/
.vscode/*
# Flutter repo-specific
/bin/cache/
/bin/internal/bootstrap.bat
/bin/internal/bootstrap.sh
/bin/mingit/
/dev/benchmarks/mega_gallery/
/dev/bots/.recipe_deps
/dev/bots/android_tools/
/dev/devicelab/ABresults*.json
/dev/docs/doc/
/dev/docs/api_docs.zip
/dev/docs/flutter.docs.zip
/dev/docs/lib/
/dev/docs/pubspec.yaml
/dev/integration_tests/**/xcuserdata
/dev/integration_tests/**/Pods
/packages/flutter/coverage/
version
analysis_benchmark.json
# packages file containing multi-root paths
.packages.generated
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
**/generated_plugin_registrant.dart
.packages
.pub-preload-cache/
.pub-cache/
.pub/
build/
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds
# Android related
**/android/**/gradle-wrapper.jar
.gradle/
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# macOS
**/Flutter/ephemeral/
**/Pods/
**/macos/Flutter/GeneratedPluginRegistrant.swift
**/macos/Flutter/ephemeral
**/xcuserdata/
# Windows
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Linux
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
# Coverage
coverage/
# Symbols
app.*.symbols
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
!.vscode/settings.json
/Android-kotlin-Code/core/.cxx
+636
View File
@@ -0,0 +1,636 @@
# UUVPN API 端点配置指南
本文档详细说明如何配置 UUVPN 的 API 端点,包括 Android 和 iOS 平台的配置方法。
---
## 目录
- [API 配置概述](#api-配置概述)
- [配置字段说明](#配置字段说明)
- [Android 配置方法](#android-配置方法)
- [iOS 配置方法](#ios-配置方法)
- [测试与验证](#测试与验证)
- [常见问题](#常见问题)
---
## API 配置概述
UUVPN 需要配置一个远程 API 端点,用于获取服务器配置信息。该配置采用 JSON 格式,包含 V2Board 面板的 API 地址、支付链接、客服支持等信息。
### 配置 URL 格式
```
https://your-domain.com/api/config.php
```
### 配置返回格式
```json
{
"baseURL": "https://api.0008.uk/api/v1/",
"baseDYURL": "https://api.gooapis.com/api/vpnnodes.php",
"mainregisterURL": "https://lelian.app/#/register?code=",
"paymentURL": "https://your-payment-gateway.com/pay",
"telegramurl": "https://t.me/fastlink",
"kefuurl": "https://gooapis.com/fastlink/",
"websiteURL": "https://gooapis.com/fastlink/",
"crisptoken": "5546c6ea-4b1e-41bc-80e4-4b6648cbca76",
"banners": [
"https://image.gooapis.com/api/images/12-11-56.png",
"https://image.gooapis.com/api/images/12-44-57.png",
"https://image.gooapis.com/api/images/12-47-03.png"
],
"message": "OK",
"code": 1
}
```
---
## 配置字段说明
### 1. baseURL
- **类型**: String
- **必填**: 是
- **描述**: V2Board 面板的主要 API 端点
- **示例**: `https://api.0008.uk/api/v1/`
- **用途**: 所有与 V2Board 服务器的 API 请求都将基于此 URL,包括:
- 用户登录/注册
- 获取节点列表
- 获取订阅信息
- 用户信息管理
### 2. baseDYURL
- **类型**: String
- **必填**: 是
- **描述**: VPN 节点测试端点
- **示例**: `https://api.gooapis.com/api/vpnnodes.php`
- **用途**: 用于测试 VPN 节点的连通性和速度,通常用于应用内的节点测速功能
### 3. mainregisterURL
- **类型**: String
- **必填**: 是
- **描述**: 用户注册页面 URL(带邀请码参数)
- **示例**: `https://lelian.app/#/register?code=`
- **用途**: 引导用户注册,支持邀请码系统
- **注意**: URL 末尾需要保留 `code=` 参数,应用会自动填充邀请码
### 4. paymentURL
- **类型**: String
- **必填**: 是
- **描述**: 支付网关 URL
- **示例**: `https://your-payment-gateway.com/pay`
- **用途**:
- **正常模式** (URL 长度 > 3): 显示外部支付选项,启用订阅功能
- **审核模式** (URL 长度 ≤ 3): 隐藏外部支付,遵守 App Store 指南
- **iOS 重要说明**:
- 正常值: `https://payment.example.com/pay` (显示支付功能)
- 审核值: `xxx``xx` (隐藏支付功能,用于 App Store 审核)
### 5. telegramurl
- **类型**: String
- **必填**: 否
- **描述**: Telegram 支持频道/群组链接
- **示例**: `https://t.me/fastlink`
- **用途**: 提供用户 Telegram 客服支持入口
### 6. kefuurl
- **类型**: String
- **必填**: 否
- **描述**: 在线客服页面 URL
- **示例**: `https://gooapis.com/fastlink/`
- **用途**: 提供网页版在线客服入口
### 7. websiteURL
- **类型**: String
- **必填**: 否
- **描述**: 官方网站 URL
- **示例**: `https://gooapis.com/fastlink/`
- **用途**: 应用内关于页面或官网链接
### 8. crisptoken
- **类型**: String
- **必填**: 否
- **描述**: Crisp 聊天服务认证令牌
- **示例**: `5546c6ea-4b1e-41bc-80e4-4b6648cbca76`
- **用途**: 集成 Crisp 实时聊天功能
- **获取方式**: 在 [Crisp 官网](https://crisp.chat/) 注册后获取
### 9. banners
- **类型**: Array of Strings
- **必填**: 否
- **描述**: 首页横幅图片 URL 数组
- **示例**:
```json
[
"https://image.gooapis.com/api/images/12-11-56.png",
"https://image.gooapis.com/api/images/12-44-57.png"
]
```
- **用途**: 应用首页轮播图展示
- **建议尺寸**: 750x300 像素
- **格式支持**: PNG, JPG, WebP
### 10. message
- **类型**: String
- **必填**: 是
- **描述**: API 响应状态消息
- **示例**: `OK`
- **用途**: 标识配置请求是否成功
### 11. code
- **类型**: Integer
- **必填**: 是
- **描述**: API 响应状态码
- **示例**: `1` (成功) 或 `0` (失败)
- **用途**: 应用根据此字段判断配置是否有效
---
## Android 配置方法
### 方法一: 修改配置文件 (推荐)
1. **定位配置文件**
在 Android 项目中找到以下文件:
```
Android-kotlin-Code/app/src/main/java/com/xxx/xxx/config/ApiConfig.kt
```
2. **修改配置 URL**
找到 `configURL` 或类似的配置常量:
```kotlin
const val CONFIG_URL = "https://your-domain.com/api/config.php"
```
3. **重新编译 APK**
```bash
./gradlew app:assembleMeta-AlphaRelease
```
### 方法二: 动态配置
如果支持动态配置,可以在应用启动时请求配置 URL:
```kotlin
// 在 Application 或 MainActivity 中
ConfigManager.init(this, "https://your-domain.com/api/config.php")
```
### Android API 请求示例
```kotlin
// ApiService.kt
interface ApiService {
@GET
suspend fun getConfig(@Url url: String): Response<ConfigResponse>
}
data class ConfigResponse(
val baseURL: String,
val baseDYURL: String,
val mainregisterURL: String,
val paymentURL: String,
val telegramurl: String,
val kefuurl: String,
val websiteURL: String,
val crisptoken: String,
val banners: List<String>,
val message: String,
val code: Int
)
```
---
## iOS 配置方法
### 方法一: 修改 StoreManager.swift (推荐)
1. **定位配置文件**
在 iOS 项目中找到:
```
iOS-SwiftUI-Code/ApplicationLibrary/Service/StoreManager.swift
```
2. **修改配置 URL**
找到 `configURL` 常量并修改:
```swift
public let configURL = "https://your-domain.com/api/config.php"
```
3. **重新编译应用**
在 Xcode 中按 `Cmd + B` 编译项目
### 方法二: 使用 Info.plist
1. **在 Info.plist 中添加配置**
```xml
<key>ConfigURL</key>
<string>https://your-domain.com/api/config.php</string>
```
2. **在代码中读取**
```swift
if let configURL = Bundle.main.infoDictionary?["ConfigURL"] as? String {
// 使用配置 URL
}
```
### iOS 网络请求示例
```swift
// StoreManager.swift
func fetchConfig() async throws -> ConfigResponse {
guard let url = URL(string: configURL) else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(Bundle.main.bundleIdentifier ?? "", forHTTPHeaderField: "bid")
request.addValue(appVersion, forHTTPHeaderField: "appver")
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(ConfigResponse.self, from: data)
}
// 数据模型
struct ConfigResponse: Codable {
let baseURL: String
let baseDYURL: String
let mainregisterURL: String
let paymentURL: String
let telegramurl: String
let kefuurl: String
let websiteURL: String
let crisptoken: String
let banners: [String]
let message: String
let code: Int
}
```
---
## 测试与验证
### 测试配置地址
项目提供了测试配置地址供开发调试使用:
```
https://vungles.com/api/test/config
```
**测试账号**:
- 邮箱: `binance3980@gmail.com`
- 密码: `Tinkl123`
### 验证配置是否正确
1. **浏览器测试**
在浏览器中访问你的配置 URL,确认返回正确的 JSON 格式
2. **Postman 测试**
使用 Postman 发送 GET 请求,验证响应内容
3. **应用内测试**
- 启动应用
- 检查是否能正常加载首页横幅图
- 测试登录/注册功能
- 验证节点列表加载
### 错误排查
如果配置加载失败:
1. **检查 URL 是否正确**
```
https://your-domain.com/api/config.php
```
2. **检查 HTTPS 证书**
- 确保证书有效且未过期
- 使用可信 CA 签发的证书
3. **检查 JSON 格式**
- 使用 JSON 格式化工具验证
- 确保所有必填字段都存在
4. **检查 CORS 配置** (Web 端)
- 服务器需要配置正确的 CORS 头
---
## 部署配置服务器
### 方案一: 静态 JSON 文件 (推荐)
将配置 JSON 存储在 CDN 或对象存储服务:
1. **创建配置文件**
创建 `config.json`:
```json
{
"baseURL": "https://api.yourdomain.com/api/v1/",
"baseDYURL": "https://api.yourdomain.com/api/vpnnodes.php",
"mainregisterURL": "https://yourdomain.com/#/register?code=",
"paymentURL": "https://pay.yourdomain.com/",
"telegramurl": "https://t.me/yourchannel",
"kefuurl": "https://yourdomain.com/support/",
"websiteURL": "https://yourdomain.com/",
"crisptoken": "your-crisp-token",
"banners": [
"https://cdn.yourdomain.com/banner1.png"
],
"message": "OK",
"code": 1
}
```
2. **上传到对象存储**
- 阿里云 OSS
- 腾讯云 COS
- AWS S3
- Cloudflare R2
3. **启用 HTTPS**
确保存储桶启用 HTTPS 访问
### 方案二: 动态 API 接口
使用后端服务动态返回配置:
**PHP 示例**:
```php
<?php
// config.php
header('Content-Type: application/json');
$config = [
'baseURL' => 'https://api.yourdomain.com/api/v1/',
'baseDYURL' => 'https://api.yourdomain.com/api/vpnnodes.php',
'mainregisterURL' => 'https://yourdomain.com/#/register?code=',
'paymentURL' => 'https://pay.yourdomain.com/',
'telegramurl' => 'https://t.me/yourchannel',
'kefuurl' => 'https://yourdomain.com/support/',
'websiteURL' => 'https://yourdomain.com/',
'crisptoken' => 'your-crisp-token',
'banners' => [
'https://cdn.yourdomain.com/banner1.png',
'https://cdn.yourdomain.com/banner2.png'
],
'message' => 'OK',
'code' => 1
];
echo json_encode($config, JSON_PRETTY_PRINT);
?>
```
**Node.js 示例**:
```javascript
// config.js
const express = require('express');
const app = express();
app.get('/api/config', (req, res) => {
res.json({
baseURL: 'https://api.yourdomain.com/api/v1/',
baseDYURL: 'https://api.yourdomain.com/api/vpnnodes.php',
mainregisterURL: 'https://yourdomain.com/#/register?code=',
paymentURL: 'https://pay.yourdomain.com/',
telegramurl: 'https://t.me/yourchannel',
kefuurl: 'https://yourdomain.com/support/',
websiteURL: 'https://yourdomain.com/',
crisptoken: 'your-crisp-token',
banners: [
'https://cdn.yourdomain.com/banner1.png'
],
message: 'OK',
code: 1
});
});
app.listen(3000, () => {
console.log('Config server running on port 3000');
});
```
---
## 安全建议
### 1. 使用 HTTPS
- 所有 API 端点必须使用 HTTPS
- 使用可信 CA 签发的 SSL 证书
- 禁用 HTTP 访问
### 2. 请求验证
在请求头中添加验证信息:
```kotlin
// Android
request.addHeader("bid", BuildConfig.APPLICATION_ID)
request.addHeader("appver", BuildConfig.VERSION_NAME)
request.addHeader("device", android.os.Build.MODEL)
```
```swift
// iOS
request.addValue(Bundle.main.bundleIdentifier ?? "", forHTTPHeaderField: "bid")
request.addValue(appVersion, forHTTPHeaderField: "appver")
request.addValue(UIDevice.current.model, forHTTPHeaderField: "device")
```
### 3. 服务器端验证
在服务器端验证请求来源:
```php
<?php
$bid = $_SERVER['HTTP_BID'] ?? '';
$appver = $_SERVER['HTTP_APPVER'] ?? '';
// 验证 Bundle ID
$allowedBids = ['com.yourcompany.uuvpn', 'com.yourcompany.uuvpn.dev'];
if (!in_array($bid, $allowedBids)) {
http_response_code(403);
exit(json_encode(['code' => 0, 'message' => 'Unauthorized']));
}
// 返回配置
// ...
?>
```
### 4. 敏感信息保护
- 不要在配置文件中存储密钥或密码
- 使用 token 而非明文密码
- 定期更新 Crisp token
---
## iOS App Store 审核配置
### 审核模式配置
iOS 应用提交 App Store 审核时,需要隐藏外部支付链接:
```json
{
"baseURL": "https://api.yourdomain.com/api/v1/",
"baseDYURL": "https://api.yourdomain.com/api/vpnnodes.php",
"mainregisterURL": "https://yourdomain.com/#/register?code=",
"paymentURL": "xx",
"telegramurl": "https://t.me/yourchannel",
"kefuurl": "https://yourdomain.com/support/",
"websiteURL": "https://yourdomain.com/",
"crisptoken": "your-crisp-token",
"banners": [],
"message": "OK",
"code": 1
}
```
**关键点**:
- `paymentURL` 设置为 `"xx"` 或 `"xxx"` (长度 ≤ 3)
- `banners` 设置为空数组,避免显示包含支付信息的横幅
### 审核通过后配置
审核通过后,将配置改回正常模式:
```json
{
"paymentURL": "https://pay.yourdomain.com/",
"banners": [
"https://cdn.yourdomain.com/banner1.png"
]
}
```
---
## 常见问题
### Q1: 修改配置后应用没有更新怎么办?
**A**: 清除应用缓存或重新安装应用
**Android**:
```bash
adb shell pm clear com.yourcompany.uuvpn
```
**iOS**: 卸载重装应用
### Q2: 配置请求超时怎么办?
**A**: 检查以下内容:
- 服务器是否正常运行
- HTTPS 证书是否有效
- 网络连接是否正常
- 考虑使用 CDN 加速
### Q3: 如何在不重新编译的情况下更改配置?
**A**: 配置是从远程 URL 动态获取的,只需修改服务器端的配置 JSON 即可,应用会在下次启动时自动获取最新配置
### Q4: 能否使用多个配置 URL?
**A**: 可以,通过在不同的编译变体中使用不同的配置 URL:
**Android**:
```kotlin
object ApiConfig {
val CONFIG_URL = when (BuildConfig.BUILD_TYPE) {
"debug" -> "https://dev.yourdomain.com/api/config.php"
"release" -> "https://yourdomain.com/api/config.php"
else -> "https://yourdomain.com/api/config.php"
}
}
```
**iOS**:
```swift
#if DEBUG
let configURL = "https://dev.yourdomain.com/api/config.php"
#else
let configURL = "https://yourdomain.com/api/config.php"
#endif
```
### Q5: 配置加载失败应用会崩溃吗?
**A**: 不会,应用应该有默认配置作为后备:
```kotlin
// Android
fun getConfig(): ConfigResponse {
return try {
apiService.getConfig(configURL)
} catch (e: Exception) {
// 返回默认配置
getDefaultConfig()
}
}
```
```swift
// iOS
func loadConfig() async {
do {
config = try await fetchConfig()
} catch {
// 使用默认配置
config = getDefaultConfig()
}
}
```
---
## 总结
配置 API 端点是 UUVPN 应用的核心步骤,正确的配置能确保应用正常工作:
1. ✅ 准备好 V2Board 服务器
2. ✅ 创建配置 JSON 文件
3. ✅ 部署到 HTTPS 服务器
4. ✅ 在应用中配置 URL
5. ✅ 测试验证配置
6. ✅ 针对iOS审核特殊处理
如有问题,请参考 [GitHub Issues](https://github.com/nicolastinkl/UUVPN/issues) 或联系技术支持。
+4
View File
@@ -0,0 +1,4 @@
[submodule "clash-foss"]
path = core/src/foss/golang/clash
url = https://github.com/MetaCubeX/mihomo
branch = Meta
+14
View File
@@ -0,0 +1,14 @@
## Contributing to Clash for Android
#### Code Style
Please use `Android Studio` or `Intellij IDEA` to open the project and use the project code style profile.
`File` -> `Settings` -> `Editor` -> `Code Style` -> `C/C++ and Kotlin` -> `Scheme` -> `Project`
#### License
Contributing to Clash for Android that assumes you allow code to be merged into closed-source branch of Clash for Android. Other terms follow the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html)
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+833
View File
@@ -0,0 +1,833 @@
3th-party software licenses
* Clash
==========================================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
* Android Open Source Project
* Android X Support Library
==========================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+50
View File
@@ -0,0 +1,50 @@
## Privacy Policy
The Clash for Android is built as an Open Source software. This app is provided by personal at no cost and is intended for use as is.
This page is used to inform visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our app.
If you choose to use our app, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the app. We will not use or share your information with anyone except as described in this Privacy Policy.
The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Clash for Android unless otherwise defined in this Privacy Policy.
**Information Collection and Use**
For a better experience, while using our app, we may require you to provide us with certain personally identifiable information. The information that we request will be retained by us and used as described in this privacy policy.
The app does use third party services that may collect information used to identify you.
Link to privacy policy of third party service providers used by the app
* [Google Play Services](https://www.google.com/policies/privacy/)
* [AppCenter](https://docs.microsoft.com/en-us/appcenter/gdpr/)
**Log Data**
We want to inform you that whenever you use our app, in a case of an error in the app we collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing our App, the time and date of your use of the app, and other statistics.
**Cookies**
Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.
This app does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this app.
**Security**
We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security.
**Links to Other Sites**
This app may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by us. Therefore, we strongly advise you to review the Privacy Policy of these websites. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
**Childrens Privacy**
These Services do not address anyone under the age of 13\. We do not knowingly collect personally identifiable information from children under 13\. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions.
**Changes to This Privacy Policy**
We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
**Contact Us**
If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.
+144
View File
@@ -0,0 +1,144 @@
## UUVPN
[https://youtube.com/shorts/zI1hrpFJbtg?feature=share](https://youtube.com/shorts/zI1hrpFJbtg?feature=share)
### Requirement
- Android 5.0+ (minimum)
- Android 7.0+ (recommend)
- `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
### Build
1. Update submodules
```bash
git submodule update --init --recursive
```
2. Install **OpenJDK 11**, **Android SDK**, **CMake** and **Golang**
3. Create `local.properties` in project root with
```properties
sdk.dir=/path/to/android-sdk
```
4. Create `signing.properties` in project root with
```properties
keystore.path=/path/to/keystore/file
keystore.password=<key store password>
key.alias=<key alias>
key.password=<key password>
```
5. Build
```bash
./gradlew app:assembleMeta-AlphaRelease
```
6. 教程:修改 API 后台地址,api请求内容如下 , 可选增加订阅地址转换地址,方便调用。
Please read 服务器配置+Kotlin-Android 文档教程.docx
![](images_05_20_11.png)
```
返回值可以直接写在阿里云oss 中,速度更快
{
"baseURL": "https://api.0008.uk/api/v1/",
"baseDYURL": "https://api.gooapis.com/api/vpnnodes.php",
"mainregisterURL": "https://lelian.app/#/register?code=",
"paymentURL": "xxxxx",
"telegramurl": "https://t.me/fastlink",
"kefuurl": "https://gooapis.com/fastlink/",
"websiteURL": "https://gooapis.com/fastlink/",
"crisptoken": "5546c6ea-4b1e-41bc-80e4-4b6648cbca76",
"banners": [
"https://image.gooapis.com/api/images/12-11-56.png",
"https://image.gooapis.com/api/images/12-44-57.png",
"https://image.gooapis.com/api/images/12-47-03.png"
],
"message": "OK",
"code": 1
}
```
![alt text](telegram-cloud-photo-size-5-6327623898581027724-y.jpg)
```
测试的 URL 配置地址: https://vungles.com/api/test/config
测试账号:
binance3980@gmail.com
Tinkl123
```
上面字段的意思:
以下是您提供的 JSON 配置文件的字段解释:
### 1. **baseURL**
- **描述**:基础 API URL,用于访问主要的 API 端点。
- **示例值**`https://api.0008.uk/api/v1/`
- **用途**:所有与服务器的 API 请求都将基于此 URL 进行。例如,可以在此基础上附加不同的 API 路径来访问特定资源。
### 2. **baseDYURL**
- **描述**:另一个 API URL,与 `baseURL` 不同,用于访问特定的 VPN默认测试节点。
- **示例值**`https://api.gooapis.com/api/vpnnodes.php`
- **用途**:通常用于请求特定的 VPN 节点数据或其他与代理/网络连接相关的信息。
### 3. **mainregisterURL**
- **描述**:用户注册页面推广的 URL,通常包含一个唯一的代码参数,用于推广注册过程。
- **示例值**`https://lelian.app/#/register?code=`
- **用途**:这个 URL 用于引导用户进行注册,`code` 参数可能是用户的注册邀请码或其他标识符。
### 4. **paymentURL**
- **描述**:支付相关的 URL,可能指向支付网关或交易页面。
- **示例值**`xxxxx`
- **用途**:用于支付的跳转链接,具体地址没有提供。
### 5. **telegramurl**
- **描述**:指向 Telegram 频道或群组的 URL。
- **示例值**`https://t.me/fastlink`
- **用途**:用户可以通过此链接访问 Telegram 频道,可能用于接收通知、更新或客服支持。
### 6. **kefuurl**
- **描述**:客服支持页面的 URL。
- **示例值**`https://gooapis.com/fastlink/`
- **用途**:提供给用户访问的在线客服页面,通常是支持问题解答和帮助解决问题的页面。
### 7. **websiteURL**
- **描述**:指向网站首页的 URL。
- **示例值**`https://gooapis.com/fastlink/`
- **用途**:指向官方网站,用户可以通过该链接访问相关产品或服务的详细信息。
### 8. **crisptoken**
- **描述**:一个唯一的令牌,通常用于身份验证或授权。
- **示例值**`5546c6ea-4b1e-41bc-80e4-4b6648cbca76`
- **用途**:令牌用于验证请求的合法性,防止未经授权的访问。可能用于 API 请求中传递身份信息。
### 9. **banners**
- **描述**:一个包含多个图片 URL 的数组,通常用于在网页或应用中展示横幅广告或图片。
- **示例值**
```json
[
"https://image.gooapis.com/api/images/12-11-56.png",
"https://image.gooapis.com/api/images/12-44-57.png",
"https://image.gooapis.com/api/images/12-47-03.png"
]
```
- **用途**:这些链接指向可以展示在前端界面上的横幅图片,可以是广告、通知、促销等。
### 10. **message**
- **描述**:一个消息字段,通常用于描述当前操作或请求的状态。
- **示例值**`OK`
- **用途**:这是 API 响应中的状态信息,`OK` 表示操作成功。
### 11. **code**
- **描述**:一个数字代码,表示响应的状态。
- **示例值**`1`
- **用途**:通常用于表示请求是否成功,`1` 可能表示操作成功,其他数字可能表示错误或特定的状态。
Binary file not shown.
+112
View File
@@ -0,0 +1,112 @@
import java.net.URL
import java.nio.file.Files
import java.nio.file.StandardCopyOption
plugins {
kotlin("android")
kotlin("kapt")
id("com.android.application")
// kotlin("jvm") version "1.8.10" // 使用兼容的 Kotlin 版本
}
dependencies {
compileOnly(project(":hideapi"))
implementation(project(":core"))
implementation(project(":service"))
implementation(project(":design"))
implementation(project(":common"))
implementation(libs.kotlin.coroutine)
implementation(libs.androidx.core)
implementation(libs.androidx.activity)
implementation(libs.androidx.fragment)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.coordinator)
implementation(libs.androidx.recyclerview)
implementation(libs.google.material)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")
// https://mvnrepository.com/artifact/androidx.appcompat/appcompat
// implementation(files("libs/appcompat-1.7.0.aar"))
// implementation("im.crisp:crisp-sdk:2.0.5")
// implementation("im.crisp:crisp-sdk:2.0.5")
// If you're using AndroidX
//implementation("androidx.multidex:multidex:2.0.1")
// If you're not using AndroidX
//implementation ("com.android.support:multidex:1.0.3")
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
tasks.getByName("clean", type = Delete::class) {
delete(file("release"))
}
val geoFilesDownloadDir = "src/main/assets"
task("downloadGeoFiles") {
val geoFilesUrls = mapOf(
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb" to "geoip.metadb",
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat" to "geosite.dat",
// "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb" to "country.mmdb",
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb" to "ASN.mmdb",
)
doLast {
geoFilesUrls.forEach { (downloadUrl, outputFileName) ->
val url = URL(downloadUrl)
val outputPath = file("$geoFilesDownloadDir/$outputFileName")
// Check if the file already exists
if (!outputPath.exists()) {
println("Downloading $outputFileName from $downloadUrl")
outputPath.parentFile.mkdirs()
url.openStream().use { input ->
Files.copy(input, outputPath.toPath(), StandardCopyOption.REPLACE_EXISTING)
println("$outputFileName downloaded to $outputPath")
}
}else{
println("$outputFileName already exists at $outputPath, skipping download")
}
}
}
}
afterEvaluate {
val downloadGeoFilesTask = tasks["downloadGeoFiles"]
tasks.forEach {
if (it.name.startsWith("assemble")) {
it.dependsOn(downloadGeoFilesTask)
}
}
}
tasks.getByName("clean", type = Delete::class) {
delete(file(geoFilesDownloadDir))
}
android {
buildTypes {
getByName("release") {
signingConfig = signingConfigs.getByName("debug")
}
getByName("debug") {
signingConfig = signingConfigs.getByName("debug")
}
}
}
Binary file not shown.
+59
View File
@@ -0,0 +1,59 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontobfuscate
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void checkNotNull(...);
public static void checkExpressionValueIsNotNull(...);
public static void checkNotNullExpressionValue(...);
public static void checkReturnedValueIsNotNull(...);
public static void checkFieldIsNotNull(...);
public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...);
}
# Kotlin Coroutine
# Allow R8 to optimize away the FastServiceLoader.
# Together with ServiceLoader optimization in R8
# this results in direct instantiation when loading Dispatchers.Main
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {
boolean FAST_SERVICE_LOADER_ENABLED return false;
}
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoaderKt {
boolean ANDROID_DETECTED return true;
}
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
# Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt {
boolean SUPPORT_MISSING return false;
}
# Statically turn off all debugging facilities and assertions
-assumenosideeffects class kotlinx.coroutines.DebugKt {
boolean getASSERTIONS_ENABLED() return false;
boolean getDEBUG() return false;
boolean getRECOVER_STACK_TRACES() return false;
}
@@ -0,0 +1,302 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.github.kr328.clash">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:usesCleartextTraffic="true"
android:banner="@mipmap/ic_launcher"
android:fullBackupContent="@xml/full_backup_content"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/BootstrapTheme"
tools:ignore="GoogleAppIndexingWarning"
tools:targetApi="N">
<meta-data
android:name="releaseName"
android:value="@string/release_name" />
<meta-data
android:name="releaseCode"
android:value="@integer/release_code" />
<activity
android:name=".MainActivity"
android:configChanges="uiMode"
android:exported="true"
android:label="@string/launch_name"
android:launchMode="singleTop" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
<activity
android:name=".ExternalControlActivity"
android:exported="true"
android:label="@string/external_control_activity"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
<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="clash-----------"/>
<data android:scheme="clashmeta-----------"/>
<data android:host="install-config-----------"/>
</intent-filter>
<intent-filter>
<action android:name="com.github.metacubex.clash.meta.action.START_CLASH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.github.metacubex.clash.meta.action.STOP_CLASH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.github.metacubex.clash.meta.action.TOGGLE_CLASH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ApkBrokenActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/application_broken" />
<activity
android:name=".AppCrashedActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/application_crashed"
android:launchMode="singleTask" />
<activity
android:name=".ProfilesActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/profiles" />
<activity
android:name=".NewProfileActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/create_profile" />
<activity
android:name=".PropertiesActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/profile" />
<activity
android:name=".ProxyActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/proxy" />
<activity
android:name=".ProvidersActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/providers" />
<activity
android:name=".LogsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/logs" />
<activity
android:name=".LogcatActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/logcat" />
<activity
android:name=".SettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/settings" />
<activity
android:name=".NetworkSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/network" />
<activity
android:name=".AppSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/app" />
<activity
android:name=".OverrideSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/override" />
<activity
android:name=".MetaFeatureSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/meta_features" />
<activity
android:name=".AccessControlActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/access_control_packages" />
<activity
android:name=".HelpActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/help" />
<activity
android:name=".FilesActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/files" />
<activity
android:name=".ProfileActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="设置中心"
/>
<activity
android:name=".LoginActivity"
android:configChanges="uiMode"
android:exported="false"
/>
<activity
android:name=".SplashActivity"
android:configChanges="uiMode"
android:exported="true" />
<activity
android:name=".PlansActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="购买订阅"
/>
<activity
android:name=".MyOrdersActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="我的订单"
/>
<activity
android:name=".ConfigOrderActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="配置订阅"
/>
<activity
android:name=".SubmitOrderActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="订单详情"
/>
<activity
android:name=".GongdanActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="我的工单"
/>
<activity
android:name=".NewGongdanActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="新建工单"
/>
<activity
android:name=".H5WebActivity"
android:configChanges="uiMode"
android:exported="false"
/>
<activity
android:name=".TicketDetailActivity"
android:configChanges="uiMode"
android:exported="false"
android:label=""
/>
<activity
android:name=".RegisterActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="注册"
/>
<activity
android:name=".InvitationActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="我的邀请"
/>
<service
android:name=".LogcatService"
android:exported="false"
android:label="@string/clash_logcat" />
<service
android:name=".TileService"
android:exported="true"
android:icon="@drawable/ic_logo_service"
android:foregroundServiceType="connectedDevice"
android:label="@string/launch_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<receiver
android:name=".RestartReceiver"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application>
</manifest>
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":120,"w":150,"h":150,"nm":"Disconnected State","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-10.788],[3.663,-3.663],[5.179,0],[3.663,3.663],[0,5.179],[0,0]],"o":[[0,0],[0,5.179],[-3.663,3.663],[-5.18,0],[-3.663,-3.663],[0,-10.788],[0,0]],"v":[[13.281,-17.188],[19.531,-2.344],[13.811,11.467],[0.001,17.188],[-13.811,11.467],[-19.531,-2.344],[-13.281,-17.188]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.001,-24.874],[0.001,-4.563]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6,0.6,0.6,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.5,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[25,27.344],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Solid Background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":0,"k":[130,130,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.866666666667,0.866666666667,0.866666666667,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Ripple","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[130,130,100]},{"t":60,"s":[197.368,197.368,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.866666666667,0.866666666667,0.866666666667,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":120,"w":150,"h":150,"nm":"Connected State","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-10.788],[3.663,-3.663],[5.179,0],[3.663,3.663],[0,5.179],[0,0]],"o":[[0,0],[0,5.179],[-3.663,3.663],[-5.18,0],[-3.663,-3.663],[0,-10.788],[0,0]],"v":[[13.281,-17.188],[19.531,-2.344],[13.811,11.467],[0.001,17.188],[-13.811,11.467],[-19.531,-2.344],[-13.281,-17.188]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.001,-24.874],[0.001,-4.563]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.5,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[25,27.344],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Solid Background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":0,"k":[130,130,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.360784313725,0.725490196078,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Ripple","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[130,130,100]},{"t":60,"s":[197.368,197.368,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.360151014141,0.725490196078,0.199154169419,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":120,"w":150,"h":150,"nm":"Connecting State","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-10.788],[3.663,-3.663],[5.179,0],[3.663,3.663],[0,5.179],[0,0]],"o":[[0,0],[0,5.179],[-3.663,3.663],[-5.18,0],[-3.663,-3.663],[0,-10.788],[0,0]],"v":[[13.281,-17.188],[19.531,-2.344],[13.811,11.467],[0.001,17.188],[-13.811,11.467],[-19.531,-2.344],[-13.281,-17.188]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.001,-24.874],[0.001,-4.563]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.5,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[25,27.344],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Progress","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":0,"k":[110,110,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[99.99]},{"t":80,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":119,"s":[0.01]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Solid Background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":0,"k":[130,130,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.494117676978,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Ripple","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[130,130,100]},{"t":60,"s":[197.368,197.368,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.494117647059,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"5.9.3","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":1920,"h":1080,"nm":"1_Loading_Line","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Blue Line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,539,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-1,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[800,800],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.212477396049,0.540471813725,0.898039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":80,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.9],"y":[0]},"t":15,"s":[0]},{"t":60.0000024438501,"s":[99.9]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.9],"y":[0]},"t":0,"s":[0.01]},{"t":45.0000018328876,"s":[100]}],"ix":2},"o":{"a":0,"k":-1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"ct":1,"bm":0}],"markers":[{"tm":60.0000024438501,"cm":"END (No Loop)","dr":0}]}
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,137 @@
package com.github.kr328.clash
import android.Manifest.permission.INTERNET
import android.content.ClipData
import android.content.ClipboardManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.core.content.getSystemService
import com.github.kr328.clash.design.AccessControlDesign
import com.github.kr328.clash.design.model.AppInfo
import com.github.kr328.clash.design.util.toAppInfo
import com.github.kr328.clash.service.store.ServiceStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
class AccessControlActivity : BaseActivity<AccessControlDesign>() {
override suspend fun main() {
val service = ServiceStore(this)
val selected = withContext(Dispatchers.IO) {
service.accessControlPackages.toMutableSet()
}
defer {
withContext(Dispatchers.IO) {
service.accessControlPackages = selected
}
}
val design = AccessControlDesign(this, uiStore, selected)
setContentDesign(design)
design.requests.send(AccessControlDesign.Request.ReloadApps)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
AccessControlDesign.Request.ReloadApps -> {
design.patchApps(loadApps(selected))
}
AccessControlDesign.Request.SelectAll -> {
val all = withContext(Dispatchers.Default) {
design.apps.map(AppInfo::packageName)
}
selected.clear()
selected.addAll(all)
design.rebindAll()
}
AccessControlDesign.Request.SelectNone -> {
selected.clear()
design.rebindAll()
}
AccessControlDesign.Request.SelectInvert -> {
val all = withContext(Dispatchers.Default) {
design.apps.map(AppInfo::packageName).toSet() - selected
}
selected.clear()
selected.addAll(all)
design.rebindAll()
}
AccessControlDesign.Request.Import -> {
val clipboard = getSystemService<ClipboardManager>()
val data = clipboard?.primaryClip
if (data != null && data.itemCount > 0) {
val packages = data.getItemAt(0).text.split("\n").toSet()
val all = design.apps.map(AppInfo::packageName).intersect(packages)
selected.clear()
selected.addAll(all)
}
design.rebindAll()
}
AccessControlDesign.Request.Export -> {
val clipboard = getSystemService<ClipboardManager>()
val data = ClipData.newPlainText(
"packages",
selected.joinToString("\n")
)
clipboard?.setPrimaryClip(data)
}
}
}
}
}
}
private suspend fun loadApps(selected: Set<String>): List<AppInfo> =
withContext(Dispatchers.IO) {
val reverse = uiStore.accessControlReverse
val sort = uiStore.accessControlSort
val systemApp = uiStore.accessControlSystemApp
val base = compareByDescending<AppInfo> { it.packageName in selected }
val comparator = if (reverse) base.thenDescending(sort) else base.then(sort)
val pm = packageManager
val packages = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
packages.asSequence()
.filter {
it.packageName != packageName
}
.filter {
it.packageName == "android" || it.requestedPermissions?.contains(INTERNET) == true
}
.filter {
systemApp || !it.isSystemApp
}
.map {
it.toAppInfo(pm)
}
.sortedWith(comparator)
.toList()
}
private val PackageInfo.isSystemApp: Boolean
get() {
return applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0
}
}
@@ -0,0 +1,20 @@
package com.github.kr328.clash
import android.content.Intent
import android.net.Uri
import com.github.kr328.clash.design.ApkBrokenDesign
import kotlinx.coroutines.isActive
class ApkBrokenActivity : BaseActivity<ApkBrokenDesign>() {
override suspend fun main() {
val design = ApkBrokenDesign(this)
setContentDesign(design)
while (isActive) {
val req = design.requests.receive()
startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(req.url)))
}
}
}
@@ -0,0 +1,33 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.compat.versionCodeCompat
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.design.AppCrashedDesign
import com.github.kr328.clash.log.SystemLogcat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
class AppCrashedActivity : BaseActivity<AppCrashedDesign>() {
override suspend fun main() {
val design = AppCrashedDesign(this)
setContentDesign(design)
val packageInfo = withContext(Dispatchers.IO) {
packageManager.getPackageInfo(packageName, 0)
}
Log.i("App version: versionName = ${packageInfo.versionName} versionCode = ${packageInfo.versionCodeCompat}")
val logs = withContext(Dispatchers.IO) {
SystemLogcat.dumpCrash()
}
design.setAppLogs(logs)
while (isActive) {
events.receive()
}
}
}
@@ -0,0 +1,62 @@
package com.github.kr328.clash
import android.content.pm.PackageManager
import com.github.kr328.clash.common.util.componentName
import com.github.kr328.clash.design.AppSettingsDesign
import com.github.kr328.clash.design.model.Behavior
import com.github.kr328.clash.service.store.ServiceStore
import com.github.kr328.clash.util.ApplicationObserver
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class AppSettingsActivity : BaseActivity<AppSettingsDesign>(), Behavior {
override suspend fun main() {
val design = AppSettingsDesign(
this,
uiStore,
ServiceStore(this),
this,
clashRunning,
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ClashStart, Event.ClashStop, Event.ServiceRecreated ->
recreate()
else -> Unit
}
}
design.requests.onReceive {
ApplicationObserver.createdActivities.forEach {
it.recreate()
}
}
}
}
}
override var autoRestart: Boolean
get() {
val status = packageManager.getComponentEnabledSetting(
RestartReceiver::class.componentName
)
return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
}
set(value) {
val status = if (value)
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
packageManager.setComponentEnabledSetting(
RestartReceiver::class.componentName,
status,
PackageManager.DONT_KILL_APP,
)
}
}
@@ -0,0 +1,229 @@
package com.github.kr328.clash
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContract
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
import com.github.kr328.clash.core.bridge.ClashException
import com.github.kr328.clash.design.Design
import com.github.kr328.clash.design.model.DarkMode
import com.github.kr328.clash.design.store.UiStore
import com.github.kr328.clash.design.ui.DayNight
import com.github.kr328.clash.design.util.resolveThemedBoolean
import com.github.kr328.clash.design.util.resolveThemedColor
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.util.ActivityResultLifecycle
import com.github.kr328.clash.util.ApplicationObserver
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
abstract class BaseActivity<D : Design<*>> : AppCompatActivity(),
CoroutineScope by MainScope(),
Broadcasts.Observer {
protected val uiStore by lazy { UiStore(this) }
protected val events = Channel<Event>(Channel.UNLIMITED)
protected var activityStarted: Boolean = false
protected val clashRunning: Boolean
get() = Remote.broadcasts.clashRunning
protected var design: D? = null
set(value) {
field = value
if (value != null) {
setContentView(value.root)
} else {
setContentView(View(this))
}
}
private var defer: suspend () -> Unit = {}
private var deferRunning = false
private val nextRequestKey = AtomicInteger(0)
private var dayNight: DayNight = DayNight.Day
protected abstract suspend fun main()
fun defer(operation: suspend () -> Unit) {
this.defer = operation
}
suspend fun <I, O> startActivityForResult(
contracts: ActivityResultContract<I, O>,
input: I,
): O = withContext(Dispatchers.Main) {
val requestKey = nextRequestKey.getAndIncrement().toString()
ActivityResultLifecycle().use { lifecycle, start ->
suspendCoroutine { c ->
activityResultRegistry.register(requestKey, lifecycle, contracts) {
c.resume(it)
}.apply { start() }.launch(input)
}
}
}
suspend fun setContentDesign(design: D) {
suspendCoroutine<Unit> {
window.decorView.post {
this.design = design
it.resume(Unit)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applyDayNight()
launch {
main()
}
}
override fun onStart() {
super.onStart()
activityStarted = true
Remote.broadcasts.addObserver(this)
events.trySend(Event.ActivityStart)
}
override fun onStop() {
super.onStop()
activityStarted = false
Remote.broadcasts.removeObserver(this)
events.trySend(Event.ActivityStop)
}
override fun onDestroy() {
design?.cancel()
cancel()
super.onDestroy()
}
override fun finish() {
if (deferRunning) return
deferRunning = true
launch {
try {
defer()
} finally {
withContext(NonCancellable) {
super.finish()
}
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (queryDayNight(newConfig) != dayNight) {
ApplicationObserver.createdActivities.forEach {
it.recreate()
}
}
}
open fun shouldDisplayHomeAsUpEnabled(): Boolean {
return true
}
override fun onSupportNavigateUp(): Boolean {
this.onBackPressed()
return true
}
override fun onProfileChanged() {
events.trySend(Event.ProfileChanged)
}
override fun onProfileUpdateCompleted(uuid: UUID?) {
events.trySend(Event.ProfileUpdateCompleted)
}
override fun onProfileUpdateFailed(uuid: UUID?, reason: String?) {
events.trySend(Event.ProfileUpdateFailed)
}
override fun onProfileLoaded() {
events.trySend(Event.ProfileLoaded)
}
override fun onServiceRecreated() {
events.trySend(Event.ServiceRecreated)
}
override fun onStarted() {
events.trySend(Event.ClashStart)
}
override fun onStopped(cause: String?) {
events.trySend(Event.ClashStop)
if (cause != null && activityStarted) {
launch {
design?.showExceptionToast(ClashException(cause))
}
}
}
private fun queryDayNight(config: Configuration = resources.configuration): DayNight {
return when (uiStore.darkMode) {
DarkMode.Auto -> if (config.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) DayNight.Night else DayNight.Day
DarkMode.ForceLight -> DayNight.Day
DarkMode.ForceDark -> DayNight.Night
}
}
private fun applyDayNight(config: Configuration = resources.configuration) {
// val dayNight = theme.applyStyle(R.style.AppThemeLight, true) //默认白天模式
/*
val dayNight = queryDayNight(config)
when (dayNight) {
DayNight.Night -> theme.applyStyle(R.style.AppThemeDark, true)
DayNight.Day -> theme.applyStyle(R.style.AppThemeLight, true)
}*/
window.isAllowForceDarkCompat = false
window.isSystemBarsTranslucentCompat = true
window.statusBarColor = resolveThemedColor(android.R.attr.statusBarColor)
window.navigationBarColor = resolveThemedColor(android.R.attr.navigationBarColor)
if (Build.VERSION.SDK_INT >= 23) {
window.isLightStatusBarsCompat = resolveThemedBoolean(android.R.attr.windowLightStatusBar)
}
if (Build.VERSION.SDK_INT >= 27) {
window.isLightNavigationBarCompat = resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
}
this.dayNight = DayNight.Night //dayNight
}
enum class Event {
ServiceRecreated,
ActivityStart,
ActivityStop,
ClashStop,
ClashStart,
ProfileLoaded,
ProfileChanged,
ProfileUpdateCompleted,
ProfileUpdateFailed,
}
}
@@ -0,0 +1,47 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.ConfigOrderDesign
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.ProfileDesign
import com.github.kr328.clash.network.PlanData
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import java.util.concurrent.TimeUnit
//ConfigOrderDesign
class ConfigOrderActivity : BaseActivity<ConfigOrderDesign>() {
override suspend fun main() {
// 获取 Intent 并接收传递的 PlanData 对象
// 使用 Parcelable 获取对象
val planData = intent.getSerializableExtra("planData") as? PlanData
val design = ConfigOrderDesign(this)
setContentDesign(design)
design.fillData(planData)
val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
println("events : " + it.name)
}
design.requests.onReceive {
println("requests : " + it.name)
when (it) {
ConfigOrderDesign.Request.SubmitOrder -> {
startActivity(SubmitOrderActivity::class.intent.putExtra("trade_no",design.trade_no))
}
}
}
}
}
}
}
@@ -0,0 +1,92 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.design.MainDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.remote.StatusClient
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.startClashService
import com.github.kr328.clash.util.stopClashService
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.util.*
class ExternalControlActivity : Activity(), CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when(intent.action) {
Intent.ACTION_VIEW -> {
val uri = intent.data ?: return finish()
val url = uri.getQueryParameter("url") ?: return finish()
launch {
val uuid = withProfile {
val type = when (uri.getQueryParameter("type")?.lowercase(Locale.getDefault())) {
"url" -> Profile.Type.Url
"file" -> Profile.Type.File
else -> Profile.Type.Url
}
val name = uri.getQueryParameter("name") ?: getString(R.string.new_profile)
create(type, name).also {
patch(it, name, url, 0)
}
}
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
finish()
}
}
Intents.ACTION_TOGGLE_CLASH -> if(Remote.broadcasts.clashRunning) {
stopClash()
}
else {
startClash()
}
Intents.ACTION_START_CLASH -> if(!Remote.broadcasts.clashRunning) {
startClash()
}
else {
Toast.makeText(this, R.string.external_control_started, Toast.LENGTH_LONG).show()
}
Intents.ACTION_STOP_CLASH -> if(Remote.broadcasts.clashRunning) {
stopClash()
}
else {
Toast.makeText(this, R.string.external_control_stopped, Toast.LENGTH_LONG).show()
}
}
return finish()
}
private fun startClash() {
// if (currentProfile == null) {
// Toast.makeText(this, R.string.no_profile_selected, Toast.LENGTH_LONG).show()
// return
// }
val vpnRequest = startClashService()
if (vpnRequest != null) {
Toast.makeText(this, R.string.unable_to_start_vpn, Toast.LENGTH_LONG).show()
return
}
Toast.makeText(this, R.string.external_control_started, Toast.LENGTH_LONG).show()
}
private fun stopClash() {
stopClashService()
Toast.makeText(this, R.string.external_control_stopped, Toast.LENGTH_LONG).show()
}
}
@@ -0,0 +1,160 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.github.kr328.clash
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.github.kr328.clash.common.util.grantPermissions
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.common.util.uuid
import com.github.kr328.clash.design.FilesDesign
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.remote.FilesClient
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.fileName
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import java.util.*
import java.util.concurrent.TimeUnit
class FilesActivity : BaseActivity<FilesDesign>() {
override suspend fun main() {
val uuid = intent.uuid ?: return finish()
val profile = withProfile { queryByUUID(uuid) } ?: return finish()
val root = uuid.toString()
val design = FilesDesign(this)
val client = FilesClient(this)
val stack = Stack<String>()
design.configurationEditable = profile.type != Profile.Type.Url
design.fetch(client, stack, root)
setContentDesign(design)
val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart, Event.ActivityStop -> {
design.fetch(client, stack, root)
}
else -> Unit
}
}
design.requests.onReceive {
try {
when (it) {
FilesDesign.Request.PopStack -> {
if (stack.empty()) {
finish()
} else {
stack.pop()
}
}
is FilesDesign.Request.OpenDirectory -> {
stack.push(it.file.id)
}
is FilesDesign.Request.OpenFile -> {
startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
Intent(Intent.ACTION_VIEW).setDataAndType(
client.buildDocumentUri(it.file.id),
"text/plain"
).grantPermissions()
)
}
is FilesDesign.Request.DeleteFile -> {
client.deleteDocument(it.file.id)
}
is FilesDesign.Request.RenameFile -> {
val newName = design.requestFileName(it.file.name)
client.renameDocument(it.file.id, newName)
}
is FilesDesign.Request.ImportFile -> {
if (Build.VERSION.SDK_INT >= 23) {
val hasPermission = ContextCompat.checkSelfPermission(
this@FilesActivity,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
if (!hasPermission) {
val granted = startActivityForResult(
ActivityResultContracts.RequestPermission(),
Manifest.permission.READ_EXTERNAL_STORAGE,
)
if (!granted) {
return@onReceive
}
}
}
val uri: Uri? = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*"
)
if (uri != null) {
if (it.file == null) {
val name = design.requestFileName(uri.fileName ?: "File")
client.importDocument(stack.last(), uri, name)
} else {
client.copyDocument(it.file!!.id, uri)
}
}
}
is FilesDesign.Request.ExportFile -> {
val uri: Uri? = startActivityForResult(
ActivityResultContracts.CreateDocument("text/plain"),
it.file.name
)
if (uri != null) {
client.copyDocument(uri, it.file.id)
}
}
}
} catch (e: Exception) {
design.showExceptionToast(e)
}
design.fetch(client, stack, root)
}
if (activityStarted) {
ticker.onReceive {
design.updateElapsed()
}
}
}
}
}
override fun onBackPressed() {
design?.requests?.trySend(FilesDesign.Request.PopStack)
}
private suspend fun FilesDesign.fetch(client: FilesClient, stack: Stack<String>, root: String) {
val documentId = stack.lastOrNull() ?: root
val files = if (stack.empty()) {
val list = client.list(documentId)
val config = list.firstOrNull { it.id.endsWith("config.yaml") }
if (config == null || config.size > 0) list else listOf(config)
} else {
client.list(documentId)
}
swapFiles(files, stack.empty())
}
}
@@ -0,0 +1,157 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.view.Gravity
import android.widget.Button
import android.widget.FrameLayout
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.design.BaseListActivity
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.adapter.TicketsDataAdapter
import com.github.kr328.clash.design.util.showCustomDialog
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.TicketsData
import com.github.kr328.clash.network.safeApiRequestCall
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class GongdanActivity : BaseListActivity<TicketsData>() {
override fun createAdapter(): RecyclerView.Adapter<*> {
return TicketsDataAdapter(onItemClick = { planData ->
// 在这里处理 item 点击事件
val intent = Intent(this, TicketDetailActivity::class.java)
intent.putExtra("ticketid", planData.id ?: 0)
startActivity(intent)
}, onCloseItemClick = { planData ->
showCustomDialog(
title = "提示",
message = "确定关闭当前工单吗?",
positiveButtonText = "确定",
negativeButtonText = "取消",
onPositiveClick = {
// 执行确认操作
// startActivity(PlansActivity::class.intent)
CoroutineScope(Dispatchers.IO).launch {
val apiService = ApiClient.retrofit.create(ApiService::class.java)
val fieldMap = mutableMapOf<String, Int>()
fieldMap.put("id",planData.id ?: 0)
safeApiRequestCall {
apiService.closeTicket(
PreferenceManager.loginauthData,
request = fieldMap
)
}.let {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
if (it != null && it.isSuccessful) {
withContext(Dispatchers.Main) {
Toast.makeText(this@GongdanActivity, "关闭成功", Toast.LENGTH_SHORT).show()
}
refereshData()
}else{
withContext(Dispatchers.Main) {
Toast.makeText(
this@GongdanActivity,
"关闭失败",
Toast.LENGTH_SHORT
).show()
}
}
}
}
},
onNegativeClick = {
// 执行取消操作
}
)
})
}
override fun onCreate() {
// Create the top-right icon
val iconView = Button(this)
iconView.setText("新建工单")
iconView.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.TOP or Gravity.END
).apply {
marginEnd = 16 // Add margin to the right
topMargin = 16 // Add margin to the top
}
// Set the background tint programmatically
/* ViewCompat.setBackgroundTintList(
iconView,
ContextCompat.getColorStateList(this, R.color.blue)
)
*/
val colorStateList: ColorStateList = ColorStateList(
arrayOf<IntArray>(intArrayOf(android.R.attr.state_pressed)), intArrayOf(
R.color.blue
)
)
iconView.setBackgroundTintList(colorStateList)
iconView.setOnClickListener {
val intent = Intent(this@GongdanActivity, NewGongdanActivity::class.java)
//startActivity(intent)
resultLauncher.launch(intent)
}
mainBinding?.activityBarLayout?.addView(iconView)
}
private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
val resultData = data?.getStringExtra("resultKey")
// Process the result
println("registerForActivityResult : resultData : ${resultData} ")
if (resultData == "shuaxin"){
refereshData()
}
}
}
override suspend fun loadData(page: Int, callback: (List<TicketsData>?, Throwable?) -> Unit) {
// android:backgroundTint="@color/blue"
// android:background="@drawable/rounded_button_background"
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.getTickets(PreferenceManager.loginauthData)}.let {
if (it != null && it.isSuccessful) {
callback(it.body()?.data,null)
}else{
callback(null, Throwable("请求失败"))
}
}
}
}
@@ -0,0 +1,37 @@
package com.github.kr328.clash
import android.content.Intent
import com.github.kr328.clash.design.H5WebViewDesign
import com.github.kr328.clash.design.PreferenceManager
class H5WebActivity : BaseActivity<H5WebViewDesign>() {
override suspend fun main() {
val design = H5WebViewDesign(this) {
startActivity(Intent(Intent.ACTION_VIEW).setData(it))
//Uri.parse(PreferenceManager.getConfigFromPreferences(this@ProfileActivity)?.telegramurl)
}
setContentDesign(design)
val url = intent.getStringExtra("url")
if (url != null && url.length > 1){
title = url
design.refereshApplyFrom()
design.breaseURL(url)
}else{
PreferenceManager.getConfigFromPreferences(this@H5WebActivity)?.kefuurl?.let {
design.breaseURL(
it
)
}
}
// while (isActive) {
// events.receive()
// }
}
}
@@ -0,0 +1,19 @@
package com.github.kr328.clash
import android.content.Intent
import com.github.kr328.clash.design.HelpDesign
import kotlinx.coroutines.isActive
class HelpActivity : BaseActivity<HelpDesign>() {
override suspend fun main() {
// val design = HelpDesign(this) {
// startActivity(Intent(Intent.ACTION_VIEW).setData(it))
// }
//
// setContentDesign(design)
//
// while (isActive) {
// events.receive()
// }
}
}
@@ -0,0 +1,24 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.InvitationDesign
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.ProfileDesign
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import java.util.concurrent.TimeUnit
class InvitationActivity : BaseActivity<InvitationDesign>() {
override suspend fun main() {
PreferenceManager.init(this)
val design = InvitationDesign(this)
setContentDesign(design)
design.requestdataDisible()
}
}
@@ -0,0 +1,187 @@
package com.github.kr328.clash
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.common.compat.startForegroundServiceCompat
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.fileName
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.LogcatDesign
import com.github.kr328.clash.design.dialog.withModelProgressBar
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.log.LogcatFilter
import com.github.kr328.clash.log.LogcatReader
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.io.OutputStreamWriter
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LogcatActivity : BaseActivity<LogcatDesign>() {
private var conn: ServiceConnection? = null
override suspend fun main() {
val fileName = intent?.fileName
if (fileName != null) {
val file = LogFile.parseFromFileName(fileName) ?: return showInvalid()
return mainLocalFile(file)
}
return mainStreaming()
}
private suspend fun mainLocalFile(file: LogFile) {
val messages = try {
LogcatReader(this, file).readAll()
} catch (e: Exception) {
Log.e("Fail to read log file ${file.fileName}: ${e.message}")
return showInvalid()
}
val design = LogcatDesign(this, false)
setContentDesign(design)
design.patchMessages(messages, 0, messages.size)
while (isActive) {
when (design.requests.receive()) {
LogcatDesign.Request.Delete -> {
withContext(Dispatchers.IO) {
logsDir.resolve(file.fileName).delete()
}
finish()
}
LogcatDesign.Request.Export -> {
val output = startActivityForResult(
ActivityResultContracts.CreateDocument("text/plain"),
file.fileName
)
if (output != null) {
try {
withContext(Dispatchers.IO) {
writeLogTo(messages, file, output)
}
design.showToast(R.string.file_exported, ToastDuration.Long)
} catch (e: Exception) {
design.showExceptionToast(e)
}
}
}
else -> Unit
}
}
}
private suspend fun mainStreaming() {
val design = LogcatDesign(this, true)
setContentDesign(design)
startForegroundServiceCompat(LogcatService::class.intent)
val logcat = bindLogcatService()
val ticker = ticker(500)
var initial = true
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
LogcatDesign.Request.Close -> {
stopService(LogcatService::class.intent)
finish()
}
else -> Unit
}
}
if (activityStarted) {
ticker.onReceive {
val snapshot = logcat.snapshot(initial) ?: return@onReceive
design.patchMessages(snapshot.messages, snapshot.removed, snapshot.appended)
initial = false
}
}
}
}
}
override fun onDestroy() {
conn?.apply(this::unbindService)
super.onDestroy()
}
private suspend fun bindLogcatService(): LogcatService {
return suspendCoroutine { ctx ->
bindService(LogcatService::class.intent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val srv = service!!.queryLocalInterface("") as LogcatService
ctx.resume(srv)
conn = this
}
override fun onServiceDisconnected(name: ComponentName?) {
conn = null
}
}, Context.BIND_AUTO_CREATE)
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun writeLogTo(messages: List<LogMessage>, file: LogFile, uri: Uri) {
LogcatFilter(OutputStreamWriter(contentResolver.openOutputStream(uri)), this).use {
withContext(Dispatchers.Main) {
withModelProgressBar {
configure {
isIndeterminate = true
max = messages.size
}
withContext(Dispatchers.IO) {
it.writeHeader(file.date)
messages.forEachIndexed { idx, msg ->
configure {
isIndeterminate = false
progress = idx
}
it.writeMessage(msg)
}
}
}
}
}
}
private fun showInvalid() {
Toast.makeText(this, R.string.invalid_log_file, Toast.LENGTH_LONG).show()
}
}
@@ -0,0 +1,164 @@
package com.github.kr328.clash
import android.app.PendingIntent
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Binder
import android.os.IBinder
import android.os.IInterface
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.github.kr328.clash.common.compat.getColorCompat
import com.github.kr328.clash.common.compat.pendingIntentFlags
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.log.LogcatCache
import com.github.kr328.clash.log.LogcatWriter
import com.github.kr328.clash.service.RemoteService
import com.github.kr328.clash.service.remote.ILogObserver
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.remote.unwrap
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.io.IOException
import java.util.*
class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.Default), IInterface {
private val cache = LogcatCache()
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
stopSelf()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
startObserver(service ?: return stopSelf())
}
}
override fun onCreate() {
super.onCreate()
running = true
createNotificationChannel()
showNotification()
bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
}
override fun onDestroy() {
cancel()
unbindService(connection)
stopForeground(true)
running = false
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder {
return this.asBinder()
}
override fun asBinder(): IBinder {
return object : Binder() {
override fun queryLocalInterface(descriptor: String): IInterface {
return this@LogcatService
}
}
}
suspend fun snapshot(full: Boolean): LogcatCache.Snapshot? {
return cache.snapshot(full)
}
private fun startObserver(binder: IBinder) {
if (!binder.isBinderAlive)
return stopSelf()
launch(Dispatchers.IO) {
val service = binder.unwrap(IRemoteService::class).clash()
val channel = Channel<LogMessage>(CACHE_CAPACITY)
try {
logsDir.mkdirs()
LogcatWriter(this@LogcatService).use {
val observer = object : ILogObserver {
override fun newItem(log: LogMessage) {
channel.trySend(log)
}
}
service.setLogObserver(observer)
while (isActive) {
val msg = channel.receive()
it.appendMessage(msg)
cache.append(msg)
}
}
} catch (e: IOException) {
Log.e("Write log file: $e", e)
} finally {
withContext(NonCancellable) {
if (binder.isBinderAlive) {
service.setLogObserver(null)
}
stopSelf()
}
}
}
}
private fun createNotificationChannel() {
NotificationManagerCompat.from(this)
.createNotificationChannel(
NotificationChannelCompat.Builder(
CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_DEFAULT
).setName(getString(R.string.clash_logcat)).build()
)
}
private fun showNotification() {
val notification = NotificationCompat
.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo_service)
.setColor(getColorCompat(R.color.color_clash_light))
.setContentTitle(getString(R.string.clash_logcat))
.setContentText(getString(R.string.running))
.setContentIntent(
PendingIntent.getActivity(
this,
R.id.nf_logcat_status,
LogcatActivity::class.intent
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP),
pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT)
)
)
.build()
startForeground(R.id.nf_logcat_status, notification)
}
companion object {
private const val CHANNEL_ID = "clash_logcat_channel"
private const val CACHE_CAPACITY = 128
var running: Boolean = false
}
}
@@ -0,0 +1,267 @@
package com.github.kr328.clash
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.databinding.ActivityLoginBinding
import com.github.kr328.clash.design.databinding.ActivityProfileBinding
import com.github.kr328.clash.design.ui.DayNight
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.layoutInflater
import com.github.kr328.clash.design.util.resolveThemedBoolean
import com.github.kr328.clash.design.util.resolveThemedColor
import com.github.kr328.clash.design.util.root
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiClientConfig
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.ConfigResponse
import com.github.kr328.clash.network.LoginRequest
import com.github.kr328.clash.network.LoginResponse
import com.github.kr328.clash.network.safeApiCall
import com.github.kr328.clash.network.safeApiRequestCall
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
class LoginActivity : AppCompatActivity() {
private lateinit var apiService: ApiService
private var dayNight: DayNight = DayNight.Day
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
// 禁用返回键
// 不调用 super.onBackPressed()
}
private var isPasswordVisible = false
private var configRetryCount = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityLoginBinding
.inflate(this.layoutInflater, this.root, false)
setContentView(binding.root)
applyDayNight()
PreferenceManager.init(this)
binding.togglePasswordVisibility.setOnClickListener {
isPasswordVisible = !isPasswordVisible
if (isPasswordVisible) {
// 显示明文密码
binding.loginPasswordEditText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
binding.togglePasswordVisibility.setImageResource(R.drawable.visibility_24px) // 更改图标为显示状态
} else {
// 显示星号密码
binding.loginPasswordEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
binding.togglePasswordVisibility.setImageResource(R.drawable.visibility_off_24px) // 更改图标为隐藏状态
}
// 将光标移动到文本的末尾
binding.loginPasswordEditText.setSelection(binding.loginPasswordEditText.text.length)
}
binding.loginLoginButton.setOnClickListener {
val email = binding.loginEmailEditText.text.toString()
val password = binding.loginPasswordEditText.text.toString()
val isAgreementChecked = binding.loginAgreementCheckBox.isChecked
if (email.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "请输入邮箱和密码", Toast.LENGTH_SHORT).show()
} else if (!isAgreementChecked) {
Toast.makeText(this, "请同意隐私政策和用户协议", Toast.LENGTH_SHORT).show()
} else {
// Hide the keyboard
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val currentFocusView = currentFocus
if (currentFocusView != null) {
inputMethodManager.hideSoftInputFromWindow(currentFocusView.windowToken, 0)
}
performLogin(email, password)
}
}
binding.loginForgotPassword.setOnClickListener {
// Navigate to Forgot Password screen or show dialog
}
binding.loginRegister.setOnClickListener {
// Navigate to Register screen
//startActivity(RegisterActivity::class::intent)
// startActivity(RegisterActivity::class::intent)
val intent = Intent(this@LoginActivity, RegisterActivity::class.java)
startActivity(intent)
// finish() // It
}
apiService = ApiClientConfig.retrofit.create(ApiService::class.java)
//获取 Config 数据
getconfig()
}
private fun getconfig(){
runOnUiThread {
LoadingDialog.show(this, "正在初始化...")
}
CoroutineScope(Dispatchers.IO).launch {
safeApiCall {apiService.getConfig()}.let {
if (it != null) {
if (it.code == 1 ) {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
PreferenceManager.saveConfigToPreferences( it)
//重新初始化 ApiClint
}
}else{
configRetryCount++
if (configRetryCount < 3) {
getconfig()
} else {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
Toast.makeText(this@LoginActivity, "初始化失败,请检查配置是否正确", Toast.LENGTH_LONG).show()
}
}
}
}
}
}
/**
* withContext(Dispatchers.Main) {
* if (response.isSuccessful) {
* val userData = response.body()
* // 更新 UI
* } else {
* Toast.makeText(this@MainActivity, "Failed to fetch user data", Toast.LENGTH_SHORT).show()
* }
* }
* */
private fun applyDayNight(config: Configuration = resources.configuration) {
// val dayNight = theme.applyStyle(R.style.AppThemeLight, true) //默认白天模式
/*
val dayNight = queryDayNight(config)
when (dayNight) {
DayNight.Night -> theme.applyStyle(R.style.AppThemeDark, true)
DayNight.Day -> theme.applyStyle(R.style.AppThemeLight, true)
}*/
window.isAllowForceDarkCompat = false
window.isSystemBarsTranslucentCompat = true
window.statusBarColor = resolveThemedColor(android.R.attr.statusBarColor)
window.navigationBarColor = resolveThemedColor(android.R.attr.navigationBarColor)
if (Build.VERSION.SDK_INT >= 23) {
window.isLightStatusBarsCompat = resolveThemedBoolean(android.R.attr.windowLightStatusBar)
}
if (Build.VERSION.SDK_INT >= 27) {
window.isLightNavigationBarCompat = resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
}
this.dayNight = DayNight.Night //dayNight
}
private fun performLogin(email: String, password: String) {
// Perform login logic here, possibly calling an API
// Show the loading indicator with a custom message
LoadingDialog.show(this, "正在登录...")
val apiServiceApp = ApiClient.retrofit.create(ApiService::class.java)
//获取 Config 数据
CoroutineScope(Dispatchers.IO).launch {
safeApiRequestCall {
apiServiceApp.loginUser(LoginRequest(email,password))}.let {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
if (it != null &&
it.isSuccessful) {
val response: LoginResponse? = it.body()
println(response )
println(it.errorBody()?.string() ?: "")
if (response?.data != null){
PreferenceManager.loginemail = email
PreferenceManager.loginToken = response.data?.token ?: ""
PreferenceManager.loginauthData = response.data?.auth_data ?: ""
PreferenceManager.isLoginin = true
// saveLoginMailToken(this@LoginActivity,email)
// saveLoginToken(this@LoginActivity, response.data.token ?: "")
// saveLoginAuthData(this@LoginActivity, response.data.auth_data ?: "")
// saveLoginStatus(this@LoginActivity,true)
//获取用户信息
withContext(Dispatchers.Main) {
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish() // It
}
}
}else{
// Handle error response, even if it's 422
try {
val errorResponse = it?.errorBody()?.string() ?: ""
print(errorResponse)
val errorJson = JSONObject(errorResponse)
val message = errorJson.optString("message")
// Process the error message or show it to the user
if (!message.isNullOrEmpty()) {
withContext(Dispatchers.Main) {
Toast.makeText(this@LoginActivity, "登录失败:${message}", Toast.LENGTH_LONG).show()
}
}
println( "Message: $message, ")
} catch (e: JSONException) {
// Handle JSON parsing error
withContext(Dispatchers.Main) {
Toast.makeText(this@LoginActivity, "登录失败:${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
}
}
}
@@ -0,0 +1,71 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setFileName
import com.github.kr328.clash.design.LogsDesign
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
class LogsActivity : BaseActivity<LogsDesign>() {
override suspend fun main() {
if (LogcatService.running) {
return startActivity(LogcatActivity::class.intent)
}
val design = LogsDesign(this)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart -> {
val files = withContext(Dispatchers.IO) {
loadFiles()
}
design.patchLogs(files)
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
LogsDesign.Request.StartLogcat -> {
startActivity(LogcatActivity::class.intent)
finish()
}
LogsDesign.Request.DeleteAll -> {
if (design.requestDeleteAll()) {
withContext(Dispatchers.IO) {
deleteAllLogs()
}
events.trySend(Event.ActivityStart)
}
}
is LogsDesign.Request.OpenFile -> {
startActivity(LogcatActivity::class.intent.setFileName(it.file.fileName))
}
}
}
}
}
}
private fun loadFiles(): List<LogFile> {
val list = cacheDir.resolve("logs").listFiles()?.toList() ?: emptyList()
return list.mapNotNull { LogFile.parseFromFileName(it.name) }
}
private fun deleteAllLogs() {
logsDir.deleteRecursively()
}
}
@@ -0,0 +1,784 @@
package com.github.kr328.clash
import android.os.Handler
import android.os.Looper
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.design.MainDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.util.startClashService
import com.github.kr328.clash.util.stopClashService
import com.github.kr328.clash.util.withClash
import com.github.kr328.clash.util.withProfile
import com.github.kr328.clash.core.bridge.*
import com.github.kr328.clash.core.model.FetchStatus
import com.github.kr328.clash.core.model.ProxySort
import com.github.kr328.clash.core.model.TunnelState
import com.github.kr328.clash.design.NewProfileDesign
import com.github.kr328.clash.design.PreferenceLiveData
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.R
import com.github.kr328.clash.design.component.ProxyMenu
import com.github.kr328.clash.design.databinding.ActivitySplashBinding
import com.github.kr328.clash.design.dialog.requestModelTextInput
import com.github.kr328.clash.design.dialog.withModelProgressBar
import com.github.kr328.clash.design.network.APIGlobalObject
import com.github.kr328.clash.design.util.ValidatorHttpUrl
import com.github.kr328.clash.design.util.root
import com.github.kr328.clash.design.util.showCustomDialog
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiClientConfig
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.ConfigResponse
import com.github.kr328.clash.network.LoginRequest
import com.github.kr328.clash.network.SubscribeData
import com.github.kr328.clash.network.SubscribeResponse
import com.github.kr328.clash.network.safeApiCall
import com.github.kr328.clash.network.safeApiRequestCall
import com.github.kr328.clash.service.ProfileManager
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.util.PreferenceManagerServer
import com.github.kr328.clash.utity.LoadingDialog
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.timer
class MainActivity : BaseActivity<MainDesign>() {
private lateinit var apiService: ApiService
private var subData: SubscribeData? = null
private val selectnodeName: LiveData<String> by lazy {
PreferenceLiveData(this)
}
override suspend fun main() {
val design = MainDesign(this)
if (PreferenceManager.isLoginin){
setContentDesign(design)
design.fetch()
design.startBannsers()
//1 : 查询 config 信息
apiService = ApiClientConfig.retrofit.create(ApiService::class.java)
//获取 Config 数据
// CoroutineScope(Dispatchers.IO).launch { }
val currentTime = System.currentTimeMillis()
val cacheExpiryTime = 10 * 60 * 1000 // 10 minutes in milliseconds
// 如果缓存数据存在且未超过 10 分钟,使用缓存数据;否则,重新请求数据
if ( PreferenceManager.cached_data != null && (currentTime - PreferenceManager.cache_timestamp) <= cacheExpiryTime) {
// 使用缓存数据
val gson = Gson()
val configResponse = gson.fromJson(PreferenceManager.cached_data, ConfigResponse::class.java)
println(configResponse)
println("Using cached data: ${PreferenceManager.cached_data}")
println("Using cached_userSubscritedata : ${PreferenceManager.cached_userSubscritedata}")
if (PreferenceManager.cached_userSubscritedata.length > 0){
val subcache = gson.fromJson(PreferenceManager.cached_userSubscritedata, SubscribeResponse::class.java)
subData = subcache.data
//存储到本地
val plainid = subcache.data.plan_id ?: 0
// 或者检查使用量
val guoqi = (subcache.data.u ) + (subcache.data.d) > (subcache.data.transfer_enable ?: 0) && (subcache.data.transfer_enable ?: 0) > 0
if ( plainid == 0 || guoqi ) {
//提示到期
//删除所有节点
withProfile {
var allProfiles = queryAll()
println(allProfiles)
//删除所有节点
if (allProfiles.isNotEmpty() && allProfiles.count() > 0){
allProfiles.forEach {
delete(it.uuid)
println("Deleted ${it.uuid} ${it.name}: ${it.source}")
}
}
}
//删除所有节点文件
withContext(Dispatchers.Main) {
showCustomDialog(
title = "提示",
message = "您的订阅已到期,请续费订阅开启服务",
positiveButtonText = "确定",
negativeButtonText = "取消",
onPositiveClick = {
// 执行确认操作
startActivity(PlansActivity::class.intent)
},
onNegativeClick = {
// 执行取消操作
}
)
}
}
}
println("subData ${subData}")
}else{
// 缓存已过期,重新请求数据
println("Cache expired, requesting new data")
// 重新请求数据
//requestNewData()
LoadingDialog.show(this, "正在更新节点数据...")
configRequesting()
}
selectnodeName.observe(this, androidx.lifecycle.Observer { newValue ->
})
val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart,
Event.ServiceRecreated,
Event.ClashStop, Event.ClashStart,
Event.ProfileLoaded, Event.ProfileChanged -> design.fetch()
else -> Unit
}
}
design.requests.onReceive {
when (it) {
MainDesign.Request.ToggleStatus -> {
if (clashRunning)
stopClashService()
else
design.startClash()
}
MainDesign.Request.OpenProxy ->
startActivity(ProxyActivity::class.intent)
MainDesign.Request.OpenProfiles ->
startActivity(ProfilesActivity::class.intent)
MainDesign.Request.OpenProviders ->
startActivity(ProvidersActivity::class.intent)
MainDesign.Request.OpenLogs ->
startActivity(LogsActivity::class.intent)
MainDesign.Request.OpenSettings ->
startActivity(SettingsActivity::class.intent)
MainDesign.Request.OpenHelp ->
startActivity(HelpActivity::class.intent)
MainDesign.Request.OpenAbout ->
design.showAbout(queryAppVersionName())
MainDesign.Request.OpenSettingsDIY ->
startActivity(ProfileActivity::class.intent)
MainDesign.Request.OpenSettingsKEFU ->
startActivity(PlansActivity::class.intent)
MainDesign.Request.OpenModeDirect ->
withClash {
PreferenceManager.modeName = TunnelState.Mode.Direct.name
val override = queryOverride(Clash.OverrideSlot.Session)
override.mode = TunnelState.Mode.Direct
patchOverride(Clash.OverrideSlot.Session, override)
}
MainDesign.Request.OpenModeGlobal -> withClash {
CoroutineScope(Dispatchers.Main).launch {
PreferenceManager.modeName = TunnelState.Mode.Global.name
LoadingDialog.show(this@MainActivity, "正在切换中...")
val names = withClash { queryProxyGroupNames(uiStore.proxyExcludeNotSelectable) }
withClash {
val o = queryOverride(Clash.OverrideSlot.Session)
o.mode = TunnelState.Mode.Global
patchOverride(Clash.OverrideSlot.Session, o)
}
startServer()
}
}
MainDesign.Request.OpenModeRule -> withClash {
CoroutineScope(Dispatchers.Main).launch {
PreferenceManager.modeName = TunnelState.Mode.Rule.name
LoadingDialog.show(this@MainActivity, "正在切换中...")
withClash {
val o = queryOverride(Clash.OverrideSlot.Session)
o.mode = TunnelState.Mode.Rule
patchOverride(Clash.OverrideSlot.Session, o)
}
startServer()
}
}
MainDesign.Request.OpenModeMenu -> {
design.OpenModeMenu()
}
MainDesign.Request.OpenChangeMode -> {
design.OpenModeMenu()
}
MainDesign.Request.OpenModeSheet ->{
design.OpenModeSheet()
}
}
}
if (clashRunning) {
ticker.onReceive {
design.fetchTraffic()
}
}
}
}
}else{
val binding = ActivitySplashBinding
.inflate(this.layoutInflater, this.root, false)
setContentView(binding.root)
Handler(Looper.getMainLooper()).postDelayed({
startActivity(LoginActivity::class.intent)
}, 1000) // Simulating a network delay
}
}
private suspend fun configRequesting(retryCount: Int = 0){
val gson = Gson()
val currentTime = System.currentTimeMillis()
/*apiService.getConfig().let {
if (it.isSuccessful) {
val response: ConfigResponse = it.body() ?: throw Exception("Response body is null")
if (response.code == 1 ) {
PreferenceManager.saveConfigToPreferences(response)
//更新缓存数据
PreferenceManager.cache_timestamp = currentTime
PreferenceManager.cached_data = gson.toJson( it.body())
println("API Response: ${gson.toJson( it.body())}")
//重新初始化 ApiClint
//2 : 获取订阅节点信息
performSubscrite()
}
} else {
println("API Error: ${it.errorBody()}")
configRequesting()
}
}
*/
safeApiCall {apiService.getConfig()}.let {
if (it !=null){
if (it.code == 1 ) {
PreferenceManager.saveConfigToPreferences(it)
//更新缓存数据
PreferenceManager.cache_timestamp = currentTime
PreferenceManager.cached_data = gson.toJson( it)
println("API Response: ${it}")
//重新初始化 ApiClint
//2 : 获取订阅节点信息
performSubscrite()
}
}else{
if (retryCount < 3) {
configRequesting(retryCount + 1)
} else {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
showCustomDialog(
title = "提示",
message = "初始化失败,请检查配置是否正确",
positiveButtonText = "确定",
negativeButtonText = "",
onPositiveClick = {
},
onNegativeClick = {
}
)
}
}
}
}
}
private fun performSubscrite(){
val apiServiceApp = ApiClient.retrofit.create(ApiService::class.java)
//获取 Config 数据
CoroutineScope(Dispatchers.IO).launch {
safeApiRequestCall {
apiServiceApp.getSubscribe(PreferenceManager.loginauthData)}.let {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
if (it != null &&
it.isSuccessful) {
println(" performSubscrite : ${it.body()?.data}")
subData = it.body()?.data
val gson = Gson()
PreferenceManager.cached_userSubscritedata = gson.toJson( it.body())
APIGlobalObject.subData = it.body()?.data
//存储到本地
val plainid = it.body()?.data?.plan_id ?: 0
// 或者检查使用量
val guoqi = (it.body()?.data?.u ?: 0) + (it.body()?.data?.d ?: 0) > (it.body()?.data?.transfer_enable ?: 0) && (it.body()?.data?.transfer_enable ?: 0) > 0
if ( plainid == 0 || guoqi ) {
//提示到期
//删除所有节点
withProfile {
var allProfiles = queryAll()
println(allProfiles)
if (allProfiles.isNotEmpty() && allProfiles.count() > 1){
allProfiles.forEach { delete(it.uuid) }
//删除所有节点
}
}
//删除所有节点文件
withContext(Dispatchers.Main) {
showCustomDialog(
title = "提示",
message = "您的订阅已到期,请续费订阅开启服务",
positiveButtonText = "确定",
negativeButtonText = "取消",
onPositiveClick = {
// 执行确认操作
startActivity(PlansActivity::class.intent)
},
onNegativeClick = {
// 执行取消操作
}
)
}
}else{
//更新本地节点
withProfile {
var profileActive = queryActive()
if (profileActive != null){
try {
println(">>>>>>>>> 更新节点订阅地址:${profileActive.uuid} ${profileActive.source}")
update(profileActive.uuid)
}catch (e: Exception){
println(">>>>>>>>> 更新节点订阅地址 catch${e.message}")
}
finally {
println(">>>>>>>>> 更新节点订阅地址 完成 finally")
}
}
}
}
}
}
}
}
private suspend fun MainDesign.fetch() {
setClashRunning(clashRunning)
val state = withClash {
queryTunnelState()
}
println("运行时查询系统mode模式: ${state}")
val providers = withClash {
queryProviders()
}
if (providers.isNotEmpty()) {
println("模式providers ${providers}")
providers.forEach { println("The element is ${it.name}") }
}
setMode(state.mode)
setHasProviders(providers.isNotEmpty())
withProfile {
setProfileName(queryActive()?.name)
setSelectNodeName(PreferenceManager.selectnodeName )
}
}
//查询上传速度和下载速度
private suspend fun MainDesign.fetchTraffic() {
withClash {
// setForwarded(queryTrafficTotal())
setUploadedSeep(queryTrafficTotal())
setDownloadedSeep(queryTrafficTotal())
}
}
private suspend fun MainDesign.startClash() {
//删除所有节点
withProfile {
var allProfiles = queryAll()
if (allProfiles.isNotEmpty() && allProfiles.count() > 1){ //删除超过多余的 1 个节点之外的节点
allProfiles.forEach { delete(it.uuid) }
//删除所有节点
}
}
// val activeProfile = withProfile { queryActive() }
// if (activeProfile != null) {
// android.util.Log.i("activeProfile",">> ${activeProfile.type.name} ${activeProfile.uuid} ${activeProfile.source} ")
// showToast(R.string.no_profile_selected, ToastDuration.Long) {
// setAction(R.string.profiles) {
// startActivity(ProfilesActivity::class.intent)
// }
// }
// }
val activeProfile = withProfile { queryActive() }
println("当前订阅文件:${activeProfile}")
if (activeProfile == null ) { //|| !activeProfile.imported
LoadingDialog.show(this@MainActivity, "正在请求节点数据...")
// showToast("正在请求节点数据....", ToastDuration.Long)
//create(Profile.Type.Url, "VPN")
withProfile{
println(">>>>>>>>>${subData}")
//https://hongxingdl.com/hxvip?token=bc2f2839ccc608f34d6a6fc90b6e7f15
//https://hongxingdl.com/api/v1/client/subscribe?token=bc2f2839ccc608f34d6a6fc90b6e7f15
//"https://api.0009.uk/api/v1/client/subscribe?token=5652e3eec20c553047dbe3fcf8754090"
if (subData?.subscribe_url != null && subData?.plan?.name != null && subData?.plan?.id != null) {
var suburl = subData?.subscribe_url ?: ""
var encodeURL = java.net.URLEncoder.encode(suburl, "utf-8")
encodeURL = encodeURL.replace("?","%3F")
var newULR =suburl
println(">>>>>>>>> "+suburl)
if( ApiClientConfig.ConfigNodeURL.length > 3){
newULR = ApiClientConfig.ConfigNodeURL + encodeURL
}
println(">>>>>>>>> "+newULR)
//val uuid: UUID = create(Profile.Type.Url,subData?.plan?.name ?: "VPN",newULR)
launch {
val uuid2 = withProfile {
val type = Profile.Type.Url
val name = subData?.plan?.name ?: "VPN"
create(type, name).also {
patch(it, name, newULR, 0)
}
}
withProcessing { updateStatus ->
withProfile {
try {
commit(uuid2) {
launch {
activeProfile()
updateStatus(it)
}
}
} catch (e: Exception) {
LoadingDialog.hide()
showExceptionToast("初始化节点数据失败,请检查网络重试")
}
coroutineScope {
}
}
}
/* coroutineScope {
commit(uuid2) {
println(">>>>>>>>>> ${it.progress} ${it.action} ${it.args.toString()} ${it} ")
if (it.action == FetchStatus.Action.Verifying){
// 在协程中启动 activeProfile 调用
CoroutineScope(Dispatchers.IO).launch {
activeProfile()
//自动开启订阅
startServer()
}
}
}
}*/
}
/* commit(uuid){
println(">> ${it.progress} ${it.action} ${it.args.toString()} ")
if (it.action == FetchStatus.Action.Verifying){
// 在协程中启动 activeProfile 调用
CoroutineScope(Dispatchers.IO).launch {
activeProfile()
//自动开启订阅
startServer()
}
}
}*/
}else{
//重新请求网络
performSubscrite()
}
/*
if (subData == null){
withContext(Dispatchers.Main){
LoadingDialog.hide()
}
performSubscrite()
}else{
val plainid =subData?.plan_id ?: 0
// 或者检查使用量
val guoqi = (subData?.u ?: 0) + (subData?.d ?: 0) > (subData?.transfer_enable ?: 0) && (subData?.transfer_enable ?: 0) > 0
if ( plainid == 0 || guoqi ) {
//提示到期
performSubscrite()
}
}
*/
}
}else{
// showToast(R.string.no_profile_selected, ToastDuration.Long) {
// setAction(R.string.profiles) {
// startActivity(ProfilesActivity::class.intent)
// }
// }
println("当前订阅文件>> ${activeProfile.type.name} ${activeProfile.uuid} ${activeProfile.imported} ")
withProfile {
activeProfile.uuid
}
startServer()
}
}
suspend fun withProcessing(executeTask: suspend (suspend (FetchStatus) -> Unit) -> Unit) {
try {
executeTask {
// applyFrom(it)
println("applyFrom ${it.progress} ${it}")
if (it.action == FetchStatus.Action.Verifying) {
//验证通过
CoroutineScope(Dispatchers.IO).launch {
//选中节点
activeProfile()
//自动开启订阅
startServer()
}
}else{
//请求中
}
}
} finally {
}
}
private suspend fun startServer(){
//读取配置文件
val vpnRequest = startClashService()
try {
if (vpnRequest != null) {
val result = startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
vpnRequest
)
if (result.resultCode == RESULT_OK)
{
startClashService()
withContext(Dispatchers.Main){
LoadingDialog.hide()
}
}
}
println("缓存配置模式:${PreferenceManager.modeName}")
CoroutineScope(Dispatchers.Main).launch {
// Delay for 3 seconds
//第一次全部使用全局模式连接
delay(500)
withClash {
// Query the current mode
val override = queryOverride(Clash.OverrideSlot.Session)
// Toggle between Rule and Global modes
if (PreferenceManager.modeName == TunnelState.Mode.Global.name){
override.mode = TunnelState.Mode.Global
}else{
override.mode = TunnelState.Mode.Rule //第一次默认使用智能 模式连接
}
// Apply the new mode`
patchOverride(Clash.OverrideSlot.Session, override)
//模式切换完毕
// Optional: Log or display the current mode for verification
println("模式切换 Clash mode is now set to: ${override.mode}")
delay(500)
//如果切换全局模式,默认 直接主动选择节点 根据缓存来
//if (override.mode == TunnelState.Mode.Global){
val names = queryProxyGroupNames(uiStore.proxyExcludeNotSelectable)
println("模式切换>>>>>>>>>>>>>"+names)
if ( names.size > 0){
var progxys = queryProxyGroup(names[0], ProxySort.Default)
if (progxys.proxies.first().title.length > 0){
println("模式切换>>>>>>>>>>>>>: "+progxys.proxies.last())
if ( progxys.proxies.size > 2)
{
val zutuolist = progxys.proxies.filter { it.title == "自动选择" }
var autoNode = ""
if (PreferenceManager.selectnodeName.length > 0){
autoNode = PreferenceManager.selectnodeName
} else{
if (zutuolist.size > 0){
//说明存在自动选择
autoNode = "自动选择"
}else{
autoNode = progxys.proxies.first().title
}
PreferenceManager.selectnodeName = autoNode // progxys.proxies.last().title
}
patchSelector(names.first(),autoNode) // progxys.proxies.last().title
withContext(Dispatchers.Main){
design?.setSelectNodeName(PreferenceManager.selectnodeName)
}
}
}
}
// }
withContext(Dispatchers.Main){
LoadingDialog.hide()
}
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main){
LoadingDialog.hide()
}
//design?.showToast(R.string.unable_to_start_vpn, ToastDuration.Long)
}
}
private suspend fun activeProfile(){
withProfile {
setActive(queryAll().first())
}
}
private suspend fun queryAppVersionName(): String {
return withContext(Dispatchers.IO) {
packageManager.getPackageInfo(packageName, 0).versionName// + "\n" + Bridge.nativeCoreVersion().replace("_", "-")
}
}
}
@@ -0,0 +1,75 @@
package com.github.kr328.clash
import android.app.Application
import android.content.Context
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.compat.currentProcessName
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.service.util.PreferenceManagerServer
import com.github.kr328.clash.service.util.sendServiceRecreated
import com.github.kr328.clash.util.clashDir
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@Suppress("unused")
class MainApplication : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
PreferenceManagerServer.init(this)
PreferenceManager.init(this)
Global.init(this)
}
override fun onCreate() {
super.onCreate()
val processName = currentProcessName
extractGeoFiles()
Log.d("Process $processName started")
if (processName == packageName) {
Remote.launch()
} else {
sendServiceRecreated()
}
}
private fun extractGeoFiles() {
clashDir.mkdirs();
val geoipFile = File(clashDir, "geoip.metadb")
if(!geoipFile.exists()) {
FileOutputStream(geoipFile).use {
assets.open("geoip.metadb").copyTo(it);
}
}
val geositeFile = File(clashDir, "geosite.dat")
if(!geositeFile.exists()) {
FileOutputStream(geositeFile).use {
assets.open("geosite.dat").copyTo(it);
}
}
val ASNFile = File(clashDir, "ASN.mmdb")
if(!ASNFile.exists()) {
FileOutputStream(ASNFile).use {
assets.open("ASN.mmdb").copyTo(it);
}
}
}
fun finalize() {
Global.destroy()
}
}
@@ -0,0 +1,136 @@
package com.github.kr328.clash
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.design.MetaFeatureSettingsDesign
import com.github.kr328.clash.util.clashDir
import com.github.kr328.clash.util.withClash
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
class MetaFeatureSettingsActivity : BaseActivity<MetaFeatureSettingsDesign>() {
override suspend fun main() {
val configuration = withClash { queryOverride(Clash.OverrideSlot.Persist) }
defer {
withClash {
patchOverride(Clash.OverrideSlot.Persist, configuration)
}
}
val design = MetaFeatureSettingsDesign(
this,
configuration
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
MetaFeatureSettingsDesign.Request.ResetOverride -> {
if (design.requestResetConfirm()) {
defer {
withClash {
clearOverride(Clash.OverrideSlot.Persist)
}
}
finish()
}
}
MetaFeatureSettingsDesign.Request.ImportGeoIp -> {
val uri = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*")
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportGeoIp)
}
MetaFeatureSettingsDesign.Request.ImportGeoSite -> {
val uri = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*")
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportGeoSite)
}
MetaFeatureSettingsDesign.Request.ImportCountry -> {
val uri = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*")
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportCountry)
}
MetaFeatureSettingsDesign.Request.ImportASN -> {
val uri = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*")
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportASN)
}
}
}
}
}
}
private val validDatabaseExtensions = listOf(
".metadb", ".db", ".dat", ".mmdb"
)
private suspend fun importGeoFile(uri: Uri?, importType: MetaFeatureSettingsDesign.Request) {
val cursor: Cursor? = uri?.let {
contentResolver.query(it, null, null, null, null, null)
}
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val displayName: String =
if (columnIndex != -1) it.getString(columnIndex) else "";
val ext = "." + displayName.substringAfterLast(".")
if (!validDatabaseExtensions.contains(ext)) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.geofile_unknown_db_format)
.setMessage(getString(R.string.geofile_unknown_db_format_message,
validDatabaseExtensions.joinToString("/")))
.setPositiveButton("OK") { _, _ -> }
.show()
return
}
val outputFileName = when (importType) {
MetaFeatureSettingsDesign.Request.ImportGeoIp ->
"geoip$ext"
MetaFeatureSettingsDesign.Request.ImportGeoSite ->
"geosite$ext"
MetaFeatureSettingsDesign.Request.ImportCountry ->
"country$ext"
MetaFeatureSettingsDesign.Request.ImportASN ->
"ASN$ext"
else -> ""
}
withContext(Dispatchers.IO) {
val outputFile = File(clashDir, outputFileName);
contentResolver.openInputStream(uri).use { ins ->
FileOutputStream(outputFile).use { outs ->
ins?.copyTo(outs)
}
}
}
Toast.makeText(this, getString(R.string.geofile_imported, displayName),
Toast.LENGTH_LONG).show()
return
}
}
Toast.makeText(this, R.string.geofile_import_failed, Toast.LENGTH_LONG).show()
}
}
@@ -0,0 +1,37 @@
package com.github.kr328.clash
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.BaseListActivity
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.adapter.OrdersDataAdapter
import com.github.kr328.clash.design.adapter.PlanDataAdapter
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.OrderData
import com.github.kr328.clash.network.PlanData
import com.github.kr328.clash.network.safeApiRequestCall
class MyOrdersActivity : BaseListActivity<OrderData>() {
override fun createAdapter(): RecyclerView.Adapter<*> {
return OrdersDataAdapter(){ item ->
startActivity(SubmitOrderActivity::class.intent.putExtra("trade_no",item.trade_no))
}
}
override fun onCreate() {}
override suspend fun loadData(page: Int, callback: (List<OrderData>?, Throwable?) -> Unit) {
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.getOrders(PreferenceManager.loginauthData)}.let {
if (it!= null && it.isSuccessful) {
callback(it.body()?.data,null)
}else{
callback(null, Throwable("请求失败"))
}
}
}
}
@@ -0,0 +1,64 @@
package com.github.kr328.clash
import android.content.pm.PackageManager
import com.github.kr328.clash.common.util.componentName
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.NetworkSettingsDesign
import com.github.kr328.clash.design.model.Behavior
import com.github.kr328.clash.service.store.ServiceStore
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class NetworkSettingsActivity : BaseActivity<NetworkSettingsDesign>(), Behavior {
override suspend fun main() {
val design = NetworkSettingsDesign(
this,
uiStore,
this,
ServiceStore(this),
clashRunning,
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ClashStart, Event.ClashStop, Event.ServiceRecreated ->
recreate()
else -> Unit
}
}
design.requests.onReceive {
when (it) {
NetworkSettingsDesign.Request.StartAccessControlList ->
startActivity(AccessControlActivity::class.intent)
}
}
}
}
}
override var autoRestart: Boolean
get() {
val status = packageManager.getComponentEnabledSetting(
RestartReceiver::class.componentName
)
return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
}
set(value) {
val status = if (value)
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
packageManager.setComponentEnabledSetting(
RestartReceiver::class.componentName,
status,
PackageManager.DONT_KILL_APP,
)
}
}
@@ -0,0 +1,51 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.HelpDesign
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
class NewGongdanActivity : BaseActivity<HelpDesign>() {
override suspend fun main() {
val design = HelpDesign(this) {
startActivity(Intent(Intent.ACTION_VIEW).setData(it))
}
setContentDesign(design)
//
// while (isActive) {
// events.receive()
// }
//val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
println("events : " + it.name)
}
design.requests.onReceive {
println("requests : " + it.name)
when (it) {
HelpDesign.Request.CommitSuccess -> {
withContext(Dispatchers.Main) {
val resultIntent = Intent()
resultIntent.putExtra("resultKey", "shuaxin")
setResult(Activity.RESULT_OK, resultIntent)
finish()
}
}
}
}
}
}
}
}
@@ -0,0 +1,145 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.design.NewProfileDesign
import com.github.kr328.clash.design.model.ProfileProvider
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.*
class NewProfileActivity : BaseActivity<NewProfileDesign>() {
private val self: NewProfileActivity
get() = this
override suspend fun main() {
val design = NewProfileDesign(this)
design.patchProviders(queryProfileProviders())
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
is NewProfileDesign.Request.Create -> {
withProfile {
val name = getString(R.string.new_profile)
val uuid: UUID? = when (val p = it.provider) {
is ProfileProvider.File ->
create(Profile.Type.File, name)
is ProfileProvider.Url ->
create(Profile.Type.Url, name)
is ProfileProvider.External -> {
val data = p.get()
if (data != null) {
val (uri, initialName) = data
create(
Profile.Type.External,
initialName ?: name,
uri.toString()
)
} else {
null
}
}
}
if (uuid != null)
launchProperties(uuid)
}
}
is NewProfileDesign.Request.OpenDetail -> {
launchAppDetailed(it.provider)
}
}
}
}
}
}
private fun launchAppDetailed(provider: ProfileProvider.External) {
val data = Uri.fromParts(
"package",
provider.intent.component?.packageName ?: return,
null
)
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(data))
}
private suspend fun launchProperties(uuid: UUID) {
Log.i("launchProperties",uuid.toString())
val r = startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
PropertiesActivity::class.intent.setUUID(uuid)
)
if (r.resultCode == Activity.RESULT_OK)
finish()
}
private suspend fun ProfileProvider.External.get(): Pair<Uri, String?>? {
val result = startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
intent
)
if (result.resultCode != RESULT_OK)
return null
val uri = result.data?.data
val name = result.data?.getStringExtra(Intents.EXTRA_NAME)
if (uri != null) {
return uri to name
}
return null
}
private suspend fun queryProfileProviders(): List<ProfileProvider> {
return withContext(Dispatchers.IO) {
val providers = packageManager.queryIntentActivities(
Intent(Intents.ACTION_PROVIDE_URL),
0
).map {
val activity = it.activityInfo
val name = activity.applicationInfo.loadLabel(packageManager)
val summary = activity.loadLabel(packageManager)
val icon = activity.loadIcon(packageManager)
val intent = Intent(Intents.ACTION_PROVIDE_URL)
.setComponent(
ComponentName(
activity.packageName,
activity.name
)
)
ProfileProvider.External(name.toString(), summary.toString(), icon, intent)
}
listOf(ProfileProvider.File(self), ProfileProvider.Url(self)) + providers
}
}
}
@@ -0,0 +1,58 @@
package com.github.kr328.clash
import android.content.pm.PackageManager
import com.github.kr328.clash.common.compat.getDrawableCompat
import com.github.kr328.clash.common.constants.Metadata
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.design.OverrideSettingsDesign
import com.github.kr328.clash.design.model.AppInfo
import com.github.kr328.clash.design.util.toAppInfo
import com.github.kr328.clash.service.store.ServiceStore
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
class OverrideSettingsActivity : BaseActivity<OverrideSettingsDesign>() {
override suspend fun main() {
val configuration = withClash { queryOverride(Clash.OverrideSlot.Persist) }
val service = ServiceStore(this)
defer {
withClash {
patchOverride(Clash.OverrideSlot.Persist, configuration)
}
}
val design = OverrideSettingsDesign(
this,
configuration
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
OverrideSettingsDesign.Request.ResetOverride -> {
if (design.requestResetConfirm()) {
defer {
withClash {
clearOverride(Clash.OverrideSlot.Persist)
}
}
finish()
}
}
}
}
}
}
}
}
@@ -0,0 +1,48 @@
package com.github.kr328.clash
import android.content.Intent
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.BaseListActivity
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.adapter.PlanDataAdapter
import com.github.kr328.clash.design.view.ActivityBarLayout
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.PlanData
import com.github.kr328.clash.network.safeApiRequestCall
class PlansActivity : BaseListActivity<PlanData>() {
override fun createAdapter(): RecyclerView.Adapter<*> {
return PlanDataAdapter(){ planData ->
// 在这里处理 item 点击事件
val intent = Intent(this, ConfigOrderActivity::class.java)
intent.putExtra("planData", planData) // 使用 Serializable 传递对象
startActivity(intent)
}
}
override fun onCreate() {
//
setRightButtonVisible {
startActivity(MyOrdersActivity::class.intent)
}
}
override suspend fun loadData(page: Int, callback: (List<PlanData>?, Throwable?) -> Unit) {
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.getplans(PreferenceManager.loginauthData)}.let {
if (it != null && it.isSuccessful) {
callback(it.body()?.data,null)
}else{
callback(null, Throwable("请求失败"))
}
}
}
}
@@ -0,0 +1,133 @@
package com.github.kr328.clash
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.PersistableBundle
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.core.bridge.Bridge
import com.github.kr328.clash.design.MainDesign
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.ProfileDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.showCustomDialog
import com.github.kr328.clash.util.stopClashService
import com.github.kr328.clash.util.withProfile
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
class ProfileActivity : BaseActivity<ProfileDesign>() {
private suspend fun queryAppVersionName(): String {
return withContext(Dispatchers.IO) {
packageManager.getPackageInfo(packageName, 0).versionName //+ "\n" + Bridge.nativeCoreVersion().replace("_", "-")
}
}
override suspend fun main() {
PreferenceManager.init(this)
val design = ProfileDesign(this)
setContentDesign(design)
design.requestdataDisible()
val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
design.queryVersion(queryAppVersionName())
while (isActive) {
select<Unit> {
events.onReceive {
println("events : " + it.name)
}
design.requests.onReceive {
println("requests : " + it.name)
when (it) {
ProfileDesign.Request.OpenBuyPlan -> startActivity(PlansActivity::class.intent)
ProfileDesign.Request.OpenMyOrders -> startActivity(MyOrdersActivity::class.intent)
ProfileDesign.Request.OpenMyBlance -> startActivity(SubmitOrderActivity::class.intent)
ProfileDesign.Request.OpenMyInvites ->startActivity(InvitationActivity::class.intent)
ProfileDesign.Request.OpenCustomView ->{ startActivity(H5WebActivity::class.intent) }
/*try {
withContext(Dispatchers.Main) {
val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setData(Uri.parse(PreferenceManager.getConfigFromPreferences(this@ProfileActivity)?.telegramurl))
startActivity(intent)
}
} catch (e: Exception) {
design.showToast("当前手机未安装浏览器",ToastDuration.Long)
} */
ProfileDesign.Request.OpenLogout -> {
showCustomDialog(
title = "提示",
message = "确定要退出吗?",
positiveButtonText = "确定",
negativeButtonText = "取消",
onPositiveClick = {
LoadingDialog.show(this@ProfileActivity, "正在退出...")
// 执行确认操作
stopClashService()
// 清理缓存,清除节点信息,切换界面
launch {
withProfile {
var allProfiles = queryAll()
println(allProfiles)
if (allProfiles.isNotEmpty() && allProfiles.count() > 0) {
allProfiles.forEach {
delete(it.uuid)
println("删除节点:${it.source}")
}
//删除所有节点
}
}
}
PreferenceManager.clearData()
//2s 后执行
// 在协程作用域中执行
GlobalScope.launch(Dispatchers.Main) {
delay(2000) // 协程延迟2000毫秒(2秒)
println("协程中的延迟执行")
LoadingDialog.hide()
startActivity(LoginActivity::class.intent)
finish()
}
},
onNegativeClick = {
// 执行取消操作
}
)
}
ProfileDesign.Request.OpenGSettings -> {
//查看是否有更新
design.showToast("当前版本是最新版本",ToastDuration.Long)
}// startActivity(NetworkSettingsActivity::class.intent)
ProfileDesign.Request.OpenMyGongdan ->startActivity(GongdanActivity::class.intent)
ProfileDesign.Request.OpenCustomViewIPtest -> startActivity(H5WebActivity::class.intent.putExtra("url","https://ipcelou.com"))
ProfileDesign.Request.OpenCustomViewSpeed -> startActivity(H5WebActivity::class.intent.putExtra("url","https://fast.com/"))
}
}
}
}
}
}
@@ -0,0 +1,127 @@
package com.github.kr328.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.ProfilesDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.R
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.*
import java.util.concurrent.TimeUnit
class ProfilesActivity : BaseActivity<ProfilesDesign>() {
override suspend fun main() {
val design = ProfilesDesign(this)
setContentDesign(design)
val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart, Event.ProfileChanged -> {
design.fetch()
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
ProfilesDesign.Request.Create ->
startActivity(NewProfileActivity::class.intent)
ProfilesDesign.Request.UpdateAll ->
withProfile {
try {
queryAll().forEach { p ->
if (p.imported && p.type != Profile.Type.File)
update(p.uuid)
}
}
finally {
withContext(Dispatchers.Main) {
design.finishUpdateAll();
}
}
}
is ProfilesDesign.Request.Update ->
withProfile { update(it.profile.uuid) }
is ProfilesDesign.Request.Delete ->
withProfile { delete(it.profile.uuid) }
is ProfilesDesign.Request.Edit ->
startActivity(PropertiesActivity::class.intent.setUUID(it.profile.uuid))
is ProfilesDesign.Request.Active -> {
withProfile {
if (it.profile.imported)
setActive(it.profile)
else
design.requestSave(it.profile)
}
}
is ProfilesDesign.Request.Duplicate -> {
val uuid = withProfile { clone(it.profile.uuid) }
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
}
}
}
if (activityStarted) {
ticker.onReceive {
design.updateElapsed()
}
}
}
}
}
private suspend fun ProfilesDesign.fetch() {
withProfile {
patchProfiles(queryAll())
}
}
override fun onProfileUpdateCompleted(uuid: UUID?) {
if(uuid == null)
return;
launch {
var name: String? = null;
withProfile {
name = queryByUUID(uuid)?.name
}
design?.showToast(
getString(R.string.toast_profile_updated_complete, name),
ToastDuration.Long
)
}
}
override fun onProfileUpdateFailed(uuid: UUID?, reason: String?) {
if(uuid == null)
return;
launch {
var name: String? = null;
withProfile {
name = queryByUUID(uuid)?.name
}
design?.showToast(
getString(R.string.toast_profile_updated_failed, name, reason),
ToastDuration.Long
){
setAction(R.string.edit) {
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
}
}
}
}
}
@@ -0,0 +1,117 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.uuid
import com.github.kr328.clash.design.PropertiesDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
class PropertiesActivity : BaseActivity<PropertiesDesign>() {
private var canceled: Boolean = false
override suspend fun main() {
setResult(RESULT_CANCELED)
val uuid = intent.uuid ?: return finish()
val design = PropertiesDesign(this)
val original = withProfile { queryByUUID(uuid) } ?: return finish()
design.profile = original
setContentDesign(design)
defer {
canceled = true
withProfile { release(uuid) }
}
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStop -> {
val profile = design.profile
if (!canceled && profile != original) {
withProfile {
patch(profile.uuid, profile.name, profile.source, profile.interval)
}
}
}
Event.ServiceRecreated -> {
finish()
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
PropertiesDesign.Request.BrowseFiles -> {
startActivity(FilesActivity::class.intent.setUUID(uuid))
}
PropertiesDesign.Request.Commit -> {
design.verifyAndCommit()
}
}
}
}
}
}
override fun onBackPressed() {
design?.apply {
launch {
if (!progressing) {
if (requestExitWithoutSaving())
finish()
}
}
} ?: return super.onBackPressed()
}
private suspend fun PropertiesDesign.verifyAndCommit() {
when {
profile.name.isBlank() -> {
showToast(R.string.empty_name, ToastDuration.Long)
}
profile.type != Profile.Type.File && profile.source.isBlank() -> {
showToast(R.string.invalid_url, ToastDuration.Long)
}
else -> {
try {
withProcessing { updateStatus ->
withProfile {
patch(profile.uuid, profile.name, profile.source, profile.interval)
coroutineScope {
commit(profile.uuid) {
launch {
withProfile {
setActive(queryAll().first())
}
updateStatus(it)
}
}
}
}
}
setResult(RESULT_OK)
finish()
} catch (e: Exception) {
showExceptionToast(e)
}
}
}
}
}
@@ -0,0 +1,71 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.ProvidersDesign
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import java.util.concurrent.TimeUnit
class ProvidersActivity : BaseActivity<ProvidersDesign>() {
override suspend fun main() {
val providers = withClash { queryProviders().sorted() }
val design = ProvidersDesign(this, providers)
setContentDesign(design)
val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ProfileLoaded -> {
val newList = withClash { queryProviders().sorted() }
if (newList != providers) {
startActivity(ProvidersActivity::class.intent)
finish()
}
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
is ProvidersDesign.Request.Update -> {
launch {
try {
withClash {
updateProvider(it.provider.type, it.provider.name)
}
design.notifyChanged(it.index)
} catch (e: Exception) {
design.showExceptionToast(
getString(
R.string.format_update_provider_failure,
it.provider.name,
e.message
)
)
design.notifyUpdated(it.index)
}
}
}
}
}
if (activityStarted) {
ticker.onReceive {
design.updateElapsed()
}
}
}
}
}
}
@@ -0,0 +1,191 @@
package com.github.kr328.clash
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.model.Proxy
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.ProxyDesign
import com.github.kr328.clash.design.model.ProxyState
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
class ProxyActivity : BaseActivity<ProxyDesign>() {
override suspend fun main() {
val mode = withClash { queryOverride(Clash.OverrideSlot.Session).mode }
val names = withClash { queryProxyGroupNames(uiStore.proxyExcludeNotSelectable) }
val states = List(names.size) { ProxyState("?") }
val unorderedStates = names.indices.map { names[it] to states[it] }.toMap()
val reloadLock = Semaphore(10)
setTitle("选择节点")
val design = ProxyDesign(
this,
mode,
names,
uiStore
)
setContentDesign(design)
//刷新所以Groups
//design.requests.send(ProxyDesign.Request.ReloadAll)
design.requests.send(ProxyDesign.Request.Reload(0))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ProfileLoaded -> {
val newNames = withClash {
queryProxyGroupNames(uiStore.proxyExcludeNotSelectable)
}
if (newNames != names) {
startActivity(ProxyActivity::class.intent)
finish()
}
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
ProxyDesign.Request.ReLaunch -> {
startActivity(ProxyActivity::class.intent)
finish()
}
ProxyDesign.Request.ReloadAll -> {
names.indices.forEach { idx ->
design.requests.trySend(ProxyDesign.Request.Reload(idx))
}
}
is ProxyDesign.Request.Reload -> {
launch {
design.urlTesting = false
val group = reloadLock.withPermit {
withClash {
queryProxyGroup(names[it.index], uiStore.proxySort)
}
}
var state = states[it.index]
println(">>>>>>>>>>>> ${names}")
println(">>>>>>>>>>>> ${it.index} group: "+group + " ${group.type.name} ${group.proxies.size}")
if (group.proxies.first().name == "GLOBAL"){
println(">>>>>>>>>>>> GLOBAL ")
}
if (PreferenceManager.selectnodeName.length > 0){
state.now = PreferenceManager.selectnodeName //group.now
}else{
state.now = group.now
}
println(">>>>>>>>>>>>unorderedStates : ${unorderedStates}")
println(">>>>>>>>>>>> state: "+state)
//PreferenceManager.selectnodeName = state.now //刷新目前使用的节点名称
// >>>>>>>>>>>> state: ProxyState(now=?)
// >>>>>>>>>>>> 0 group: 距离下次重置剩余:18 天 Selector
// >>>>>>>>>>>>{代理=ProxyState(now=距离下次重置剩余:18 天), 电报=ProxyState(now=?), 油管=ProxyState(now=?), 奈飞=ProxyState(now=?), b站=ProxyState(now=?), 规则外路由选择=ProxyState(now=?)}
design.updateGroup(
it.index,
group.proxies,
group.type == Proxy.Type.Selector,
state,
unorderedStates
)
design.finishreferesh()
/*val group = reloadLock.withPermit {
withClash {
queryProxyGroup(names[it.index], uiStore.proxySort)
}
}
val state = states[it.index]
state.now = group.now
design.updateGroup(
it.index,
group.proxies,
group.type == Proxy.Type.Selector,
state,
unorderedStates
) */
}
}
is ProxyDesign.Request.Select -> {
withClash {
patchSelector(names[it.index], it.name)
states[it.index].now = it.name
}
PreferenceManager.selectnodeName = it.name
design.requestRedrawVisible()
launch {
finish()
}
}
is ProxyDesign.Request.UrlTest -> {
launch {
withClash {
healthCheck(names[it.index])
}
design.requests.send(ProxyDesign.Request.Reload(it.index))
}
}
is ProxyDesign.Request.PatchMode -> {
// design.showModeSwitchTips()
withClash {
val o = queryOverride(Clash.OverrideSlot.Session)
o.mode = it.mode
patchOverride(Clash.OverrideSlot.Session, o)
}
}
}
}
}
}
// 使用协程延迟 500ms 后执行
/* GlobalScope.launch(Dispatchers.Main) {
delay(1500) // 延迟 500ms
launch {
withClash {
healthCheck(names[0])
}
design.requests.send(ProxyDesign.Request.Reload(0))
}
}*/
}
}
@@ -0,0 +1,217 @@
package com.github.kr328.clash
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import com.github.kr328.clash.common.util.intent
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.databinding.ActivityLoginBinding
import com.github.kr328.clash.design.databinding.ActivityProfileBinding
import com.github.kr328.clash.design.databinding.ActivityRegisterBinding
import com.github.kr328.clash.design.ui.DayNight
import com.github.kr328.clash.design.util.layoutInflater
import com.github.kr328.clash.design.util.resolveThemedBoolean
import com.github.kr328.clash.design.util.resolveThemedColor
import com.github.kr328.clash.design.util.root
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiClientConfig
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.ConfigResponse
import com.github.kr328.clash.network.LoginRequest
import com.github.kr328.clash.network.LoginResponse
import com.github.kr328.clash.network.safeApiRequestCall
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
class RegisterActivity : AppCompatActivity() {
private lateinit var apiService: ApiService
private var dayNight: DayNight = DayNight.Day
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityRegisterBinding
.inflate(this.layoutInflater, this.root, false)
setContentView(binding.root)
applyDayNight()
PreferenceManager.init(this)
binding.loginLoginButton.setOnClickListener {
val email = binding.loginEmailEditText.text.toString()
val password = binding.loginPasswordEditText.text.toString()
val password2 = binding.loginPasswordEditText2.text.toString()
val isAgreementChecked = binding.loginAgreementCheckBox.isChecked
if (email.isEmpty() || password.isEmpty() || password2.isEmpty()) {
Toast.makeText(this, "请输入邮箱和密码", Toast.LENGTH_SHORT).show()
} else if (!isAgreementChecked) {
Toast.makeText(this, "请同意隐私政策和用户协议", Toast.LENGTH_SHORT).show()
} else {
// Hide the keyboard
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val currentFocusView = currentFocus
if (currentFocusView != null) {
inputMethodManager.hideSoftInputFromWindow(currentFocusView.windowToken, 0)
}
performLogin(email, password)
}
}
binding.loginForgotPassword.setOnClickListener {
// Navigate to Forgot Password screen or show dialog
}
binding.loginRegister.setOnClickListener {
// Navigate to Register screen
finish()
}
apiService = ApiClientConfig.retrofit.create(ApiService::class.java)
//获取 Config 数据
CoroutineScope(Dispatchers.IO).launch {
safeApiRequestCall { apiService.getConfig()}.let {
if (it != null && it.isSuccessful) {
val response: ConfigResponse = it.body() ?: throw Exception("Response body is null")
if (response.code == 1 ) {
PreferenceManager.saveConfigToPreferences( response)
//重新初始化 ApiClint
}
println("API Response: ${it.body()}")
} else {
println("API Error: 请求失败")
}
}
}
}
/**
* withContext(Dispatchers.Main) {
* if (response.isSuccessful) {
* val userData = response.body()
* // 更新 UI
* } else {
* Toast.makeText(this@MainActivity, "Failed to fetch user data", Toast.LENGTH_SHORT).show()
* }
* }
* */
private fun applyDayNight(config: Configuration = resources.configuration) {
//val dayNight = theme.applyStyle(R.style.AppThemeLight, true) //默认白天模式
/*
val dayNight = queryDayNight(config)
when (dayNight) {
DayNight.Night -> theme.applyStyle(R.style.AppThemeDark, true)
DayNight.Day -> theme.applyStyle(R.style.AppThemeLight, true)
}*/
window.isAllowForceDarkCompat = false
window.isSystemBarsTranslucentCompat = true
window.statusBarColor = resolveThemedColor(android.R.attr.statusBarColor)
window.navigationBarColor = resolveThemedColor(android.R.attr.navigationBarColor)
if (Build.VERSION.SDK_INT >= 23) {
window.isLightStatusBarsCompat = resolveThemedBoolean(android.R.attr.windowLightStatusBar)
}
if (Build.VERSION.SDK_INT >= 27) {
window.isLightNavigationBarCompat = resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
}
this.dayNight = DayNight.Night //dayNight
}
private fun performLogin(email: String, password: String) {
// Perform login logic here, possibly calling an API
// Show the loading indicator with a custom message
LoadingDialog.show(this, "正在注册...")
val apiServiceApp = ApiClient.retrofit.create(ApiService::class.java)
//获取 Config 数据
CoroutineScope(Dispatchers.IO).launch {
safeApiRequestCall {
apiServiceApp.registerUser(LoginRequest(email,password))}.let {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
if (it != null &&
it.isSuccessful) {
println("API Response: ${it.code()} ${it.body()} ${it.raw().body}")
val response: LoginResponse = it.body() ?: throw Exception("Response body is null")
if (response.data != null){
PreferenceManager.loginemail = email
PreferenceManager.loginToken = response.data?.token ?: ""
PreferenceManager.loginauthData = response.data?.auth_data ?: ""
PreferenceManager.isLoginin = true
// saveLoginMailToken(this@LoginActivity,email)
// saveLoginToken(this@LoginActivity, response.data.token ?: "")
// saveLoginAuthData(this@LoginActivity, response.data.auth_data ?: "")
// saveLoginStatus(this@LoginActivity,true)
//获取用户信息
withContext(Dispatchers.Main) {
val intent = Intent(this@RegisterActivity, MainActivity::class.java)
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish() // It
}
}
}else{
// Handle error response, even if it's 422
try {
val errorResponse = it?.errorBody()?.string() ?: ""
val errorJson = JSONObject(errorResponse)
val message = errorJson.optString("message")
// Process the error message or show it to the user
if (!message.isNullOrEmpty()) {
withContext(Dispatchers.Main) {
Toast.makeText(this@RegisterActivity, "注册失败:${message}", Toast.LENGTH_LONG).show()
}
}
println( "Message: $message, ")
} catch (e: JSONException) {
// Handle JSON parsing error
withContext(Dispatchers.Main) {
Toast.makeText(this@RegisterActivity, "注册失败:${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
}
}
}
@@ -0,0 +1,18 @@
package com.github.kr328.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.service.StatusProvider
import com.github.kr328.clash.util.startClashService
class RestartReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
if (StatusProvider.shouldStartClashOnBoot)
context.startClashService()
}
}
}
}
@@ -0,0 +1,34 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.SettingsDesign
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class SettingsActivity : BaseActivity<SettingsDesign>() {
override suspend fun main() {
val design = SettingsDesign(this)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
SettingsDesign.Request.StartApp ->
startActivity(AppSettingsActivity::class.intent)
SettingsDesign.Request.StartNetwork ->
startActivity(NetworkSettingsActivity::class.intent)
SettingsDesign.Request.StartOverride ->
startActivity(OverrideSettingsActivity::class.intent)
SettingsDesign.Request.StartMetaFeature ->
startActivity(MetaFeatureSettingsActivity::class.intent)
}
}
}
}
}
}
@@ -0,0 +1,36 @@
package com.github.kr328.clash
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.databinding.ActivityLoginBinding
import com.github.kr328.clash.design.databinding.ActivitySplashBinding
import com.github.kr328.clash.design.util.root
import com.github.kr328.clash.network.ApiService
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivitySplashBinding
.inflate(this.layoutInflater, this.root, false)
setContentView(binding.root)
PreferenceManager.init(this)
Handler(Looper.getMainLooper()).postDelayed({
// Hide the loading indicator
startActivity(LoginActivity::class.intent)
finish()
}, 1000) // Simulating a network delay
}
}
@@ -0,0 +1,16 @@
package com.github.kr328.clash
import com.github.kr328.clash.design.SubmitOrderViewDesign
class SubmitOrderActivity : BaseActivity<SubmitOrderViewDesign>() {
override suspend fun main() {
val design = SubmitOrderViewDesign(this)
setContentDesign(design)
//
val trade_no = intent.getStringExtra("trade_no")
design.fetchTradeInfo(trade_no)
}
}
@@ -0,0 +1,258 @@
package com.github.kr328.clash
import android.graphics.Color
import android.graphics.Rect
import android.provider.Settings.Global
import android.view.Gravity
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.design.BaseListActivity
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.adapter.TicketDetailAdapter
import com.github.kr328.clash.design.adapter.TicketsDataAdapter
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.PublicResponse
import com.github.kr328.clash.network.TicketMessage
import com.github.kr328.clash.network.safeApiRequestCall
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLEncoder
class TicketDetailActivity : BaseListActivity<TicketMessage>() {
override fun createAdapter(): RecyclerView.Adapter<*> {
return TicketDetailAdapter(){ planData ->
// 在这里处理 item 点击事件
}
}
override fun onCreate() {
}
fun encodeString(input: String): String {
val encoded = StringBuilder()
input.forEach { char ->
when {
char.isWhitespace() -> {
// 编码空格为 %20
encoded.append("")
}
char.isLetterOrDigit() || char.toInt() in 0x4E00..0x9FFF -> {
// 保留字母、数字和中文字符
encoded.append(char)
}
else -> {
// 其他字符使用 URLEncoder 编码
encoded.append(URLEncoder.encode(char.toString(), "UTF-8"))
}
}
}
return encoded.toString()
}
fun onCreateInputView() {
// 将 dp 转换为 px 的实用函数
fun dpToPx(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}
// 创建顶部分割线
val topDivider = View(this).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
dpToPx(1) // 设置分割线高度为 1dp
) .apply {
gravity = Gravity.TOP // 将分割线置于顶部
}
setBackgroundColor(ContextCompat.getColor(this@TicketDetailActivity, android.R.color.darker_gray)) // 设置分割线颜色
}
val rootLayout = FrameLayout(this).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
addView(topDivider) // 将分割线添加到根布局
}
// Create the input container layout programmatically
// Create the input container layout programmatically
val inputContainer = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(8, 8, 8, 8)
// setBackgroundColor(ContextCompat.getColor(this@TicketDetailActivity, android.R.color.white))
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.BOTTOM // Aligns the input container to the bottom
)
setBackgroundColor(ContextCompat.getColor(this@TicketDetailActivity, R.color.white))
}
// Create the EditText for message input
val editText = EditText(this).apply {
hint = "输入内容回复工单..."
setHintTextColor(ContextCompat.getColor(this@TicketDetailActivity, R.color.gray)) // Set the placeholder color
// Utility function to convert dp to pixels
fun dpToPx2(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}
textSize = 14f
// Set other EditText properties
setTextColor(Color.BLACK) // Text color
inputType = android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE
setTextColor(ContextCompat.getColor(this@TicketDetailActivity, R.color.black))
maxLines = 1
// background = ContextCompat.getDrawable(this@TicketDetailActivity, R.drawable.rounded_button_background)
// 设置内边距为 4dp(上下左右)
// val padding = dpToPx(8)
// setPadding(padding,0 , padding, 0)
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f // Weight to make it fill available width
)
}
// Create the Send Button
val sendButton = Button(this).apply {
text = "回复"
setTextColor(ContextCompat.getColor(this@TicketDetailActivity, android.R.color.white)) // Set text color for better contrast
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
}
// Set the background tint programmatically
ViewCompat.setBackgroundTintList(
sendButton,
ContextCompat.getColorStateList(this, R.color.blue)
)
// Add EditText and Button to the input container
inputContainer.addView(editText)
inputContainer.addView(sendButton)
sendButton.setOnClickListener {
val message = editText.text.toString()
if (message.isNullOrEmpty()){
}else{
val fieldMap = mutableMapOf<String, String>()
val messageProperty1 = encodeString(message)
val ticketid = intent.getIntExtra("ticketid",0)
fieldMap.put("message",""+messageProperty1)
fieldMap.put("id",""+ticketid)
GlobalScope.launch {
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.replyTicket(PreferenceManager.loginauthData, fieldMap)}.let {
if (it != null && it.isSuccessful) {
withContext(Dispatchers.Main) {
Toast.makeText(this@TicketDetailActivity, "回复成功", Toast.LENGTH_SHORT).show()
editText.setText("")
}
refereshData()
}else{
val errorinfo = it?.errorBody()?.string()
if (errorinfo != null){
//解析错错误信息
val gson = Gson()
val submitResponse = gson.fromJson(errorinfo, PublicResponse::class.java)
if (submitResponse?.message != null){
withContext(Dispatchers.Main) {
Toast.makeText(this@TicketDetailActivity, "回复失败: ${submitResponse.message}", Toast.LENGTH_SHORT).show()
}
}
}else{
withContext(Dispatchers.Main) {
Toast.makeText(this@TicketDetailActivity, "回复失败", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
}
// 将分割线添加到输入容器顶部
rootLayout.addView(inputContainer)
// rootLayout.addView(topDivider) // 将分割线添加到根布局
// Add the input container to the root layout
mainBinding?.mainview?.addView(rootLayout)
rootLayout.viewTreeObserver.addOnGlobalLayoutListener {
val rect = Rect()
rootLayout.getWindowVisibleDisplayFrame(rect)
val screenHeight = rootLayout.rootView.height
val keypadHeight = screenHeight - rect.bottom
val params = inputContainer.layoutParams as FrameLayout.LayoutParams
if (keypadHeight > screenHeight * 0.15) {
// Keyboard is visible; adjust inputContainer to stay above it
params.bottomMargin = keypadHeight
} else {
// Keyboard is hidden; reset inputContainer to bottom
params.bottomMargin = 0
}
inputContainer.layoutParams = params
}
}
override suspend fun loadData(page: Int, callback: (List<TicketMessage>?, Throwable?) -> Unit) {
val ticketid = intent.getIntExtra("ticketid",0)
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.getTicketsByTID(PreferenceManager.loginauthData,ticketid)}.let {
if (it != null && it.isSuccessful) {
withContext(Dispatchers.Main) {
referTitle(it.body()?.data?.subject ?: "")
}
if(it.body()?.data?.status == 0){
withContext(Dispatchers.Main) {
onCreateInputView()
}
}
callback(it.body()?.data?.message,null)
}else{
callback(null, Throwable("请求失败"))
}
}
}
}
@@ -0,0 +1,104 @@
package com.github.kr328.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.constants.Permissions
import com.github.kr328.clash.remote.StatusClient
import com.github.kr328.clash.util.startClashService
import com.github.kr328.clash.util.stopClashService
@RequiresApi(Build.VERSION_CODES.N)
class TileService : TileService() {
private var currentProfile = ""
private var clashRunning = false
override fun onClick() {
val tile = qsTile ?: return
when (tile.state) {
Tile.STATE_INACTIVE -> {
startClashService()
}
Tile.STATE_ACTIVE -> {
stopClashService()
}
}
}
override fun onStartListening() {
super.onStartListening()
registerReceiver(
receiver,
IntentFilter().apply {
addAction(Intents.ACTION_CLASH_STARTED)
addAction(Intents.ACTION_CLASH_STOPPED)
addAction(Intents.ACTION_PROFILE_LOADED)
addAction(Intents.ACTION_SERVICE_RECREATED)
},
Permissions.RECEIVE_SELF_BROADCASTS,
null
)
val name = StatusClient(this).currentProfile()
clashRunning = name != null
currentProfile = name ?: ""
updateTile()
}
override fun onStopListening() {
super.onStopListening()
unregisterReceiver(receiver)
}
private fun updateTile() {
val tile = qsTile ?: return
tile.state = if (clashRunning)
Tile.STATE_ACTIVE
else
Tile.STATE_INACTIVE
tile.label = if (currentProfile.isEmpty())
getText(R.string.launch_name)
else
currentProfile
tile.icon = Icon.createWithResource(this, R.drawable.ic_logo_service)
tile.updateTile()
}
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intents.ACTION_CLASH_STARTED -> {
clashRunning = true
currentProfile = ""
}
Intents.ACTION_CLASH_STOPPED, Intents.ACTION_SERVICE_RECREATED -> {
clashRunning = false
currentProfile = ""
}
Intents.ACTION_PROFILE_LOADED -> {
currentProfile = StatusClient(this@TileService).currentProfile() ?: ""
}
}
updateTile()
}
}
}
@@ -0,0 +1,52 @@
package com.github.kr328.clash.log
import androidx.collection.CircularArray
import com.github.kr328.clash.core.model.LogMessage
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class LogcatCache {
data class Snapshot(val messages: List<LogMessage>, val removed: Int, val appended: Int)
private val array = CircularArray<LogMessage>(CAPACITY)
private val lock = Mutex()
private var removed: Int = 0
private var appended: Int = 0
suspend fun append(msg: LogMessage) {
lock.withLock {
if (array.size() >= CAPACITY) {
array.removeFromStart(1)
removed++
appended--
}
array.addLast(msg)
appended++
}
}
suspend fun snapshot(full: Boolean): Snapshot? {
return lock.withLock {
if (!full && removed == 0 && appended == 0) {
return@withLock null
}
Snapshot(
List(array.size()) { array[it] },
removed,
if (full) array.size() + appended else appended
).also {
removed = 0
appended = 0
}
}
}
companion object {
const val CAPACITY = 128
}
}
@@ -0,0 +1,25 @@
package com.github.kr328.clash.log
import android.content.Context
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.util.format
import java.io.BufferedWriter
import java.io.Writer
import java.util.*
class LogcatFilter(output: Writer, private val context: Context) : BufferedWriter(output) {
fun writeHeader(time: Date) {
appendLine("# Capture on ${time.format(context)}")
}
fun writeMessage(message: LogMessage) {
val time = message.time.format(context, includeDate = false)
val level = message.level.name
appendLine(FORMAT.format(time, level, message.message))
}
companion object {
private const val FORMAT = "%12s %7s: %s"
}
}
@@ -0,0 +1,44 @@
package com.github.kr328.clash.log
import android.content.Context
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.util.logsDir
import java.io.BufferedReader
import java.io.FileReader
import java.util.*
class LogcatReader(context: Context, file: LogFile) : AutoCloseable {
private val reader = BufferedReader(FileReader(context.logsDir.resolve(file.fileName)))
override fun close() {
reader.close()
}
fun readAll(): List<LogMessage> {
var lastTime = Date(0)
return reader.lineSequence()
.map { it.trim() }
.filter { !it.startsWith("#") }
.map { it.split(":", limit = 3) }
.map {
val time = it[0].toLongOrNull()?.let { Date(it) } ?: lastTime
val logMessage = if (it[0].toLongOrNull() != null) {
LogMessage(
time = time,
level = LogMessage.Level.valueOf(it[1]),
message = it[2]
)
} else {
LogMessage(
time = time,
level = LogMessage.Level.Warning, // or any default level
message = it.joinToString(":")
)
}
lastTime = time
logMessage
}
.toList()
}
}
@@ -0,0 +1,25 @@
package com.github.kr328.clash.log
import android.content.Context
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.util.logsDir
import java.io.BufferedWriter
import java.io.FileWriter
class LogcatWriter(context: Context) : AutoCloseable {
private val file = LogFile.generate()
private val writer = BufferedWriter(FileWriter(context.logsDir.resolve(file.fileName)))
override fun close() {
writer.close()
}
fun appendMessage(message: LogMessage) {
writer.appendLine(FORMAT.format(message.time.time, message.level.name, message.message))
}
companion object {
private const val FORMAT = "%d:%s:%s"
}
}
@@ -0,0 +1,32 @@
package com.github.kr328.clash.log
object SystemLogcat {
private val command = arrayOf(
"logcat",
"-d",
"-s",
"Go",
"DEBUG",
"AndroidRuntime",
"UUVPN",
"LwIP",
)
fun dumpCrash(): String {
return try {
val process = Runtime.getRuntime().exec(command)
val result = process.inputStream.use { stream ->
stream.reader().readLines()
.filterNot { it.startsWith("------") }
.joinToString("\n")
}
process.waitFor()
result.trim()
} catch (e: Exception) {
""
}
}
}
@@ -0,0 +1,130 @@
package com.github.kr328.clash.remote
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import androidx.core.content.ContextCompat
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.log.Log
import java.util.*
class Broadcasts(private val context: Application) {
interface Observer {
fun onServiceRecreated()
fun onStarted()
fun onStopped(cause: String?)
fun onProfileChanged()
fun onProfileUpdateCompleted(uuid: UUID?)
fun onProfileUpdateFailed(uuid: UUID?, reason: String?)
fun onProfileLoaded()
}
var clashRunning: Boolean = false
private var registered = false
private val receivers = mutableListOf<Observer>()
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.`package` != context?.packageName)
return
when (intent?.action) {
Intents.ACTION_SERVICE_RECREATED -> {
clashRunning = false
receivers.forEach {
it.onServiceRecreated()
}
}
Intents.ACTION_CLASH_STARTED -> {
clashRunning = true
receivers.forEach {
it.onStarted()
}
}
Intents.ACTION_CLASH_STOPPED -> {
clashRunning = false
receivers.forEach {
it.onStopped(intent.getStringExtra(Intents.EXTRA_STOP_REASON))
}
}
Intents.ACTION_PROFILE_CHANGED ->
receivers.forEach {
it.onProfileChanged()
}
Intents.ACTION_PROFILE_UPDATE_COMPLETED ->
receivers.forEach {
it.onProfileUpdateCompleted(
UUID.fromString(intent.getStringExtra(Intents.EXTRA_UUID)))
}
Intents.ACTION_PROFILE_UPDATE_FAILED ->
receivers.forEach {
it.onProfileUpdateFailed(
UUID.fromString(intent.getStringExtra(Intents.EXTRA_UUID)),
intent.getStringExtra(Intents.EXTRA_FAIL_REASON))
}
Intents.ACTION_PROFILE_LOADED -> {
receivers.forEach {
it.onProfileLoaded()
}
}
}
}
}
fun addObserver(observer: Observer) {
receivers.add(observer)
}
fun removeObserver(observer: Observer) {
receivers.remove(observer)
}
fun register() {
if (registered)
return
try {
val filter = IntentFilter().apply {
addAction(Intents.ACTION_SERVICE_RECREATED)
addAction(Intents.ACTION_CLASH_STARTED)
addAction(Intents.ACTION_CLASH_STOPPED)
addAction(Intents.ACTION_PROFILE_CHANGED)
addAction(Intents.ACTION_PROFILE_UPDATE_COMPLETED)
addAction(Intents.ACTION_PROFILE_UPDATE_FAILED)
addAction(Intents.ACTION_PROFILE_LOADED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(broadcastReceiver, filter, Context.RECEIVER_EXPORTED)
} else {
context.registerReceiver(broadcastReceiver, filter)
}
registered = true
clashRunning = StatusClient(context).currentProfile() != null
} catch (e: Exception) {
Log.w("Register global receiver: $e", e)
}
}
fun unregister() {
if (!registered)
return
try {
context.unregisterReceiver(broadcastReceiver)
registered = false
clashRunning = false
} catch (e: Exception) {
Log.w("Unregister global receiver: $e", e)
}
}
}
@@ -0,0 +1,94 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.github.kr328.clash.remote
import android.content.Context
import android.net.Uri
import com.github.kr328.clash.common.constants.Authorities
import com.github.kr328.clash.design.model.File
import com.github.kr328.clash.util.copyContentTo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import android.provider.DocumentsContract as DC
class FilesClient(private val context: Context) {
suspend fun list(parentDocumentId: String): List<File> = withContext(Dispatchers.IO) {
val uri = DC.buildChildDocumentsUri(Authorities.FILES_PROVIDER, parentDocumentId)
context.contentResolver.query(uri, FilesProjection, null, null, null)?.use { cursor ->
val idIndex = cursor.getColumnIndex(DC.Document.COLUMN_DOCUMENT_ID)
val nameIndex = cursor.getColumnIndex(DC.Document.COLUMN_DISPLAY_NAME)
val sizeIndex = cursor.getColumnIndex(DC.Document.COLUMN_SIZE)
val lastModified = cursor.getColumnIndex(DC.Document.COLUMN_LAST_MODIFIED)
val mimeTypeIndex = cursor.getColumnIndex(DC.Document.COLUMN_MIME_TYPE)
cursor.moveToFirst()
List(cursor.count) {
File(
id = cursor.getString(idIndex),
name = cursor.getString(nameIndex),
size = cursor.getLong(sizeIndex),
lastModified = cursor.getLong(lastModified),
isDirectory = cursor.getString(mimeTypeIndex) == DC.Document.MIME_TYPE_DIR,
).also {
cursor.moveToNext()
}
}.sortedWith(compareBy({ !it.isDirectory }, { it.name }))
} ?: emptyList()
}
suspend fun renameDocument(documentId: String, name: String) = withContext(Dispatchers.IO) {
val uri = buildDocumentUri(documentId)
DC.renameDocument(context.contentResolver, uri, name)
}
suspend fun deleteDocument(documentId: String) = withContext(Dispatchers.IO) {
val uri = buildDocumentUri(documentId)
DC.deleteDocument(context.contentResolver, uri)
}
suspend fun importDocument(
parentDocumentId: String,
source: Uri,
name: String
) = withContext(Dispatchers.IO) {
val target = buildDocumentUri("$parentDocumentId/$name")
context.contentResolver.copyContentTo(source, target)
}
suspend fun copyDocument(
documentId: String,
source: Uri
) {
val target = buildDocumentUri(documentId)
context.contentResolver.copyContentTo(source, target)
}
suspend fun copyDocument(
target: Uri,
documentId: String
) {
val source = buildDocumentUri(documentId)
context.contentResolver.copyContentTo(source, target)
}
fun buildDocumentUri(documentId: String): Uri {
return DC.buildDocumentUri(Authorities.FILES_PROVIDER, documentId)
}
companion object {
private val FilesProjection = arrayOf(
DC.Document.COLUMN_DOCUMENT_ID,
DC.Document.COLUMN_DISPLAY_NAME,
DC.Document.COLUMN_SIZE,
DC.Document.COLUMN_LAST_MODIFIED,
DC.Document.COLUMN_MIME_TYPE,
)
}
}
@@ -0,0 +1,73 @@
package com.github.kr328.clash.remote
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.AppCrashedActivity
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.store.AppStore
import com.github.kr328.clash.util.ApplicationObserver
import com.github.kr328.clash.util.verifyApk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
object Remote {
val broadcasts: Broadcasts = Broadcasts(Global.application)
val service: Service = Service(Global.application) {
ApplicationObserver.createdActivities.forEach { it.finish() }
val intent = AppCrashedActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Global.application.startActivity(intent)
}
private val visible = Channel<Boolean>(Channel.CONFLATED)
fun launch() {
ApplicationObserver.attach(Global.application)
ApplicationObserver.onVisibleChanged {
if(it) {
Log.d("App becomes visible")
service.bind()
broadcasts.register()
}
else {
Log.d("App becomes invisible")
service.unbind()
broadcasts.unregister()
}
}
Global.launch(Dispatchers.IO) {
verifyApp()
}
}
private suspend fun verifyApp() {
val context = Global.application
val store = AppStore(context)
val updatedAt = getLastUpdated(context)
if (store.updatedAt != updatedAt) {
if (!context.verifyApk()) {
ApplicationObserver.createdActivities.forEach { it.finish() }
val intent = ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return context.startActivity(intent)
} else {
store.updatedAt = updatedAt
}
}
}
private fun getLastUpdated(context: Context): Long {
return context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
}
}
@@ -0,0 +1,74 @@
package com.github.kr328.clash.remote
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
class Resource<T> {
private interface Callback<T> {
fun accept(value: T)
}
private val pending: MutableSet<Callback<T>> = mutableSetOf()
private var value: T? = null
suspend fun get(): T {
return suspendCancellableCoroutine { ctx ->
val callback = object : Callback<T> {
override fun accept(value: T) {
ctx.resume(value)
}
}
ctx.invokeOnCancellation {
cancel(callback)
}
get(callback)
}
}
fun set(v: T?) {
setAndNotify(v)
}
fun reset(v: T) {
resetIfMatched(v)
}
@Synchronized
private fun get(callback: Callback<T>) {
val v = value
if (v == null) {
pending.add(callback)
} else {
callback.accept(v)
}
}
@Synchronized
private fun setAndNotify(value: T?) {
this.value = value
if (value != null) {
pending.forEach {
it.accept(value)
}
pending.clear()
}
}
@Synchronized
private fun resetIfMatched(value: T) {
if (this.value === value) {
this.value = null
}
}
@Synchronized
private fun cancel(callback: Callback<T>) {
pending.remove(callback)
}
}
@@ -0,0 +1,59 @@
package com.github.kr328.clash.remote
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.os.IBinder
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.service.RemoteService
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.remote.unwrap
import com.github.kr328.clash.util.unbindServiceSilent
import java.util.concurrent.TimeUnit
class Service(private val context: Application, val crashed: () -> Unit) {
val remote = Resource<IRemoteService>()
private val connection = object : ServiceConnection {
private var lastCrashed: Long = -1
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
remote.set(service.unwrap(IRemoteService::class))
}
override fun onServiceDisconnected(name: ComponentName?) {
remote.set(null)
if (System.currentTimeMillis() - lastCrashed < TOGGLE_CRASHED_INTERVAL) {
unbind()
crashed()
}
lastCrashed = System.currentTimeMillis()
Log.w("RemoteService killed or crashed")
}
}
fun bind() {
try {
context.bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
unbind()
crashed()
}
}
fun unbind() {
context.unbindServiceSilent(connection)
remote.set(null)
}
companion object {
private val TOGGLE_CRASHED_INTERVAL = TimeUnit.SECONDS.toMillis(10)
}
}
@@ -0,0 +1,34 @@
package com.github.kr328.clash.remote
import android.content.Context
import android.net.Uri
import com.github.kr328.clash.common.constants.Authorities
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.service.StatusProvider
class StatusClient(private val context: Context) {
private val uri: Uri
get() {
return Uri.Builder()
.scheme("content")
.authority(Authorities.STATUS_PROVIDER)
.build()
}
fun currentProfile(): String? {
return try {
val result = context.contentResolver.call(
uri,
StatusProvider.METHOD_CURRENT_PROFILE,
null,
null
)
result?.getString("name")
} catch (e: Exception) {
Log.w("Query current profile: $e", e)
null
}
}
}
@@ -0,0 +1,22 @@
package com.github.kr328.clash.store
import android.content.Context
import com.github.kr328.clash.common.store.Store
import com.github.kr328.clash.common.store.asStoreProvider
class AppStore(context: Context) {
private val store = Store(
context
.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
.asStoreProvider()
)
var updatedAt: Long by store.long(
key = "updated_at",
defaultValue = -1,
)
companion object {
private const val FILE_NAME = "app"
}
}
@@ -0,0 +1,29 @@
package com.github.kr328.clash.store
import android.content.Context
import com.github.kr328.clash.common.store.Store
import com.github.kr328.clash.common.store.asStoreProvider
class TipsStore(context: Context) {
private val store = Store(
context
.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
.asStoreProvider()
)
var requestDonate: Boolean by store.boolean(
key = "request_donate",
defaultValue = true,
)
var primaryVersion: Int by store.int(
key = "primary_version",
defaultValue = -1,
)
companion object {
const val CURRENT_PRIMARY_VERSION = 1
private const val FILE_NAME = "tips"
}
}
@@ -0,0 +1,44 @@
package com.github.kr328.clash.util
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
class ActivityResultLifecycle : LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.currentState = Lifecycle.State.INITIALIZED
}
override fun getLifecycle(): Lifecycle {
return lifecycle
}
suspend fun <T> use(block: suspend (lifecycle: ActivityResultLifecycle, start: () -> Unit) -> T): T {
return try {
markCreated()
block(this, this::markStarted)
} finally {
withContext(NonCancellable) {
markDestroy()
}
}
}
private fun markCreated() {
lifecycle.currentState = Lifecycle.State.CREATED
}
private fun markStarted() {
lifecycle.currentState = Lifecycle.State.STARTED
lifecycle.currentState = Lifecycle.State.RESUMED
}
private fun markDestroy() {
lifecycle.currentState = Lifecycle.State.DESTROYED
}
}
@@ -0,0 +1,85 @@
package com.github.kr328.clash.util
import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Build
import android.os.Bundle
import java.io.File
import java.util.zip.ZipFile
object ApplicationObserver {
private val _createdActivities: MutableSet<Activity> = mutableSetOf()
private val _visibleActivities: MutableSet<Activity> = mutableSetOf()
private var visibleChanged: (Boolean) -> Unit = {}
private var appVisible = false
private set(value) {
if (field != value) {
field = value
visibleChanged(value)
}
}
val createdActivities: Set<Activity>
get() = _createdActivities
private val activityObserver = object : Application.ActivityLifecycleCallbacks {
@Synchronized
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
_createdActivities.add(activity)
}
@Synchronized
override fun onActivityDestroyed(activity: Activity) {
_createdActivities.remove(activity)
_visibleActivities.remove(activity)
appVisible = _visibleActivities.isNotEmpty()
}
override fun onActivityStarted(activity: Activity) {
_visibleActivities.add(activity)
appVisible = true
}
override fun onActivityStopped(activity: Activity) {
_visibleActivities.remove(activity)
appVisible = _visibleActivities.isNotEmpty()
}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
}
fun onVisibleChanged(visibleChanged: (Boolean) -> Unit) {
this.visibleChanged = visibleChanged
}
fun attach(application: Application) {
application.registerActivityLifecycleCallbacks(activityObserver)
}
}
fun Context.verifyApk(): Boolean {
return try {
val info = applicationInfo
val sources = info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return false
val regexNativeLibrary = Regex("lib/(\\S+)/libclash.so")
val availableAbi = Build.SUPPORTED_ABIS.toSet()
val apkAbi = sources
.asSequence()
.filter { File(it).exists() }
.flatMap { ZipFile(it).entries().asSequence() }
.mapNotNull { regexNativeLibrary.matchEntire(it.name) }
.mapNotNull { it.groups[1]?.value }
.toSet()
availableAbi.intersect(apkAbi).isNotEmpty()
} catch (e: Exception) {
false
}
}
@@ -0,0 +1,32 @@
package com.github.kr328.clash.util
import android.content.Context
import android.content.Intent
import android.net.VpnService
import com.github.kr328.clash.common.compat.startForegroundServiceCompat
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.store.UiStore
import com.github.kr328.clash.service.ClashService
import com.github.kr328.clash.service.TunService
import com.github.kr328.clash.service.util.sendBroadcastSelf
fun Context.startClashService(): Intent? {
val startTun = UiStore(this).enableVpn
if (startTun) {
val vpnRequest = VpnService.prepare(this)
if (vpnRequest != null)
return vpnRequest
startForegroundServiceCompat(TunService::class.intent)
} else {
startForegroundServiceCompat(ClashService::class.intent)
}
return null
}
fun Context.stopClashService() {
sendBroadcastSelf(Intent(Intents.ACTION_CLASH_REQUEST_STOP))
}
@@ -0,0 +1,25 @@
package com.github.kr328.clash.util
import android.content.ContentResolver
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
private fun fileNotFound(file: Uri): FileNotFoundException {
return FileNotFoundException("$file not found")
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun ContentResolver.copyContentTo(
source: Uri,
target: Uri
) {
withContext(Dispatchers.IO) {
(openInputStream(source) ?: throw fileNotFound(source)).use { input ->
(openOutputStream(target, "rwt") ?: throw fileNotFound(target)).use { output ->
input.copyTo(output)
}
}
}
}
@@ -0,0 +1,10 @@
package com.github.kr328.clash.util
import android.content.Context
import java.io.File
val Context.logsDir: File
get() = cacheDir.resolve("logs")
val Context.clashDir: File
get() = filesDir.resolve("clash")
@@ -0,0 +1,46 @@
package com.github.kr328.clash.util
import android.os.DeadObjectException
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.service.remote.IClashManager
import com.github.kr328.clash.service.remote.IProfileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
suspend fun <T> withClash(
context: CoroutineContext = Dispatchers.IO,
block: suspend IClashManager.() -> T
): T {
while (true) {
val remote = Remote.service.remote.get()
val client = remote.clash()
try {
return withContext(context) { client.block() }
} catch (e: DeadObjectException) {
Log.w("Remote services panic")
Remote.service.remote.reset(remote)
}
}
}
suspend fun <T> withProfile(
context: CoroutineContext = Dispatchers.IO,
block: suspend IProfileManager.() -> T
): T {
while (true) {
val remote = Remote.service.remote.get()
val client = remote.profile()
try {
return withContext(context) { client.block() }
} catch (e: DeadObjectException) {
Log.w("Remote services panic")
Remote.service.remote.reset(remote)
}
}
}
@@ -0,0 +1,12 @@
package com.github.kr328.clash.util
import android.content.Context
import android.content.ServiceConnection
fun Context.unbindServiceSilent(connection: ServiceConnection) {
try {
unbindService(connection)
} catch (e: Exception) {
// ignore
}
}
@@ -0,0 +1,6 @@
package com.github.kr328.clash.util
import android.net.Uri
val Uri.fileName: String?
get() = schemeSpecificPart.split("/").lastOrNull()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#1e1f22"
android:pathData="M480,640Q555,640 607.5,587.5Q660,535 660,460Q660,385 607.5,332.5Q555,280 480,280Q405,280 352.5,332.5Q300,385 300,460Q300,535 352.5,587.5Q405,640 480,640ZM480,568Q435,568 403.5,536.5Q372,505 372,460Q372,415 403.5,383.5Q435,352 480,352Q525,352 556.5,383.5Q588,415 588,460Q588,505 556.5,536.5Q525,568 480,568ZM480,760Q334,760 214,678.5Q94,597 40,460Q94,323 214,241.5Q334,160 480,160Q626,160 746,241.5Q866,323 920,460Q866,597 746,678.5Q626,760 480,760ZM480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,680Q593,680 687.5,620.5Q782,561 832,460Q782,359 687.5,299.5Q593,240 480,240Q367,240 272.5,299.5Q178,359 128,460Q178,561 272.5,620.5Q367,680 480,680Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#1e1f22"
android:pathData="M644,532L586,474Q595,427 559,386Q523,345 466,354L408,296Q425,288 442.5,284Q460,280 480,280Q555,280 607.5,332.5Q660,385 660,460Q660,480 656,497.5Q652,515 644,532ZM772,658L714,602Q752,573 781.5,538.5Q811,504 832,460Q782,359 688.5,299.5Q595,240 480,240Q451,240 423,244Q395,248 368,256L306,194Q347,177 390,168.5Q433,160 480,160Q631,160 749,243.5Q867,327 920,460Q897,519 859.5,569.5Q822,620 772,658ZM792,904L624,738Q589,749 553.5,754.5Q518,760 480,760Q329,760 211,676.5Q93,593 40,460Q61,407 93,361.5Q125,316 166,280L56,168L112,112L848,848L792,904ZM222,336Q193,362 169,393Q145,424 128,460Q178,561 271.5,620.5Q365,680 480,680Q500,680 519,677.5Q538,675 558,672L522,634Q511,637 501,638.5Q491,640 480,640Q405,640 352.5,587.5Q300,535 300,460Q300,449 301.5,439Q303,429 306,418L222,336ZM541,429L541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429ZM390,504Q390,504 390,504Q390,504 390,504L390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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