

























@@ -19,6 +19,7 @@ import kotlinx.coroutines.cancel
1919import kotlinx.coroutines.flow.combine
2020import kotlinx.coroutines.launch
212122+/** Foreground service that keeps the Android node connection and voice capture visible to the OS. */
2223class NodeForegroundService : Service() {
2324private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
2425private var notificationJob: Job? = null
@@ -36,6 +37,8 @@ class NodeForegroundService : Service() {
3637 stopSelf()
3738return
3839 }
40+// Split connection and capture flows before combining so notification text
41+// can update without restarting runtime-owned connection work.
3942 notificationJob =
4043 scope.launch {
4144 combine(
@@ -181,6 +184,7 @@ class NodeForegroundService : Service() {
181184private fun startForegroundWithTypes(notification: Notification) {
182185val serviceTypes = foregroundServiceTypesForVoiceMode(voiceCaptureMode)
183186if (didStartForeground) {
187+// Re-issue startForeground when Talk mode toggles so Android sees the microphone service type.
184188ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
185189return
186190 }
@@ -196,16 +200,19 @@ class NodeForegroundService : Service() {
196200private const val ACTION_SET_VOICE_CAPTURE_MODE = "ai.openclaw.app.action.SET_VOICE_CAPTURE_MODE"
197201private const val EXTRA_VOICE_CAPTURE_MODE = "ai.openclaw.app.extra.VOICE_CAPTURE_MODE"
198202203+/** Starts the persistent node foreground service from UI lifecycle code. */
199204fun start(context: Context) {
200205val intent = Intent(context, NodeForegroundService::class.java)
201206 context.startForegroundService(intent)
202207 }
203208209+/** Requests disconnect through the service action path so notification actions and UI share behavior. */
204210fun stop(context: Context) {
205211val intent = Intent(context, NodeForegroundService::class.java).setAction(ACTION_STOP)
206212 context.startService(intent)
207213 }
208214215+/** Updates Android's foreground-service type before voice capture mode changes require microphone access. */
209216fun setVoiceCaptureMode(
210217context: Context,
211218mode: VoiceCaptureMode,
@@ -215,6 +222,7 @@ class NodeForegroundService : Service() {
215222 .setAction(ACTION_SET_VOICE_CAPTURE_MODE)
216223 .putExtra(EXTRA_VOICE_CAPTURE_MODE, mode.name)
217224if (mode == VoiceCaptureMode.TalkMode) {
225+// Microphone foreground service type must be declared before Talk capture starts.
218226ContextCompat.startForegroundService(context, intent)
219227 } else {
220228 context.startService(intent)
@@ -223,6 +231,9 @@ class NodeForegroundService : Service() {
223231 }
224232}
225233234+/**
235+ * Foreground-service type mask required by Android for the current voice capture mode.
236+ */
226237internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
227238val base = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
228239return if (mode == VoiceCaptureMode.TalkMode) {
@@ -232,6 +243,9 @@ internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
232243 }
233244}
234245246+/**
247+ * Compact notification suffix for voice state; kept pure for service-notification tests.
248+ */
235249internal fun voiceNotificationSuffix(
236250mode: VoiceCaptureMode,
237251manualMicEnabled: Boolean,
@@ -260,20 +274,23 @@ private fun String?.toVoiceCaptureMode(): VoiceCaptureMode =
260274 it.name == this
261275 } ?: VoiceCaptureMode.Off
262276277+/** Connection fields that drive foreground notification title/body text. */
263278private data class VoiceNotificationBase(
264279val status: String,
265280val server: String?,
266281val connected: Boolean,
267282val mode: VoiceCaptureMode,
268283)
269284285+/** Voice capture fields that affect foreground-service type and suffix. */
270286private data class VoiceNotificationCapture(
271287val micEnabled: Boolean,
272288val micListening: Boolean,
273289val talkListening: Boolean,
274290val talkSpeaking: Boolean,
275291)
276292293+/** Aggregated notification state from runtime flows. */
277294private data class VoiceNotificationState(
278295val base: VoiceNotificationBase,
279296val capture: VoiceNotificationCapture,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。