cb0e879118
Bisher pausierte Android nach ~30s im Hintergrund die JS-Engine.
WebSocket schlief ein, Trigger-Replies vom Brain kamen nicht durch,
Timer-Erinnerungen feuerten in der App nicht obwohl im Brain
ausgeloest. Nach laengerer Hintergrund-Pause warf Android den
Prozess ganz raus → beim Wiedereroeffnen Cold-Start, sah aus wie Crash.
Loesung: Foreground-Service mit persistenter Notification — die ist
ohnehin schon da fuer TTS/Mic-Aktivitaet (`AriaPlaybackService`).
Wir erweitern das Slot-System um einen `background`-Slot der dauerhaft
aktiv ist (Settings-Toggle, default an). Notification zeigt "ARIA aktiv
— Hintergrund-Modus" wenn nichts spezifisches laeuft, escaliert zu
"ARIA spricht/hoert" bei TTS/Mic. Tap → App.
Drei Dateien:
- services/backgroundAudio.ts: 'background' als 4. Slot (niedrigste
Prio, Fallback-Notification). Bestehende tts/rec/wake unveraendert.
- App.tsx: beim Start `acquireBackgroundAudio('background')` aufrufen
wenn Settings nicht explizit deaktiviert. Plus POST_NOTIFICATIONS-
Permission-Request (Android 13+).
- screens/SettingsScreen.tsx: neuer Toggle in Allgemein-Section.
Plus Hinweis auf Android-Akku-Optimierung-Whitelist falls trotzdem
was klemmt (manche Hersteller-ROMs killen aggressiv).
AndroidManifest unveraendert — foregroundServiceType="mediaPlayback|
microphone" deckt unseren Use-Case ab (ARIA spielt regelmaessig TTS
ab, was den Type rechtfertigt). Service stoppt sich selbst wenn alle
Slots leer sind, das passiert nur wenn der User in Settings den
Hintergrund-Modus deaktiviert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.8 KiB
TypeScript
82 lines
2.8 KiB
TypeScript
/**
|
|
* Background-Audio + Hintergrund-Persistenz: ARIAs TTS, Mic-Aufnahme,
|
|
* Wake-Word-Lauschen UND der allgemeine Hintergrund-Modus laufen
|
|
* weiter wenn die App minimiert ist. Wir starten dafuer einen Foreground-
|
|
* Service mit foregroundServiceType=mediaPlayback|microphone, der eine
|
|
* persistente Notification zeigt solange irgendein Slot aktiv ist.
|
|
*
|
|
* Mehrere Komponenten koennen den Service unabhaengig "halten":
|
|
* - 'tts' : ARIA spricht
|
|
* - 'rec' : Aufnahme laeuft
|
|
* - 'wake' : Wake-Word lauscht passiv (Ohr aktiv)
|
|
* - 'background' : Persistenter Hintergrund-Modus (Settings-Toggle).
|
|
* Haelt JS-Engine + WebSocket auch ohne Audio am Leben
|
|
* → Trigger-Replies, Reconnects, Push-Reaktionen.
|
|
*
|
|
* Solange mindestens ein Slot aktiv ist, laeuft der Service. Wenn alle
|
|
* Slots leer sind, wird er gestoppt. Der Notification-Text passt sich an
|
|
* den hoechstprioren Slot an (tts > rec > wake > background).
|
|
*/
|
|
|
|
import { NativeModules } from 'react-native';
|
|
|
|
interface BackgroundAudioNative {
|
|
start(reason: string): Promise<boolean>;
|
|
stop(): Promise<boolean>;
|
|
}
|
|
|
|
const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative };
|
|
|
|
type Slot = 'tts' | 'rec' | 'wake' | 'background';
|
|
|
|
const slots = new Set<Slot>();
|
|
|
|
// Prioritaet fuer den Notification-Text — hoechste zuerst. 'background'
|
|
// ist die fallback-Anzeige wenn nichts anderes laeuft.
|
|
const PRIORITY: Slot[] = ['tts', 'rec', 'wake', 'background'];
|
|
|
|
function topReason(): string {
|
|
for (const s of PRIORITY) {
|
|
if (slots.has(s)) return s;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
async function applyState(): Promise<void> {
|
|
if (!BackgroundAudio) return;
|
|
if (slots.size === 0) {
|
|
try { await BackgroundAudio.stop(); } catch {}
|
|
console.log('[BackgroundAudio] Service gestoppt (keine Slots)');
|
|
return;
|
|
}
|
|
const reason = topReason();
|
|
try {
|
|
await BackgroundAudio.start(reason);
|
|
console.log('[BackgroundAudio] Service aktiv (slot=%s, slots=%s)',
|
|
reason, [...slots].join('+'));
|
|
} catch (err: any) {
|
|
console.warn('[BackgroundAudio] start fehlgeschlagen:', err?.message || err);
|
|
}
|
|
}
|
|
|
|
export async function acquireBackgroundAudio(slot: Slot): Promise<void> {
|
|
if (slots.has(slot)) return;
|
|
slots.add(slot);
|
|
await applyState();
|
|
}
|
|
|
|
export async function releaseBackgroundAudio(slot: Slot): Promise<void> {
|
|
if (!slots.has(slot)) return;
|
|
slots.delete(slot);
|
|
await applyState();
|
|
}
|
|
|
|
export function backgroundAudioActive(): boolean {
|
|
return slots.size > 0;
|
|
}
|
|
|
|
// --- Legacy API (nur tts-Slot) — fuer Aufruf-Sites die noch nichts vom Slot-
|
|
// system wissen. Mappt auf den 'tts'-Slot. ---
|
|
export const startBackgroundAudio = () => acquireBackgroundAudio('tts');
|
|
export const stopBackgroundAudio = () => releaseBackgroundAudio('tts');
|