Skip to content

Commit 0a227a2

Browse files
committed
feat: 添加 KeepAliveService 以确保 Tasker 广播可靠接收
问题: - 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: 更新文档说明
1 parent 01ddb1a commit 0a227a2

File tree

6 files changed

+181
-9
lines changed

6 files changed

+181
-9
lines changed

TASKER_GUIDE.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,28 @@
1212
2. 已安装 Tasker
1313
3. 已授予 Tasker 必要的权限
1414
4. **首次使用前,必须在 CMFA 中手动启动一次 VPN 并授予权限**
15+
5. **确认你的应用包名**(见下方说明)
16+
17+
### 📦 如何确认应用包名
18+
19+
**非常重要**:不同的编译版本和配置会有不同的包名。请使用以下方法确认你的应用包名:
20+
21+
**方法 1:通过 ADB(推荐)**
22+
```bash
23+
adb shell pm list packages | grep clash
24+
```
25+
26+
**方法 2:通过应用信息**
27+
1. 长按 CMFA 应用图标
28+
2. 点击"应用信息"
29+
3. 查看应用详情中的"包名"字段
30+
31+
常见的包名:
32+
- 自定义构建版本:`com.github.kr328.clash.tasker`(或其他自定义名称)
33+
- Alpha 官方版本:`com.github.kr328.clash.alpha`
34+
- Meta 官方版本:`com.github.metacubex.clash.meta`
35+
36+
**在下面的配置中,请将 `YOUR_PACKAGE_NAME` 替换为你实际的包名!**
1537

1638
⚠️ **重要:首次 VPN 权限授予**
1739

@@ -47,12 +69,14 @@
4769
| **Mime Type** | 留空 |
4870
| **Data** | 留空 |
4971
| **Extra** | 留空 |
50-
| **Package** | `com.github.metacubex.clash.meta` |
72+
| **Package** | `YOUR_PACKAGE_NAME` ⚠️(替换为你的实际包名,例如 `com.github.kr328.clash.tasker` |
5173
| **Class** | 留空(重要!) |
5274
| **Target** | **Broadcast Receiver**(非常重要!) |
5375

5476
7. 点击 **返回** 保存
5577

78+
**示例**:如果你的包名是 `com.github.kr328.clash.tasker`,则 Package 字段应填写:`com.github.kr328.clash.tasker`
79+
5680
### 步骤 2:创建停止 Clash 的 Task
5781

5882
重复步骤 1,但修改以下内容:

app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@
186186
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
187187
android:value="explanation_for_special_use"/>
188188
</service>
189+
190+
<!-- KeepAliveService: 保持应用活跃,确保自动化广播能可靠接收 -->
191+
<service
192+
android:name=".KeepAliveService"
193+
android:exported="false"
194+
android:label="@string/keepalive_service"
195+
android:foregroundServiceType="specialUse">
196+
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
197+
android:value="Keep app alive for automation broadcast reception"/>
198+
</service>
189199
<service
190200
android:name=".TileService"
191201
android:exported="true"
@@ -217,7 +227,7 @@
217227
</receiver>
218228

219229
<!-- ExternalControlReceiver: 用于 Tasker 等自动化工具的后台控制 -->
220-
<!-- 相比 ExternalControlActivity,BroadcastReceiver 完全后台运行,不会触发界面 -->
230+
<!-- 配合 KeepAliveService 使用,确保应用保持活跃以可靠接收广播 -->
221231
<receiver
222232
android:name=".ExternalControlReceiver"
223233
android:exported="true">

app/src/main/java/com/github/kr328/clash/ExternalControlReceiver.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import com.github.kr328.clash.util.stopClashService
1111
/**
1212
* ExternalControlReceiver - 用于 Tasker 等自动化工具的后台控制接收器
1313
*
14-
* 相比 ExternalControlActivity,BroadcastReceiver 完全在后台运行,
15-
* 不会触发任何界面,适合各种 ROM(包括 Flyme 等国产 ROM)的自动化场景。
14+
* 直接在 onReceive 中执行操作,无需启动 Activity。
1615
*
1716
* 使用方法(Tasker):
1817
* - 动作类型:Send Intent
@@ -28,16 +27,14 @@ class ExternalControlReceiver : BroadcastReceiver() {
2827
override fun onReceive(context: Context, intent: Intent) {
2928
Log.d(TAG, "收到广播: action=${intent.action}")
3029

30+
// 直接执行操作
3131
when (intent.action) {
3232
"com.github.metacubex.clash.meta.action.START_CLASH" -> {
3333
Log.d(TAG, "处理 START_CLASH,当前状态: ${Remote.broadcasts.clashRunning}")
34-
// 只在未运行时启动
3534
if (!Remote.broadcasts.clashRunning) {
3635
val vpnRequest = context.startClashService()
3736
if (vpnRequest != null) {
3837
Log.e(TAG, "需要 VPN 权限,请先在应用中手动启动一次")
39-
// BroadcastReceiver 无法启动 Activity 请求权限
40-
// 用户需要先在应用中手动启动一次授予 VPN 权限
4138
} else {
4239
Log.d(TAG, "Clash 服务已启动")
4340
}
@@ -48,7 +45,6 @@ class ExternalControlReceiver : BroadcastReceiver() {
4845

4946
"com.github.metacubex.clash.meta.action.STOP_CLASH" -> {
5047
Log.d(TAG, "处理 STOP_CLASH,当前状态: ${Remote.broadcasts.clashRunning}")
51-
// 只在运行时停止
5248
if (Remote.broadcasts.clashRunning) {
5349
context.stopClashService()
5450
Log.d(TAG, "Clash 服务已停止")
@@ -59,7 +55,6 @@ class ExternalControlReceiver : BroadcastReceiver() {
5955

6056
"com.github.metacubex.clash.meta.action.TOGGLE_CLASH" -> {
6157
Log.d(TAG, "处理 TOGGLE_CLASH,当前状态: ${Remote.broadcasts.clashRunning}")
62-
// 切换状态
6358
if (Remote.broadcasts.clashRunning) {
6459
context.stopClashService()
6560
Log.d(TAG, "Clash 服务已停止")
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.github.kr328.clash
2+
3+
import android.app.Notification
4+
import android.app.NotificationChannel
5+
import android.app.NotificationManager
6+
import android.app.PendingIntent
7+
import android.app.Service
8+
import android.content.Intent
9+
import android.os.Build
10+
import android.os.IBinder
11+
import android.util.Log
12+
import androidx.core.app.NotificationCompat
13+
14+
/**
15+
* KeepAliveService - 保持应用活跃的前台服务
16+
*
17+
* 用途:确保 ExternalControlReceiver 能可靠地接收来自 Tasker 等自动化工具的广播。
18+
* 通过运行一个轻量级的前台服务,防止应用进入深度休眠状态(CACHED_EMPTY),
19+
* 从而让 BroadcastReceiver 能够接收到第三方应用发送的广播。
20+
*
21+
* 特性:
22+
* - 使用最低优先级的通知(IMPORTANCE_MIN),不会打扰用户
23+
* - 几乎不消耗系统资源,仅用于保持进程优先级
24+
* - 在应用启动时自动启动,确保自动化功能始终可用
25+
*/
26+
class KeepAliveService : Service() {
27+
companion object {
28+
private const val TAG = "KeepAliveService"
29+
private const val NOTIFICATION_ID = 1001
30+
private const val CHANNEL_ID = "clash_keepalive"
31+
private const val CHANNEL_NAME = "后台自动化"
32+
}
33+
34+
override fun onCreate() {
35+
super.onCreate()
36+
Log.d(TAG, "KeepAliveService 已创建")
37+
38+
// 创建通知渠道(Android 8.0+)
39+
createNotificationChannel()
40+
41+
// 启动前台服务
42+
val notification = createNotification()
43+
startForeground(NOTIFICATION_ID, notification)
44+
45+
Log.d(TAG, "前台服务已启动,应用将保持活跃以接收自动化广播")
46+
}
47+
48+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
49+
Log.d(TAG, "onStartCommand: 服务正在运行")
50+
// START_STICKY: 如果服务被系统杀死,会自动重启
51+
return START_STICKY
52+
}
53+
54+
override fun onDestroy() {
55+
super.onDestroy()
56+
Log.d(TAG, "KeepAliveService 已销毁")
57+
}
58+
59+
override fun onBind(intent: Intent?): IBinder? {
60+
// 这是一个纯前台服务,不提供绑定
61+
return null
62+
}
63+
64+
/**
65+
* 创建通知渠道(Android 8.0+ 必需)
66+
*/
67+
private fun createNotificationChannel() {
68+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
69+
val notificationManager = getSystemService(NotificationManager::class.java)
70+
71+
// 使用 IMPORTANCE_MIN,通知会被最小化,不会发出声音和震动
72+
val channel = NotificationChannel(
73+
CHANNEL_ID,
74+
CHANNEL_NAME,
75+
NotificationManager.IMPORTANCE_MIN
76+
).apply {
77+
description = "保持应用活跃,确保 Tasker 等自动化工具能正常控制 Clash"
78+
setShowBadge(false) // 不显示角标
79+
enableLights(false) // 不闪烁指示灯
80+
enableVibration(false) // 不震动
81+
setSound(null, null) // 不发出声音
82+
}
83+
84+
notificationManager.createNotificationChannel(channel)
85+
Log.d(TAG, "通知渠道已创建: $CHANNEL_ID")
86+
}
87+
}
88+
89+
/**
90+
* 创建前台服务通知
91+
*/
92+
private fun createNotification(): Notification {
93+
// 点击通知时打开主界面
94+
val intent = Intent(this, MainActivity::class.java).apply {
95+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
96+
}
97+
val pendingIntent = PendingIntent.getActivity(
98+
this,
99+
0,
100+
intent,
101+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
102+
)
103+
104+
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
105+
.setContentTitle("Clash 服务")
106+
.setContentText("运行中")
107+
.setSmallIcon(R.mipmap.ic_launcher) // 使用应用启动器图标
108+
.setOngoing(true) // 不可滑动清除
109+
.setPriority(NotificationCompat.PRIORITY_MIN) // 最低优先级
110+
.setCategory(NotificationCompat.CATEGORY_SERVICE)
111+
112+
// 设置点击事件
113+
builder.setContentIntent(pendingIntent)
114+
115+
return builder.build()
116+
}
117+
}

app/src/main/java/com/github/kr328/clash/MainApplication.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.github.kr328.clash
22

33
import android.app.Application
44
import android.content.Context
5+
import android.content.Intent
6+
import android.os.Build
57
import com.github.kr328.clash.common.Global
68
import com.github.kr328.clash.common.compat.currentProcessName
79
import com.github.kr328.clash.common.log.Log
@@ -30,6 +32,8 @@ class MainApplication : Application() {
3032

3133
if (processName == packageName) {
3234
Remote.launch()
35+
// 启动 KeepAlive Service 以确保自动化广播能可靠接收
36+
startKeepAliveService()
3337
} else {
3438
sendServiceRecreated()
3539
}
@@ -70,6 +74,24 @@ class MainApplication : Application() {
7074
}
7175
}
7276

77+
/**
78+
* 启动 KeepAlive Service 以保持应用活跃
79+
* 确保 ExternalControlReceiver 能可靠接收来自 Tasker 等自动化工具的广播
80+
*/
81+
private fun startKeepAliveService() {
82+
try {
83+
val intent = Intent(this, KeepAliveService::class.java)
84+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
85+
startForegroundService(intent)
86+
} else {
87+
startService(intent)
88+
}
89+
Log.d("KeepAlive Service started successfully")
90+
} catch (e: Exception) {
91+
Log.e("Failed to start KeepAlive Service", e)
92+
}
93+
}
94+
7395
fun finalize() {
7496
Global.destroy()
7597
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<string name="keepalive_service">后台自动化保活服务</string>
4+
</resources>

0 commit comments

Comments
 (0)