diff --git a/android/App.tsx b/android/App.tsx index 36a3069..d4b11e5 100644 --- a/android/App.tsx +++ b/android/App.tsx @@ -6,7 +6,8 @@ */ import React, { useEffect } from 'react'; -import { StatusBar, StyleSheet } from 'react-native'; +import { PermissionsAndroid, Platform, StatusBar, StyleSheet } from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import { NavigationContainer, DefaultTheme } from '@react-navigation/native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; @@ -14,6 +15,7 @@ import ChatScreen from './src/screens/ChatScreen'; import SettingsScreen from './src/screens/SettingsScreen'; import rvs from './src/services/rvs'; import { initLogger, installGlobalCrashReporter } from './src/services/logger'; +import { acquireBackgroundAudio } from './src/services/backgroundAudio'; // --- Navigation --- @@ -61,6 +63,42 @@ const App: React.FC = () => { }; initConnection(); + // Hintergrund-Modus: Foreground-Service starten damit JS-Engine + + // WebSocket auch ueberleben wenn die App im Hintergrund ist. + // Trigger-Replies, Reconnects, Timer-Erinnerungen kommen sonst nicht + // durch weil Android nach ~30s die JS-Engine pausiert. + // + // Default an, kann in Settings → Hintergrund-Modus deaktiviert werden. + // Braucht POST_NOTIFICATIONS Permission ab Android 13. + const initBackground = async () => { + const setting = await AsyncStorage.getItem('aria_background_mode'); + if (setting === 'false') { + console.log('[App] Hintergrund-Modus deaktiviert (Settings)'); + return; + } + // Permission fuer die persistente Notification + if (Platform.OS === 'android' && Platform.Version >= 33) { + try { + await PermissionsAndroid.request( + 'android.permission.POST_NOTIFICATIONS' as any, + { + title: 'Hintergrund-Modus', + message: 'ARIA zeigt eine Notification damit Trigger und Reconnects auch laufen wenn die App im Hintergrund ist.', + buttonPositive: 'Erlauben', + buttonNegative: 'Spaeter', + }, + ); + } catch {} + } + try { + await acquireBackgroundAudio('background'); + console.log('[App] Hintergrund-Modus aktiv'); + } catch (err: any) { + console.warn('[App] Hintergrund-Modus konnte nicht starten:', err?.message || err); + } + }; + initBackground(); + // Beim Beenden: Verbindung sauber trennen return () => { rvs.disconnect(); diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index ece205f..1a1402e 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -53,6 +53,7 @@ import { } from '../services/audio'; import audioService from '../services/audio'; import gpsTrackingService from '../services/gpsTracking'; +import { acquireBackgroundAudio, releaseBackgroundAudio } from '../services/backgroundAudio'; import MemoryBrowser from '../components/MemoryBrowser'; import TriggerBrowser from '../components/TriggerBrowser'; import { isVerboseLogging, setVerboseLogging } from '../services/logger'; @@ -129,6 +130,7 @@ const SettingsScreen: React.FC = () => { const [currentMode, setCurrentMode] = useState('normal'); const [gpsEnabled, setGpsEnabled] = useState(false); const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive()); + const [backgroundMode, setBackgroundMode] = useState(true); // Default an const [scannerVisible, setScannerVisible] = useState(false); const [logTab, setLogTab] = useState('live'); const [logs, setLogs] = useState([]); @@ -196,6 +198,10 @@ const SettingsScreen: React.FC = () => { AsyncStorage.getItem('aria_gps_enabled').then(saved => { if (saved !== null) setGpsEnabled(saved === 'true'); }); + AsyncStorage.getItem('aria_background_mode').then(saved => { + // Default ist an — nur explicit 'false' deaktiviert + setBackgroundMode(saved !== 'false'); + }); // gpsTrackingService status syncen + auf Aenderungen lauschen setGpsTracking(gpsTrackingService.isActive()); const offGps = gpsTrackingService.onChange(setGpsTracking); @@ -579,6 +585,37 @@ const SettingsScreen: React.FC = () => { AsyncStorage.setItem('aria_gps_enabled', String(value)).catch(() => {}); }, []); + // --- Hintergrund-Modus Toggle --- + + const handleBackgroundModeToggle = useCallback(async (value: boolean) => { + setBackgroundMode(value); + AsyncStorage.setItem('aria_background_mode', String(value)).catch(() => {}); + try { + if (value) { + // Permission fuer Notification (Android 13+) — sonst sieht der User + // den Hintergrund-Modus nicht und wundert sich + if (Platform.OS === 'android' && Platform.Version >= 33) { + await PermissionsAndroid.request( + 'android.permission.POST_NOTIFICATIONS' as any, + { + title: 'Hintergrund-Modus', + message: 'ARIA zeigt eine Notification damit die App im Hintergrund laufen darf.', + buttonPositive: 'Erlauben', + buttonNegative: 'Spaeter', + }, + ); + } + await acquireBackgroundAudio('background'); + ToastAndroid.show('Hintergrund-Modus aktiv', ToastAndroid.SHORT); + } else { + await releaseBackgroundAudio('background'); + ToastAndroid.show('Hintergrund-Modus aus', ToastAndroid.SHORT); + } + } catch (err: any) { + console.warn('[Settings] Background-Toggle gescheitert:', err?.message || err); + } + }, []); + // --- XTTS Voice --- const selectVoice = useCallback((voiceName: string) => { @@ -1065,6 +1102,33 @@ const SettingsScreen: React.FC = () => { /> + + {/* === Hintergrund-Modus === */} + Hintergrund-Modus + + + + App im Hintergrund weiterlaufen + + Haelt die Verbindung zu ARIA auch dann offen wenn die App minimiert + ist. Sonst pausiert Android nach ~30s die JS-Engine und Timer-/Watcher- + Trigger kommen nicht durch. Notification "ARIA aktiv" bleibt sichtbar + waehrend der Modus laeuft (das ist Android-Vorschrift fuer Foreground- + Services). Akku-Mehrverbrauch minimal solange ARIA nichts tut. + {'\n\n'} + Wenn nach Akku-Optimierung Trigger trotzdem nicht durchkommen: + Android-Einstellungen → Apps → ARIA Cockpit → Akku → "Uneingeschraenkt" + setzen. + + + + + )} {/* === Spracheingabe (geraetelokal) === */} diff --git a/android/src/services/backgroundAudio.ts b/android/src/services/backgroundAudio.ts index a5bd62c..a826c61 100644 --- a/android/src/services/backgroundAudio.ts +++ b/android/src/services/backgroundAudio.ts @@ -1,17 +1,21 @@ /** - * Background-Audio: ARIAs TTS, Mic-Aufnahme und Wake-Word-Lauschen sollen - * auch bei minimierter App weiterlaufen. Wir starten dafuer einen Foreground- + * 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 waehrend irgendein Audio-Slot aktiv ist. + * 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) + * - '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). + * den hoechstprioren Slot an (tts > rec > wake > background). */ import { NativeModules } from 'react-native'; @@ -23,12 +27,13 @@ interface BackgroundAudioNative { const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative }; -type Slot = 'tts' | 'rec' | 'wake'; +type Slot = 'tts' | 'rec' | 'wake' | 'background'; const slots = new Set(); -// Prioritaet fuer den Notification-Text — hoechste zuerst. -const PRIORITY: Slot[] = ['tts', 'rec', 'wake']; +// 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) {