fix(app): WakeLock auch im AriaPlaybackService — Pipeline-weiter Schutz

Stefan-Ergaenzung: nach Wake-Word muss Aufnahme, Senden und ARIA-
Antwort + TTS auch im Hintergrund klappen, und danach soll das ganze
wieder von vorne als Konversations-Schleife laufen.

Vorher hielt nur OpenWakeWordModule einen WakeLock (commit 408d20a).
Sobald Wake-Word erkannt wurde, ruft die JS-Seite OpenWakeWord.stop()
fuer das Mic-Handover an audioService.startRecording() — und der
WakeLock wurde released. Mid-Aufnahme konnte die CPU dann in Doze
gehen, Audio-Chunks erreichten die JS-Bridge nicht zuverlaessig.

Fix: AriaPlaybackService haelt selbst einen PARTIAL_WAKE_LOCK,
solange der Foreground-Service aktiv ist. acquireBackgroundAudio()
in der JS-Seite haelt den Service ueber alle Pipeline-Schritte
(wake → rec → tts → wake) durchgehend — damit ist der WakeLock
ueber die ganze Konversations-Schleife durchgehend aktiv.

Doppelter Schutz (WakeLock auch im OpenWakeWordModule) bleibt drin
als defense in depth — beide haben setReferenceCounted(false), also
keine doppel-buchhaltung, einfach robuster gegen einzeln-failende
acquires.

APK neu bauen erforderlich (native Kotlin-Aenderung).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 20:04:55 +02:00
parent 408d20a087
commit 543ad3c46d
@@ -5,9 +5,11 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.util.Log
import androidx.core.app.NotificationCompat
@@ -32,15 +34,26 @@ class AriaPlaybackService : Service() {
private var currentReason: String = ""
// PARTIAL_WAKE_LOCK haelt die CPU wach solange der Foreground-Service
// aktiv ist. Damit bleibt die JS-Bridge im Doze ansprechbar und die
// gesamte Sprach-Pipeline (Wake → Aufnahme → POST → ARIA → TTS → wieder
// Wake) laeuft durchgehend im Hintergrund. Ein einziger Lock fuer den
// ganzen Foreground-Cycle, nicht pro Sub-Modul.
private var wakeLock: PowerManager.WakeLock? = null
override fun onCreate() {
super.onCreate()
ensureNotificationChannel()
acquireWakeLock()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val reason = intent?.getStringExtra(EXTRA_REASON) ?: ""
currentReason = reason
Log.i(TAG, "Foreground-Service start/update (reason=$reason)")
// Falls der Lock zwischendurch released wurde (z.B. nach onCreate-
// race oder OS-quirk), hier sicherheits-halber erneut anfordern.
acquireWakeLock()
try {
startForeground(NOTIFICATION_ID, buildNotification(reason))
} catch (e: Exception) {
@@ -53,10 +66,36 @@ class AriaPlaybackService : Service() {
}
override fun onDestroy() {
releaseWakeLock()
Log.i(TAG, "Foreground-Service gestoppt")
super.onDestroy()
}
private fun acquireWakeLock() {
if (wakeLock?.isHeld == true) return
try {
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
"AriaCockpit:Pipeline").apply {
setReferenceCounted(false)
acquire(8 * 60 * 60 * 1000L) // 8h Sicherheits-Cap
}
Log.i(TAG, "WakeLock acquired (CPU bleibt wach im Hintergrund)")
} catch (e: Exception) {
Log.w(TAG, "WakeLock acquire fehlgeschlagen: ${e.message}")
}
}
private fun releaseWakeLock() {
try {
wakeLock?.takeIf { it.isHeld }?.release()
if (wakeLock != null) Log.i(TAG, "WakeLock released")
} catch (e: Exception) {
Log.w(TAG, "WakeLock release fehlgeschlagen: ${e.message}")
}
wakeLock = null
}
override fun onBind(intent: Intent?): IBinder? = null
private fun ensureNotificationChannel() {