feat(audio): TTS pausiert bei Anruf + Conversation-Focus haelt Spotify durchgehend gepaust
Bug 1a — Anruf-Pause: Neues PhoneCallModule.kt nutzt TelephonyCallback (API 31+) bzw. PhoneStateListener (Pre-12) um auf RINGING/OFFHOOK/IDLE zu reagieren. Bei Klingeln/Gespraech ruft phoneCall.ts → audioService.haltAllPlayback, ARIA verstummt sofort. READ_PHONE_STATE Permission wird beim ersten Start angefragt; ohne Permission failt der Listener leise. Bug 1b — Spotify-Resume: AudioFocus wird jetzt an den Conversation-Lifecycle gekoppelt statt an einzelne Streams. Solange wakeWordState 'conversing' ist, blockt acquireConversationFocus() jeden per-Stream-Release. Erst beim Wechsel auf 'armed'/'off' darf der Focus tatsaechlich freigegeben werden. Verhindert das "Spotify kommt nach 10s wieder hoch"-Phaenomen auch ueber Render-Pausen + zwischen mehreren ARIA-Antworten hinweg. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* PhoneCall-Service — pausiert die TTS-Wiedergabe wenn das Telefon klingelt
|
||||
* oder ein Anruf laeuft. Native-Bindung an PhoneCallModule.kt.
|
||||
*
|
||||
* Bei "ringing" oder "offhook" wird audioService.haltAllPlayback() gerufen —
|
||||
* ARIA verstummt sofort. Nach dem Auflegen passiert nichts automatisch
|
||||
* (Audio kommt nicht zurueck), der User muesste die Antwort manuell
|
||||
* nochmal anfordern (Play-Button auf der Nachricht).
|
||||
*
|
||||
* Permission READ_PHONE_STATE muss vom Nutzer einmalig erteilt werden —
|
||||
* wenn nicht, failed start() leise und der Rest funktioniert wie bisher.
|
||||
*/
|
||||
|
||||
import {
|
||||
NativeEventEmitter,
|
||||
NativeModules,
|
||||
PermissionsAndroid,
|
||||
Platform,
|
||||
ToastAndroid,
|
||||
} from 'react-native';
|
||||
import audioService from './audio';
|
||||
|
||||
interface PhoneCallNative {
|
||||
start(): Promise<boolean>;
|
||||
stop(): Promise<boolean>;
|
||||
}
|
||||
|
||||
const { PhoneCall } = NativeModules as { PhoneCall?: PhoneCallNative };
|
||||
|
||||
type PhoneState = 'idle' | 'ringing' | 'offhook';
|
||||
|
||||
class PhoneCallService {
|
||||
private started: boolean = false;
|
||||
private subscription: { remove: () => void } | null = null;
|
||||
private lastState: PhoneState = 'idle';
|
||||
|
||||
async start(): Promise<boolean> {
|
||||
if (this.started || !PhoneCall) return false;
|
||||
if (Platform.OS !== 'android') return false;
|
||||
|
||||
// Runtime-Permission holen (nur einmal noetig)
|
||||
try {
|
||||
const granted = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.READ_PHONE_STATE,
|
||||
{
|
||||
title: 'ARIA Cockpit — Anruf-Erkennung',
|
||||
message: 'Damit ARIA bei einem eingehenden Anruf nicht weiterredet, '
|
||||
+ 'darf die App den Anruf-Status sehen (Klingeln/Aktiv/Aufgelegt). '
|
||||
+ 'Es werden keine Anrufdaten gelesen oder gespeichert.',
|
||||
buttonPositive: 'Erlauben',
|
||||
buttonNegative: 'Spaeter',
|
||||
},
|
||||
);
|
||||
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
|
||||
console.warn('[PhoneCall] READ_PHONE_STATE Permission abgelehnt');
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[PhoneCall] Permission-Anfrage gescheitert', err);
|
||||
}
|
||||
|
||||
try {
|
||||
const ok = await PhoneCall.start();
|
||||
if (!ok) {
|
||||
console.warn('[PhoneCall] Native start() lieferte false (Permission?)');
|
||||
return false;
|
||||
}
|
||||
const emitter = new NativeEventEmitter(NativeModules.PhoneCall as any);
|
||||
this.subscription = emitter.addListener('PhoneCallStateChanged', (e: { state: PhoneState }) => {
|
||||
this._onStateChanged(e.state);
|
||||
});
|
||||
this.started = true;
|
||||
console.log('[PhoneCall] Listener aktiv');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.warn('[PhoneCall] start gescheitert:', err?.message || err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.started || !PhoneCall) return;
|
||||
try {
|
||||
this.subscription?.remove();
|
||||
this.subscription = null;
|
||||
await PhoneCall.stop();
|
||||
} catch {}
|
||||
this.started = false;
|
||||
this.lastState = 'idle';
|
||||
}
|
||||
|
||||
private _onStateChanged(state: PhoneState): void {
|
||||
if (state === this.lastState) return;
|
||||
console.log('[PhoneCall] State: %s → %s', this.lastState, state);
|
||||
this.lastState = state;
|
||||
if (state === 'ringing' || state === 'offhook') {
|
||||
audioService.haltAllPlayback(`Telefon-State: ${state}`);
|
||||
ToastAndroid.show(
|
||||
state === 'ringing' ? 'Anruf — ARIA pausiert' : 'Im Gespraech — ARIA pausiert',
|
||||
ToastAndroid.SHORT,
|
||||
);
|
||||
}
|
||||
// idle: nichts automatisch — User soll nichts unbeabsichtigt re-triggern
|
||||
}
|
||||
}
|
||||
|
||||
const phoneCallService = new PhoneCallService();
|
||||
export default phoneCallService;
|
||||
Reference in New Issue
Block a user