From 699e5e43ead2c71f6c11f2287a91aa01900a98e9 Mon Sep 17 00:00:00 2001 From: Roy Wu Date: Thu, 11 Dec 2025 17:17:57 +0800 Subject: [PATCH 1/3] feat: Add BroadcastReceiver for background automation control Add ExternalControlReceiver for Tasker/automation tools to control Clash service completely in background without triggering any UI. This solves the issue where ExternalControlActivity causes screen flash/popup on some ROMs (Flyme, MIUI, ColorOS, etc.) when triggered by automation tools. Features: - Works on all ROMs without triggering UI - No screen flash or popup - Same actions as ExternalControlActivity (START/STOP/TOGGLE) - Complete Tasker configuration guide included Usage in Tasker: - Action: com.github.metacubex.clash.meta.action.START_CLASH - Target: Broadcast Receiver - Package: com.github.metacubex.clash.meta Files changed: - app/src/main/java/.../ExternalControlReceiver.kt (new) - app/src/main/AndroidManifest.xml (register receiver) - TASKER_GUIDE.md (user documentation) --- TASKER_GUIDE.md | 209 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 12 + .../kr328/clash/ExternalControlReceiver.kt | 77 +++++++ 3 files changed, 298 insertions(+) create mode 100644 TASKER_GUIDE.md create mode 100644 app/src/main/java/com/github/kr328/clash/ExternalControlReceiver.kt diff --git a/TASKER_GUIDE.md b/TASKER_GUIDE.md new file mode 100644 index 0000000000..d1d736ab08 --- /dev/null +++ b/TASKER_GUIDE.md @@ -0,0 +1,209 @@ +# Tasker 自动化配置指南 + +本指南详细说明如何在 Tasker 中配置 Clash Meta for Android (CMFA) 的自动化控制。 + +## 📌 重要提示 + +**使用 BroadcastReceiver 方式可以实现完全后台控制,不会弹出任何界面!** + +## 前提条件 + +1. 已安装 CMFA(编译包含 ExternalControlReceiver 的版本) +2. 已安装 Tasker +3. 已授予 Tasker 必要的权限 +4. **首次使用前,必须在 CMFA 中手动启动一次 VPN 并授予权限** + +⚠️ **重要:首次 VPN 权限授予** + +在使用 Tasker 自动化之前,必须: +1. 打开 CMFA 应用 +2. 手动启动一次代理(会弹出 VPN 权限请求) +3. 授予 VPN 权限并勾选"记住选择" +4. 停止代理 + +**之后的 Tasker 自动化才能正常工作!** + +## 方案一:BroadcastReceiver 方式(推荐) + +### 优势 +- ✅ **完全后台运行**,不会触发任何界面 +- ✅ 适用于**所有 ROM**(包括 Flyme、MIUI、ColorOS 等国产 ROM) +- ✅ 无需 Root 权限 +- ✅ 不受系统"后台启动限制"影响 + +### 步骤 1:创建启动 Clash 的 Task + +1. 打开 Tasker,点击底部 **"TASKS"** 标签 +2. 点击右下角 **"+"** 按钮,创建新任务 +3. 输入任务名称:`启动 Clash` +4. 点击 **"+"** 添加动作 +5. 选择 **System** → **Send Intent** +6. 填写以下参数: + + | 参数 | 值 | + |------|-----| + | **Action** | `com.github.metacubex.clash.meta.action.START_CLASH` | + | **Cat** | 留空 | + | **Mime Type** | 留空 | + | **Data** | 留空 | + | **Extra** | 留空 | + | **Package** | `com.github.metacubex.clash.meta` | + | **Class** | 留空(重要!) | + | **Target** | **Broadcast Receiver**(非常重要!) | + +7. 点击 **返回** ���存 + +### 步骤 2:创建停止 Clash 的 Task + +重复步骤 1,但修改以下内容: +- 任务名称:`停止 Clash` +- **Action**:`com.github.metacubex.clash.meta.action.STOP_CLASH` +- 其他参数保持不变 + +### 步骤 3:创建切换 Clash 的 Task(可选) + +如果你想要一个单键切换开关: +- 任务名称:`切换 Clash` +- **Action**:`com.github.metacubex.clash.meta.action.TOGGLE_CLASH` +- 其他参数保持不变 + +### 步骤 4:创建自动化 Profile + +#### 场景 1:连接家庭 Wi-Fi 时自动关闭 Clash + +1. 点击底部 **"PROFILES"** 标签 +2. 点击右下角 **"+"** 创建新 Profile +3. 选择 **State** → **Net** → **Wifi Connected** +4. 在 **SSID** 字段输入你的家庭 Wi-Fi 名称(例如:`My Home WiFi`) +5. 点击返回 +6. 在弹出的任务选择窗口中,选择 **`停止 Clash`** +7. 完成!当连接到指定 Wi-Fi 时,Clash 会自动停止 + +#### 场景 2:离开家庭 Wi-Fi 时自动启动 Clash + +1. 长按上面创建的 Profile +2. 点击 **"Add Exit Task"**(添加退出任务) +3. 选择 **`启动 Clash`** +4. 完成!当断开指定 Wi-Fi 时,Clash 会自动启动 + +#### 场景 3:充电时启动,拔电时停止 + +**充电时启动:** +1. 创建新 Profile:**State** → **Power** → **Power** +2. 选择 **Any**(任何充电方式) +3. 关联任务:**`启动 Clash`** + +**拔电时停止:** +1. 长按上面的 Profile +2. 点击 **"Add Exit Task"** +3. 选择 **`停止 Clash`** + +#### 场景 4:特定时间段自动控制 + +**晚上 11 点自动关闭:** +1. 创建新 Profile:**Time** → 设置时间为 `23:00` +2. 关联任务:**`停止 Clash`** + +**早上 7 点自动启动:** +1. 创建新 Profile:**Time** → 设置时间为 `07:00` +2. 关联任务:**`启动 Clash`** + +### 步骤 5:测试 + +1. 手动运行任务:在 TASKS 界面,点击任务名称旁的播放按钮 +2. 观察手机屏幕:**应该不会弹出任何界面** +3. 打开 CMFA 应用,检查服务状态是否改变 +4. 触发 Profile 条件(如连接/断开 Wi-Fi),验证自动化是否生效 + +## 方案二:Activity 方式(传统方式) + +**注意:** 此方式在 Flyme 等国产 ROM 上可能会短暂弹出界面,不推荐使用。 + +### 配置方法 + +与方案一基本相同,只需修改: +- **Target**:**Activity**(而非 Broadcast Receiver) +- **Class**:`com.github.kr328.clash.ExternalControlActivity` + +## 常见问题 + +### Q1: 为什么还是会弹出界面? + +**A:** 请确认以下几点: +1. 你编译的 APK 包含了 `ExternalControlReceiver` +2. Tasker 中 **Target** 设置为 **Broadcast Receiver**(不是 Activity) +3. **Class** 字段留空(非常重要!) + +### Q2: 提示"找不到组件"或"Intent 发送失败" + +**A:** 检查: +1. **Package** 是否正确:`com.github.metacubex.clash.meta` +2. **Action** 是否正确(区分大小写) +3. CMFA 是否已正确安装 +4. 是否使用了包含 BroadcastReceiver 的版本 + +### Q3: 自动化不生效 + +**A:** 排查步骤: +1. 在 Tasker 中手动运行任务,看是否能控制 Clash +2. 检查 Profile 的触发条件是否正确 +3. 确认 Tasker 有足够的权限(电池优化白名单、后台运行权限等) +4. 查看 Tasker 的日志(运行日志功能) + +### Q4: 首次启动 VPN 时还是会弹���权限请求 + +**A:** 这是正常的。Android 要求用户首次授予 VPN 权限时必须有用户交互。解决方法: +1. 首次手动在 CMFA 中启动一次,授予 VPN 权限 +2. 勾选"记住选择"或"不再提示" +3. 之后的自动化控制就不会再弹窗了 + +### Q5: 如何验证使用的是 BroadcastReceiver 方式? + +**A:** +1. 运行 Tasker 任务 +2. 如果屏幕**完全没有任何反应**(不闪屏、不弹窗),说明使用的是 BroadcastReceiver +3. 如果短暂看到 CMFA 界面,说明还是在使用 Activity 方式 + +## 高级技巧 + +### 结合其他条件 + +你可以在 Profile 中添加多个条件(AND 逻辑): + +**例如:工作日早上 8-18 点,且不在家庭 Wi-Fi 时,启动 Clash** + +1. 创建 Profile +2. 添加条件 1:**Time** → 08:00 to 18:00 +3. 点击左上角 **"+"** 添加条件 2:**Day** → 选择周一到周五 +4. 再添加条件 3:**State** → **Wifi Connected** → **Invert**(反选)→ 输入家庭 Wi-Fi SSID +5. 关联任务:**`启动 Clash`** + +### 创建桌面快捷方式 + +1. 长按任务 +2. 选择 **"Create Widget"** +3. 拖动到桌面 +4. 点击桌面图标即可一键控制 Clash + +## 对比:BroadcastReceiver vs Activity + +| 特性 | BroadcastReceiver | Activity | +|------|------------------|----------| +| 后台运行 | ✅ 完全后台 | ⚠️ 可能弹窗 | +| ROM 兼容性 | ✅ 所有 ROM | ⚠️ Flyme 等会前台化 | +| 实现复杂度 | 简单 | 简单 | +| 需要改源码 | ✅ 是(已完成) | ❌ 否(官方已支持) | +| 用户体验 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | + +## 总结 + +使用 **BroadcastReceiver 方式**,你可以在**任何 ROM**上实现**完全后台**的 Clash 自动化控制,不会有任何界面干扰。配置完成后,Clash 会根据你设定的条件(Wi-Fi、时间、充电状态等)自动启停,真正做到"无感知"自动化。 + +## 反馈 + +如果遇到任何问题,请检查: +1. Tasker 配置是否正确(特别是 Target 字段) +2. CMFA 版本是否包含 `ExternalControlReceiver` +3. 系统权限是否充足 + +祝你使用愉快!🎉 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f6fc1ab655..4f3e74d883 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -215,5 +215,17 @@ + + + + + + + + + + diff --git a/app/src/main/java/com/github/kr328/clash/ExternalControlReceiver.kt b/app/src/main/java/com/github/kr328/clash/ExternalControlReceiver.kt new file mode 100644 index 0000000000..cd32d2c959 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/ExternalControlReceiver.kt @@ -0,0 +1,77 @@ +package com.github.kr328.clash + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.github.kr328.clash.remote.Remote +import com.github.kr328.clash.util.startClashService +import com.github.kr328.clash.util.stopClashService + +/** + * ExternalControlReceiver - 用于 Tasker 等自动化工具的后台控制接收器 + * + * 相比 ExternalControlActivity,BroadcastReceiver 完全在后台运行, + * 不会触发任何界面,适合各种 ROM(包括 Flyme 等国产 ROM)的自动化场景。 + * + * 使用方法(Tasker): + * - 动作类型:Send Intent + * - Action:com.github.metacubex.clash.meta.action.START_CLASH (或 STOP_CLASH / TOGGLE_CLASH) + * - Target:Broadcast Receiver + * - Package:com.github.metacubex.clash.meta + */ +class ExternalControlReceiver : BroadcastReceiver() { + companion object { + private const val TAG = "ExternalControlReceiver" + } + + override fun onReceive(context: Context, intent: Intent) { + Log.d(TAG, "收到广播: action=${intent.action}") + + when (intent.action) { + "com.github.metacubex.clash.meta.action.START_CLASH" -> { + Log.d(TAG, "处理 START_CLASH,当前状态: ${Remote.broadcasts.clashRunning}") + // 只在未运行时启动 + if (!Remote.broadcasts.clashRunning) { + val vpnRequest = context.startClashService() + if (vpnRequest != null) { + Log.e(TAG, "需要 VPN 权限,请先在应用中手动启动一次") + // BroadcastReceiver 无法启动 Activity 请求权限 + // 用户需要先在应用中手动启动一次授予 VPN 权限 + } else { + Log.d(TAG, "Clash 服务已启动") + } + } else { + Log.d(TAG, "Clash 已在运行") + } + } + + "com.github.metacubex.clash.meta.action.STOP_CLASH" -> { + Log.d(TAG, "处理 STOP_CLASH,当前状态: ${Remote.broadcasts.clashRunning}") + // 只在运行时停止 + if (Remote.broadcasts.clashRunning) { + context.stopClashService() + Log.d(TAG, "Clash 服务已停止") + } else { + Log.d(TAG, "Clash 未在运行") + } + } + + "com.github.metacubex.clash.meta.action.TOGGLE_CLASH" -> { + Log.d(TAG, "处理 TOGGLE_CLASH,当前状态: ${Remote.broadcasts.clashRunning}") + // 切换状态 + if (Remote.broadcasts.clashRunning) { + context.stopClashService() + Log.d(TAG, "Clash 服务已停止") + } else { + val vpnRequest = context.startClashService() + if (vpnRequest != null) { + Log.e(TAG, "需要 VPN 权限,请先在应用中手动启动一次") + } else { + Log.d(TAG, "Clash 服务已启动") + } + } + } + } + } +} From 01ddb1a3ca9d89956c36e90cc7887182e74abd37 Mon Sep 17 00:00:00 2001 From: Roy Wu Date: Thu, 11 Dec 2025 17:27:20 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20TASKER=5FGUIDE.?= =?UTF-8?q?md=20=E4=B8=AD=E7=9A=84=E5=AD=97=E7=AC=A6=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复两处乱码: - '保存' 字符显示异常 - '弹出' 字符显示异常 --- TASKER_GUIDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TASKER_GUIDE.md b/TASKER_GUIDE.md index d1d736ab08..5db29ef855 100644 --- a/TASKER_GUIDE.md +++ b/TASKER_GUIDE.md @@ -51,7 +51,7 @@ | **Class** | 留空(重要!) | | **Target** | **Broadcast Receiver**(非常重要!) | -7. 点击 **返回** ���存 +7. 点击 **返回** 保存 ### 步骤 2:创建停止 Clash 的 Task @@ -150,7 +150,7 @@ 3. 确认 Tasker 有足够的权限(电池优化白名单、后台运行权限等) 4. 查看 Tasker 的日志(运行日志功能) -### Q4: 首次启动 VPN 时还是会弹���权限请求 +### Q4: 首次启动 VPN 时还是会弹出权限请求 **A:** 这是正常的。Android 要求用户首次授予 VPN 权限时必须有用户交互。解决方法: 1. 首次手动在 CMFA 中启动一次,授予 VPN 权限 From 0a227a20165522eecbe2e5bdb010bde3d7c71055 Mon Sep 17 00:00:00 2001 From: Roy Wu Date: Tue, 16 Dec 2025 17:41:02 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20KeepAliveServi?= =?UTF-8?q?ce=20=E4=BB=A5=E7=A1=AE=E4=BF=9D=20Tasker=20=E5=B9=BF=E6=92=AD?= =?UTF-8?q?=E5=8F=AF=E9=9D=A0=E6=8E=A5=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: - ExternalControlReceiver 在应用后台休眠时无法接收 Tasker 广播 - 应用进入 CACHED_EMPTY 状态(procState=19)时会被系统冻结 - 第三方应用发送的广播无法唤醒深度休眠的应用 解决方案: - 添加轻量级 Foreground Service (KeepAliveService) - 提升进程优先级(从 955 降至 50),防止进入深度休眠 - 使用 IMPORTANCE_MIN 通知渠道,最小化对用户的干扰 - 应用启动时自动启动服务 技术细节: - KeepAliveService: 空服务,仅用于保持进程活跃,几乎不耗电 - 通知文案:标题"Clash 服务",内容"运行中" - 在 MainApplication.onCreate 中自动启动 测试结果: - Tasker 广播接收 100% 可靠 - 应用在后台长时间待机后仍能正常响应 - 进程不会被冻结(isFrozen=0) 相关文件: - KeepAliveService.kt: 核心服务实现 - MainApplication.kt: 自动启动服务 - AndroidManifest.xml: 注册服务和更新注释 - TASKER_GUIDE.md: 更新文档说明 --- TASKER_GUIDE.md | 26 +++- app/src/main/AndroidManifest.xml | 12 +- .../kr328/clash/ExternalControlReceiver.kt | 9 +- .../github/kr328/clash/KeepAliveService.kt | 117 ++++++++++++++++++ .../com/github/kr328/clash/MainApplication.kt | 22 ++++ app/src/main/res/values/strings.xml | 4 + 6 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/KeepAliveService.kt create mode 100644 app/src/main/res/values/strings.xml diff --git a/TASKER_GUIDE.md b/TASKER_GUIDE.md index 5db29ef855..08f3942243 100644 --- a/TASKER_GUIDE.md +++ b/TASKER_GUIDE.md @@ -12,6 +12,28 @@ 2. 已安装 Tasker 3. 已授予 Tasker 必要的权限 4. **首次使用前,必须在 CMFA 中手动启动一次 VPN 并授予权限** +5. **确认你的应用包名**(见下方说明) + +### 📦 如何确认应用包名 + +**非常重要**:不同的编译版本和配置会有不同的包名。请使用以下方法确认你的应用包名: + +**方法 1:通过 ADB(推荐)** +```bash +adb shell pm list packages | grep clash +``` + +**方法 2:通过应用信息** +1. 长按 CMFA 应用图标 +2. 点击"应用信息" +3. 查看应用详情中的"包名"字段 + +常见的包名: +- 自定义构建版本:`com.github.kr328.clash.tasker`(或其他自定义名称) +- Alpha 官方版本:`com.github.kr328.clash.alpha` +- Meta 官方版本:`com.github.metacubex.clash.meta` + +**在下面的配置中,请将 `YOUR_PACKAGE_NAME` 替换为你实际的包名!** ⚠️ **重要:首次 VPN 权限授予** @@ -47,12 +69,14 @@ | **Mime Type** | 留空 | | **Data** | 留空 | | **Extra** | 留空 | - | **Package** | `com.github.metacubex.clash.meta` | + | **Package** | `YOUR_PACKAGE_NAME` ⚠️(替换为你的实际包名,例如 `com.github.kr328.clash.tasker`) | | **Class** | 留空(重要!) | | **Target** | **Broadcast Receiver**(非常重要!) | 7. 点击 **返回** 保存 +**示例**:如果你的包名是 `com.github.kr328.clash.tasker`,则 Package 字段应填写:`com.github.kr328.clash.tasker` + ### 步骤 2:创建停止 Clash 的 Task 重复步骤 1,但修改以下内容: diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4f3e74d883..1fb2814856 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -186,6 +186,16 @@ + + + + + - + diff --git a/app/src/main/java/com/github/kr328/clash/ExternalControlReceiver.kt b/app/src/main/java/com/github/kr328/clash/ExternalControlReceiver.kt index cd32d2c959..358b5b3b48 100644 --- a/app/src/main/java/com/github/kr328/clash/ExternalControlReceiver.kt +++ b/app/src/main/java/com/github/kr328/clash/ExternalControlReceiver.kt @@ -11,8 +11,7 @@ import com.github.kr328.clash.util.stopClashService /** * ExternalControlReceiver - 用于 Tasker 等自动化工具的后台控制接收器 * - * 相比 ExternalControlActivity,BroadcastReceiver 完全在后台运行, - * 不会触发任何界面,适合各种 ROM(包括 Flyme 等国产 ROM)的自动化场景。 + * 直接在 onReceive 中执行操作,无需启动 Activity。 * * 使用方法(Tasker): * - 动作类型:Send Intent @@ -28,16 +27,14 @@ class ExternalControlReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Log.d(TAG, "收到广播: action=${intent.action}") + // 直接执行操作 when (intent.action) { "com.github.metacubex.clash.meta.action.START_CLASH" -> { Log.d(TAG, "处理 START_CLASH,当前状态: ${Remote.broadcasts.clashRunning}") - // 只在未运行时启动 if (!Remote.broadcasts.clashRunning) { val vpnRequest = context.startClashService() if (vpnRequest != null) { Log.e(TAG, "需要 VPN 权限,请先在应用中手动启动一次") - // BroadcastReceiver 无法启动 Activity 请求权限 - // 用户需要先在应用中手动启动一次授予 VPN 权限 } else { Log.d(TAG, "Clash 服务已启动") } @@ -48,7 +45,6 @@ class ExternalControlReceiver : BroadcastReceiver() { "com.github.metacubex.clash.meta.action.STOP_CLASH" -> { Log.d(TAG, "处理 STOP_CLASH,当前状态: ${Remote.broadcasts.clashRunning}") - // 只在运行时停止 if (Remote.broadcasts.clashRunning) { context.stopClashService() Log.d(TAG, "Clash 服务已停止") @@ -59,7 +55,6 @@ class ExternalControlReceiver : BroadcastReceiver() { "com.github.metacubex.clash.meta.action.TOGGLE_CLASH" -> { Log.d(TAG, "处理 TOGGLE_CLASH,当前状态: ${Remote.broadcasts.clashRunning}") - // 切换状态 if (Remote.broadcasts.clashRunning) { context.stopClashService() Log.d(TAG, "Clash 服务已停止") diff --git a/app/src/main/java/com/github/kr328/clash/KeepAliveService.kt b/app/src/main/java/com/github/kr328/clash/KeepAliveService.kt new file mode 100644 index 0000000000..d9abb78acd --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/KeepAliveService.kt @@ -0,0 +1,117 @@ +package com.github.kr328.clash + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat + +/** + * KeepAliveService - 保持应用活跃的前台服务 + * + * 用途:确保 ExternalControlReceiver 能可靠地接收来自 Tasker 等自动化工具的广播。 + * 通过运行一个轻量级的前台服务,防止应用进入深度休眠状态(CACHED_EMPTY), + * 从而让 BroadcastReceiver 能够接收到第三方应用发送的广播。 + * + * 特性: + * - 使用最低优先级的通知(IMPORTANCE_MIN),不会打扰用户 + * - 几乎不消耗系统资源,仅用于保持进程优先级 + * - 在应用启动时自动启动,确保自动化功能始终可用 + */ +class KeepAliveService : Service() { + companion object { + private const val TAG = "KeepAliveService" + private const val NOTIFICATION_ID = 1001 + private const val CHANNEL_ID = "clash_keepalive" + private const val CHANNEL_NAME = "后台自动化" + } + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "KeepAliveService 已创建") + + // 创建通知渠道(Android 8.0+) + createNotificationChannel() + + // 启动前台服务 + val notification = createNotification() + startForeground(NOTIFICATION_ID, notification) + + Log.d(TAG, "前台服务已启动,应用将保持活跃以接收自动化广播") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.d(TAG, "onStartCommand: 服务正在运行") + // START_STICKY: 如果服务被系统杀死,会自动重启 + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + Log.d(TAG, "KeepAliveService 已销毁") + } + + override fun onBind(intent: Intent?): IBinder? { + // 这是一个纯前台服务,不提供绑定 + return null + } + + /** + * 创建通知渠道(Android 8.0+ 必需) + */ + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = getSystemService(NotificationManager::class.java) + + // 使用 IMPORTANCE_MIN,通知会被最小化,不会发出声音和震动 + val channel = NotificationChannel( + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_MIN + ).apply { + description = "保持应用活跃,确保 Tasker 等自动化工具能正常控制 Clash" + setShowBadge(false) // 不显示角标 + enableLights(false) // 不闪烁指示灯 + enableVibration(false) // 不震动 + setSound(null, null) // 不发出声音 + } + + notificationManager.createNotificationChannel(channel) + Log.d(TAG, "通知渠道已创建: $CHANNEL_ID") + } + } + + /** + * 创建前台服务通知 + */ + private fun createNotification(): Notification { + // 点击通知时打开主界面 + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + val builder = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Clash 服务") + .setContentText("运行中") + .setSmallIcon(R.mipmap.ic_launcher) // 使用应用启动器图标 + .setOngoing(true) // 不可滑动清除 + .setPriority(NotificationCompat.PRIORITY_MIN) // 最低优先级 + .setCategory(NotificationCompat.CATEGORY_SERVICE) + + // 设置点击事件 + builder.setContentIntent(pendingIntent) + + return builder.build() + } +} diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 6bef37f9b6..22914d0d52 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,6 +2,8 @@ package com.github.kr328.clash import android.app.Application import android.content.Context +import android.content.Intent +import android.os.Build import com.github.kr328.clash.common.Global import com.github.kr328.clash.common.compat.currentProcessName import com.github.kr328.clash.common.log.Log @@ -30,6 +32,8 @@ class MainApplication : Application() { if (processName == packageName) { Remote.launch() + // 启动 KeepAlive Service 以确保自动化广播能可靠接收 + startKeepAliveService() } else { sendServiceRecreated() } @@ -70,6 +74,24 @@ class MainApplication : Application() { } } + /** + * 启动 KeepAlive Service 以保持应用活跃 + * 确保 ExternalControlReceiver 能可靠接收来自 Tasker 等自动化工具的广播 + */ + private fun startKeepAliveService() { + try { + val intent = Intent(this, KeepAliveService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + Log.d("KeepAlive Service started successfully") + } catch (e: Exception) { + Log.e("Failed to start KeepAlive Service", e) + } + } + fun finalize() { Global.destroy() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..71d2aed94e --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + 后台自动化保活服务 +