fix(app): PARTIAL_WAKE_LOCK fuer Wake-Word — JS-Bridge bleibt im Hintergrund aktiv

Stefan-Bug-Report: Wake-Word wird im Hintergrund erkannt (Spotify
pausiert sofort), aber der Gong + Aufnahme-Start kommen erst wenn die
App in den Vordergrund geholt wird. Akku-Optimierung war bereits
deaktiviert ("Hintergrund aktiv").

Ursache: Foreground-Service haelt den App-Prozess am Leben + erlaubt
mic-Zugriff via foregroundServiceType=microphone. Aber: ohne expliziten
WakeLock kann die CPU im Doze-Mode (Display aus / Telefon idle) die
Auslieferung von DeviceEvents an die React-Native-JS-Bridge pausieren.
Folge: Native erkennt Wake-Word, ruft emit("WakeWordDetected"), aber
das Event queued sich nur — der JS-Listener (onWakeDetected → start-
Recording + playWakeReadySound) feuert erst beim naechsten JS-Tick,
und der kommt erst beim App-Resume.

Fix:
- AndroidManifest: WAKE_LOCK Permission hinzu (kein User-Prompt noetig,
  ist eine "normal" Permission).
- OpenWakeWordModule.kt: PowerManager.PARTIAL_WAKE_LOCK in start()
  acquired (8h Cap als Sicherheit), in stop() + dispose() released.
  Lock-Tag "AriaCockpit:WakeWordRecord" damit der in adb shell dumpsys
  power sichtbar ist.

Wirkung: solange Wake-Word "armed" ist, bleibt die CPU wach und die
JS-Bridge verarbeitet die Detection-Events live — Gong, Mic-Start,
ARIA-Antwort kommen ohne Foreground-Resume durch.

APK muss neu gebaut werden (native Kotlin-Aenderung).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 20:03:08 +02:00
parent 0756baa2a0
commit 408d20a087
2 changed files with 41 additions and 0 deletions
@@ -17,6 +17,11 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- WAKE_LOCK damit Wake-Word + JS-Bridge auch bei aus-Display und Doze
arbeiten: ohne Lock pausiert Android die CPU, Native-AudioRecord
laeuft weiter aber JS-Bridge frisst die DeviceEvents nicht mehr ->
Wake-Word wird erkannt aber callbacks feuern erst beim App-Resume. -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@@ -4,6 +4,7 @@ import ai.onnxruntime.OnnxTensor
import ai.onnxruntime.OrtEnvironment import ai.onnxruntime.OrtEnvironment
import ai.onnxruntime.OrtSession import ai.onnxruntime.OrtSession
import android.Manifest import android.Manifest
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.AudioFormat import android.media.AudioFormat
import android.media.AudioRecord import android.media.AudioRecord
@@ -11,6 +12,7 @@ import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.AutomaticGainControl import android.media.audiofx.AutomaticGainControl
import android.media.audiofx.NoiseSuppressor import android.media.audiofx.NoiseSuppressor
import android.os.PowerManager
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.facebook.react.bridge.Promise import com.facebook.react.bridge.Promise
@@ -80,6 +82,13 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
private var ns: NoiseSuppressor? = null private var ns: NoiseSuppressor? = null
private var agc: AutomaticGainControl? = null private var agc: AutomaticGainControl? = null
// PARTIAL_WAKE_LOCK damit die CPU bei aus-Display nicht in Doze geht und
// die JS-Bridge die WakeWordDetected-Events live verarbeitet (sonst
// queuen sich die Events nur und werden erst beim App-Foreground
// delivered — Stefan-Beobachtung: "Spotify pausiert, aber Gong/Aufnahme
// kommen erst wenn ich die App nach vorne hole").
private var wakeLock: PowerManager.WakeLock? = null
// Inferenz-State // Inferenz-State
private val melBuffer: ArrayList<FloatArray> = ArrayList(256) // Liste von 32-dim Frames private val melBuffer: ArrayList<FloatArray> = ArrayList(256) // Liste von 32-dim Frames
private var melProcessedIdx: Int = 0 private var melProcessedIdx: Int = 0
@@ -198,6 +207,21 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
running.set(true) running.set(true)
record.startRecording() record.startRecording()
// PARTIAL_WAKE_LOCK greifen damit die CPU nicht in Doze geht und
// die JS-Bridge die emit("WakeWordDetected")-Events live verarbeitet.
// 8h Cap als Sicherheit gegen forgotten-release.
try {
val pm = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
"AriaCockpit:WakeWordRecord").apply {
setReferenceCounted(false)
acquire(8 * 60 * 60 * 1000L)
}
Log.i(TAG, "WakeLock acquired")
} catch (e: Exception) {
Log.w(TAG, "WakeLock acquire fehlgeschlagen: ${e.message}")
}
captureThread = Thread({ captureLoop() }, "OpenWakeWordCapture").apply { captureThread = Thread({ captureLoop() }, "OpenWakeWordCapture").apply {
isDaemon = true isDaemon = true
start() start()
@@ -232,6 +256,7 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
try { audioRecord?.release() } catch (_: Exception) {} try { audioRecord?.release() } catch (_: Exception) {}
audioRecord = null audioRecord = null
releaseAudioEffects() releaseAudioEffects()
releaseWakeLock()
Log.i(TAG, "Lauschen gestoppt") Log.i(TAG, "Lauschen gestoppt")
promise.resolve(true) promise.resolve(true)
} }
@@ -245,10 +270,21 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
try { audioRecord?.release() } catch (_: Exception) {} try { audioRecord?.release() } catch (_: Exception) {}
audioRecord = null audioRecord = null
releaseAudioEffects() releaseAudioEffects()
releaseWakeLock()
disposeSessions() disposeSessions()
promise.resolve(true) promise.resolve(true)
} }
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
}
@ReactMethod @ReactMethod
fun isAvailable(promise: Promise) { fun isAvailable(promise: Promise) {
// Wake-Word ist immer verfuegbar (kein API-Key, alles on-device) // Wake-Word ist immer verfuegbar (kein API-Key, alles on-device)