
























@@ -3,12 +3,14 @@ package ai.openclaw.app
33import android.app.Notification
44import android.app.NotificationChannel
55import android.app.NotificationManager
6-import android.app.Service
76import android.app.PendingIntent
7+import android.app.Service
88import android.content.Context
99import android.content.Intent
1010import android.content.pm.ServiceInfo
1111import androidx.core.app.NotificationCompat
12+import androidx.core.app.ServiceCompat
13+import androidx.core.content.ContextCompat
1214import kotlinx.coroutines.CoroutineScope
1315import kotlinx.coroutines.Dispatchers
1416import kotlinx.coroutines.Job
@@ -21,6 +23,7 @@ class NodeForegroundService : Service() {
2123private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
2224private var notificationJob: Job? = null
2325private var didStartForeground = false
26+private var voiceCaptureMode = VoiceCaptureMode.Off
24272528override fun onCreate() {
2629super.onCreate()
@@ -36,22 +39,51 @@ class NodeForegroundService : Service() {
3639 notificationJob =
3740 scope.launch {
3841 combine(
39- runtime.statusText,
40- runtime.serverName,
41- runtime.isConnected,
42- runtime.micEnabled,
43- runtime.micIsListening,
44- ) { status, server, connected, micEnabled, micListening ->
45-Quint(status, server, connected, micEnabled, micListening)
46- }.collect { (status, server, connected, micEnabled, micListening) ->
47-val title = if (connected) "OpenClaw Node · Connected" else "OpenClaw Node"
48-val micSuffix =
49-if (micEnabled) {
50-if (micListening) " · Mic: Listening" else " · Mic: Pending"
51- } else {
52-""
42+ combine(
43+ runtime.statusText,
44+ runtime.serverName,
45+ runtime.isConnected,
46+ runtime.voiceCaptureMode,
47+ ) { status, server, connected, mode ->
48+VoiceNotificationBase(
49+ status = status,
50+ server = server,
51+ connected = connected,
52+ mode = mode,
53+ )
54+ },
55+ combine(
56+ runtime.micEnabled,
57+ runtime.micIsListening,
58+ runtime.talkModeListening,
59+ runtime.talkModeSpeaking,
60+ ) { micEnabled, micListening, talkListening, talkSpeaking ->
61+VoiceNotificationCapture(
62+ micEnabled = micEnabled,
63+ micListening = micListening,
64+ talkListening = talkListening,
65+ talkSpeaking = talkSpeaking,
66+ )
67+ },
68+ ) { base, capture ->
69+VoiceNotificationState(base = base, capture = capture)
70+ }.collect { state ->
71+ voiceCaptureMode = state.mode
72+val title =
73+when {
74+ state.connected && state.mode == VoiceCaptureMode.TalkMode -> "OpenClaw Node · Talk"
75+ state.connected -> "OpenClaw Node · Connected"
76+else -> "OpenClaw Node"
5377 }
54-val text = (server?.let { "$status · $it" } ?: status) + micSuffix
78+val text =
79+ (state.server?.let { "${state.status} · $it" } ?: state.status) +
80+ voiceNotificationSuffix(
81+ mode = state.mode,
82+ manualMicEnabled = state.capture.micEnabled,
83+ manualMicListening = state.capture.micListening,
84+ talkListening = state.capture.talkListening,
85+ talkSpeaking = state.capture.talkSpeaking,
86+ )
55875688 startForegroundWithTypes(
5789 notification = buildNotification(title = title, text = text),
@@ -60,13 +92,27 @@ class NodeForegroundService : Service() {
6092 }
6193 }
629463-override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
95+override fun onStartCommand(
96+intent: Intent?,
97+flags: Int,
98+startId: Int,
99+ ): Int {
64100when (intent?.action) {
65101ACTION_STOP -> {
66102 (application as NodeApp).peekRuntime()?.disconnect()
67103 stopSelf()
68104return START_NOT_STICKY
69105 }
106+ACTION_SET_VOICE_CAPTURE_MODE -> {
107+ voiceCaptureMode = intent.getStringExtra(EXTRA_VOICE_CAPTURE_MODE).toVoiceCaptureMode()
108+ startForegroundWithTypes(
109+ notification =
110+ buildNotification(
111+ title = "OpenClaw Node",
112+ text = if (voiceCaptureMode == VoiceCaptureMode.TalkMode) "Talk mode active" else "Connected",
113+ ),
114+ )
115+ }
70116 }
71117// Keep running; connection is managed by NodeRuntime (auto-reconnect + manual).
72118return START_STICKY
@@ -127,17 +173,13 @@ class NodeForegroundService : Service() {
127173 .build()
128174 }
129175130-private fun updateNotification(notification: Notification) {
131-val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
132- mgr.notify(NOTIFICATION_ID, notification)
133- }
134-135176private fun startForegroundWithTypes(notification: Notification) {
177+val serviceTypes = foregroundServiceTypesForVoiceMode(voiceCaptureMode)
136178if (didStartForeground) {
137-updateNotification(notification)
179+ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
138180return
139181 }
140- startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
182+ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
141183 didStartForeground = true
142184 }
143185@@ -146,6 +188,8 @@ class NodeForegroundService : Service() {
146188private const val NOTIFICATION_ID = 1
147189148190private const val ACTION_STOP = "ai.openclaw.app.action.STOP"
191+private const val ACTION_SET_VOICE_CAPTURE_MODE = "ai.openclaw.app.action.SET_VOICE_CAPTURE_MODE"
192+private const val EXTRA_VOICE_CAPTURE_MODE = "ai.openclaw.app.extra.VOICE_CAPTURE_MODE"
149193150194fun start(context: Context) {
151195val intent = Intent(context, NodeForegroundService::class.java)
@@ -156,7 +200,85 @@ class NodeForegroundService : Service() {
156200val intent = Intent(context, NodeForegroundService::class.java).setAction(ACTION_STOP)
157201 context.startService(intent)
158202 }
203+204+fun setVoiceCaptureMode(
205+context: Context,
206+mode: VoiceCaptureMode,
207+ ) {
208+val intent =
209+Intent(context, NodeForegroundService::class.java)
210+ .setAction(ACTION_SET_VOICE_CAPTURE_MODE)
211+ .putExtra(EXTRA_VOICE_CAPTURE_MODE, mode.name)
212+if (mode == VoiceCaptureMode.TalkMode) {
213+ContextCompat.startForegroundService(context, intent)
214+ } else {
215+ context.startService(intent)
216+ }
217+ }
218+ }
219+}
220+221+internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
222+val base = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
223+return if (mode == VoiceCaptureMode.TalkMode) {
224+ base or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
225+ } else {
226+ base
159227 }
160228}
161229162-private data class Quint<A, B, C, D, E>(val first: A, val second: B, val third: C, val fourth: D, val fifth: E)
230+internal fun voiceNotificationSuffix(
231+mode: VoiceCaptureMode,
232+manualMicEnabled: Boolean,
233+manualMicListening: Boolean,
234+talkListening: Boolean,
235+talkSpeaking: Boolean,
236+): String {
237+return when (mode) {
238+VoiceCaptureMode.TalkMode ->
239+when {
240+ talkSpeaking -> " · Talk: Speaking"
241+ talkListening -> " · Talk: Listening"
242+else -> " · Talk: On"
243+ }
244+VoiceCaptureMode.ManualMic ->
245+if (manualMicEnabled) {
246+if (manualMicListening) " · Mic: Listening" else " · Mic: Pending"
247+ } else {
248+""
249+ }
250+VoiceCaptureMode.Off -> ""
251+ }
252+}
253+254+private fun String?.toVoiceCaptureMode(): VoiceCaptureMode {
255+return VoiceCaptureMode.entries.firstOrNull { it.name == this } ?: VoiceCaptureMode.Off
256+}
257+258+private data class VoiceNotificationBase(
259+val status: String,
260+val server: String?,
261+val connected: Boolean,
262+val mode: VoiceCaptureMode,
263+)
264+265+private data class VoiceNotificationCapture(
266+val micEnabled: Boolean,
267+val micListening: Boolean,
268+val talkListening: Boolean,
269+val talkSpeaking: Boolean,
270+)
271+272+private data class VoiceNotificationState(
273+val base: VoiceNotificationBase,
274+val capture: VoiceNotificationCapture,
275+) {
276+val status: String
277+ get() = base.status
278+val server: String?
279+ get() = base.server
280+val connected: Boolean
281+ get() = base.connected
282+val mode: VoiceCaptureMode
283+ get() = base.mode
284+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。