Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2475ffef6 | |||
| 98982fea2f | |||
| 356f8b3171 | |||
| b4115bb345 | |||
| 02cac99ef9 | |||
| 2940ce0075 | |||
| d78b668e31 | |||
| a9115699db | |||
| f2bfd4bbc6 |
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 10605
|
versionCode 10609
|
||||||
versionName "0.1.6.5"
|
versionName "0.1.6.9"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,20 @@
|
|||||||
<!-- Optional: GPS-Position der Frage anhaengen (nur wenn User in Settings aktiviert) -->
|
<!-- Optional: GPS-Position der Frage anhaengen (nur wenn User in Settings aktiviert) -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<!-- Background-Location ist OPT-IN (Settings → GPS auch im Hintergrund).
|
||||||
|
Muss vom User explizit in Android-Einstellungen auf "Immer erlauben"
|
||||||
|
gesetzt werden — kann nicht ueber den normalen Permission-Dialog
|
||||||
|
angefordert werden (Android 10+). Default: aus. -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
<!-- Foreground-Service damit TTS auch bei minimierter App weiterlaeuft.
|
<!-- Foreground-Service damit TTS auch bei minimierter App weiterlaeuft.
|
||||||
FOREGROUND_SERVICE_MICROPHONE ist Pflicht ab Android 14 wenn der
|
FOREGROUND_SERVICE_MICROPHONE ist Pflicht ab Android 14 wenn der
|
||||||
Service waehrend des Backgrounds aufs Mikro zugreift (Wake-Word,
|
Service waehrend des Backgrounds aufs Mikro zugreift (Wake-Word,
|
||||||
Aufnahme im Gespraechsmodus). -->
|
Aufnahme im Gespraechsmodus). LOCATION wird nur aktiv wenn der
|
||||||
|
User Background-GPS in Settings einschaltet. -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<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.FOREGROUND_SERVICE_LOCATION" />
|
||||||
<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
|
<!-- WAKE_LOCK damit Wake-Word + JS-Bridge auch bei aus-Display und Doze
|
||||||
arbeiten: ohne Lock pausiert Android die CPU, Native-AudioRecord
|
arbeiten: ohne Lock pausiert Android die CPU, Native-AudioRecord
|
||||||
@@ -57,6 +64,6 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".AriaPlaybackService"
|
android:name=".AriaPlaybackService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="mediaPlayback|microphone" />
|
android:foregroundServiceType="mediaPlayback|microphone|location" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.6.5",
|
"version": "0.1.6.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -1270,9 +1270,11 @@ const ChatScreen: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubWake = wakeWordService.onWakeWord(async () => {
|
const unsubWake = wakeWordService.onWakeWord(async () => {
|
||||||
console.log('[Chat] Gespraechsmodus — starte Auto-Aufnahme');
|
console.log('[Chat] Gespraechsmodus — starte Auto-Aufnahme');
|
||||||
|
import('../services/logger').then(m => m.reportAppDebug('wake.cb', 'callback fired, calling startRecording')).catch(()=>{});
|
||||||
// Conversation-Window: User hat X Sekunden um anzufangen, sonst Konversation aus
|
// Conversation-Window: User hat X Sekunden um anzufangen, sonst Konversation aus
|
||||||
const windowMs = await loadConvWindowMs();
|
const windowMs = await loadConvWindowMs();
|
||||||
const started = await audioService.startRecording(true, windowMs);
|
const started = await audioService.startRecording(true, windowMs);
|
||||||
|
import('../services/logger').then(m => m.reportAppDebug('wake.cb', `startRecording returned ${started}`)).catch(()=>{});
|
||||||
if (started) {
|
if (started) {
|
||||||
// Erst JETZT signalisieren dass das Mikro wirklich offen ist —
|
// Erst JETZT signalisieren dass das Mikro wirklich offen ist —
|
||||||
// vorher war's noch in der Init-Phase. So weiss der User exakt
|
// vorher war's noch in der Init-Phase. So weiss der User exakt
|
||||||
@@ -1280,6 +1282,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
// ueber Settings → Wake-Word abschaltbar.
|
// ueber Settings → Wake-Word abschaltbar.
|
||||||
ToastAndroid.show('🎤 Mikro offen — sprich jetzt', ToastAndroid.SHORT);
|
ToastAndroid.show('🎤 Mikro offen — sprich jetzt', ToastAndroid.SHORT);
|
||||||
playWakeReadySound().catch(() => {});
|
playWakeReadySound().catch(() => {});
|
||||||
|
import('../services/logger').then(m => m.reportAppDebug('wake.cb', 'gong played + recording started')).catch(()=>{});
|
||||||
} else {
|
} else {
|
||||||
// Mikrofon nicht verfuegbar, naechsten Versuch
|
// Mikrofon nicht verfuegbar, naechsten Versuch
|
||||||
wakeWordService.resume();
|
wakeWordService.resume();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
PermissionsAndroid,
|
PermissionsAndroid,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
|
DeviceEventEmitter,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
@@ -52,13 +53,17 @@ import {
|
|||||||
TTS_SPEED_STORAGE_KEY,
|
TTS_SPEED_STORAGE_KEY,
|
||||||
} from '../services/audio';
|
} from '../services/audio';
|
||||||
import audioService from '../services/audio';
|
import audioService from '../services/audio';
|
||||||
import gpsTrackingService from '../services/gpsTracking';
|
import gpsTrackingService, {
|
||||||
|
isBackgroundGpsEnabled,
|
||||||
|
setBackgroundGpsEnabled,
|
||||||
|
ensureBackgroundLocationPermission,
|
||||||
|
} from '../services/gpsTracking';
|
||||||
import { acquireBackgroundAudio, releaseBackgroundAudio } from '../services/backgroundAudio';
|
import { acquireBackgroundAudio, releaseBackgroundAudio } from '../services/backgroundAudio';
|
||||||
import MemoryBrowser from '../components/MemoryBrowser';
|
import MemoryBrowser from '../components/MemoryBrowser';
|
||||||
import TriggerBrowser from '../components/TriggerBrowser';
|
import TriggerBrowser from '../components/TriggerBrowser';
|
||||||
import SkillBrowser from '../components/SkillBrowser';
|
import SkillBrowser from '../components/SkillBrowser';
|
||||||
import OAuthBrowser from '../components/OAuthBrowser';
|
import OAuthBrowser from '../components/OAuthBrowser';
|
||||||
import { isVerboseLogging, setVerboseLogging } from '../services/logger';
|
import { isVerboseLogging, setVerboseLogging, isDebugLogsToBridge, setDebugLogsToBridge, APP_LOG_EVENT } from '../services/logger';
|
||||||
import {
|
import {
|
||||||
isWakeReadySoundEnabled,
|
isWakeReadySoundEnabled,
|
||||||
setWakeReadySoundEnabled,
|
setWakeReadySoundEnabled,
|
||||||
@@ -134,6 +139,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [currentMode, setCurrentMode] = useState('normal');
|
const [currentMode, setCurrentMode] = useState('normal');
|
||||||
const [gpsEnabled, setGpsEnabled] = useState(false);
|
const [gpsEnabled, setGpsEnabled] = useState(false);
|
||||||
const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive());
|
const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive());
|
||||||
|
const [bgGpsEnabled, setBgGpsEnabled] = useState(false);
|
||||||
const [backgroundMode, setBackgroundMode] = useState(true); // Default an
|
const [backgroundMode, setBackgroundMode] = useState(true); // Default an
|
||||||
const [showSystemHints, setShowSystemHints] = useState(false); // Default aus
|
const [showSystemHints, setShowSystemHints] = useState(false); // Default aus
|
||||||
const [scannerVisible, setScannerVisible] = useState(false);
|
const [scannerVisible, setScannerVisible] = useState(false);
|
||||||
@@ -155,6 +161,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [apkCacheInfo, setApkCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
|
const [apkCacheInfo, setApkCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
|
||||||
const [ttsCacheInfo, setTtsCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
|
const [ttsCacheInfo, setTtsCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
|
||||||
const [verboseLogging, setVerboseLoggingState] = useState<boolean>(isVerboseLogging());
|
const [verboseLogging, setVerboseLoggingState] = useState<boolean>(isVerboseLogging());
|
||||||
|
const [debugLogsToBridge, setDebugLogsToBridgeState] = useState<boolean>(isDebugLogsToBridge());
|
||||||
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
|
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
|
||||||
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
|
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
|
||||||
const [wakeStatus, setWakeStatus] = useState<string>('');
|
const [wakeStatus, setWakeStatus] = useState<string>('');
|
||||||
@@ -216,6 +223,8 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const offGps = gpsTrackingService.onChange(setGpsTracking);
|
const offGps = gpsTrackingService.onChange(setGpsTracking);
|
||||||
// Persistierten Status wiederherstellen (war Tracking beim letzten Mal an?)
|
// Persistierten Status wiederherstellen (war Tracking beim letzten Mal an?)
|
||||||
gpsTrackingService.restoreFromStorage().catch(() => {});
|
gpsTrackingService.restoreFromStorage().catch(() => {});
|
||||||
|
// Background-GPS-Toggle initial laden
|
||||||
|
isBackgroundGpsEnabled().then(setBgGpsEnabled).catch(() => {});
|
||||||
AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => {
|
AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => {
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
const n = parseFloat(saved);
|
const n = parseFloat(saved);
|
||||||
@@ -380,6 +389,19 @@ const SettingsScreen: React.FC = () => {
|
|||||||
setConnLog(prev => [...prev.slice(-99), entry]);
|
setConnLog(prev => [...prev.slice(-99), entry]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Lokale App-Logs (reportAppDebug/Error) im Live-Logs-Tab anzeigen
|
||||||
|
// — damit Stefan ohne curl direkt in der App sieht was passiert.
|
||||||
|
const localLogSub = DeviceEventEmitter.addListener(APP_LOG_EVENT, (e: any) => {
|
||||||
|
const entry: LogEntry = {
|
||||||
|
id: `applog_${e.ts || Date.now()}_${logIdCounter++}`,
|
||||||
|
timestamp: e.ts || Date.now(),
|
||||||
|
source: e.scope || 'app',
|
||||||
|
message: e.message || '',
|
||||||
|
level: e.level || 'info',
|
||||||
|
};
|
||||||
|
setLogs(prev => [...prev.slice(-200), entry]);
|
||||||
|
});
|
||||||
|
|
||||||
const unsubMessage = rvs.onMessage((message: RVSMessage) => {
|
const unsubMessage = rvs.onMessage((message: RVSMessage) => {
|
||||||
if (message.type === 'log') {
|
if (message.type === 'log') {
|
||||||
const entry: LogEntry = {
|
const entry: LogEntry = {
|
||||||
@@ -515,6 +537,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
unsubState();
|
unsubState();
|
||||||
unsubMessage();
|
unsubMessage();
|
||||||
unsubLog();
|
unsubLog();
|
||||||
|
localLogSub.remove();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -1117,6 +1140,52 @@ const SettingsScreen: React.FC = () => {
|
|||||||
thumbColor={gpsTracking ? '#FFFFFF' : '#666680'}
|
thumbColor={gpsTracking ? '#FFFFFF' : '#666680'}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Background-GPS opt-in — Default AUS. Braucht ACCESS_BACKGROUND_LOCATION
|
||||||
|
(User muss in Android-Settings 'Immer erlauben' aktivieren). */}
|
||||||
|
<View style={[styles.toggleRow, {marginTop: 12, borderTopWidth: 1, borderTopColor: '#1E1E2E', paddingTop: 12}]}>
|
||||||
|
<View style={styles.toggleInfo}>
|
||||||
|
<Text style={styles.toggleLabel}>GPS auch im Hintergrund</Text>
|
||||||
|
<Text style={styles.toggleHint}>
|
||||||
|
Damit ARIA auch unterwegs deine aktuelle Position kennt wenn die
|
||||||
|
App im Hintergrund ist (Auto, Handy-Tasche). Standard: aus.
|
||||||
|
{'\n\n'}
|
||||||
|
Android verlangt fuer Background-GPS, dass du in den
|
||||||
|
System-Einstellungen unter Standort "Immer erlauben" auswaehlst.
|
||||||
|
Beim Aktivieren wird Android-Settings geoeffnet falls noetig.
|
||||||
|
{'\n\n'}
|
||||||
|
Akku-Verbrauch: ~3-5% mehr pro Tag durch dauerhaftes Polling.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={bgGpsEnabled}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
if (v) {
|
||||||
|
const ok = await ensureBackgroundLocationPermission();
|
||||||
|
if (!ok) {
|
||||||
|
// User muss in Android-Settings auf "Immer erlauben" — Toggle
|
||||||
|
// bleibt aus bis er zurueckkommt und nochmal tippt.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await setBackgroundGpsEnabled(true);
|
||||||
|
setBgGpsEnabled(true);
|
||||||
|
// Wenn Tracking bereits laeuft: neu starten damit der
|
||||||
|
// Foreground-Service jetzt mit location-Slot kommt
|
||||||
|
if (gpsTrackingService.isActive()) {
|
||||||
|
gpsTrackingService.stop('bg-toggle');
|
||||||
|
gpsTrackingService.start('bg-aktiviert').catch(() => {});
|
||||||
|
}
|
||||||
|
ToastAndroid.show('Background-GPS aktiviert', ToastAndroid.SHORT);
|
||||||
|
} else {
|
||||||
|
await setBackgroundGpsEnabled(false);
|
||||||
|
setBgGpsEnabled(false);
|
||||||
|
ToastAndroid.show('Background-GPS aus — nur noch Foreground', ToastAndroid.SHORT);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
trackColor={{ false: '#2A2A3E', true: '#FF3B30' }}
|
||||||
|
thumbColor={bgGpsEnabled ? '#FFFFFF' : '#666680'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* === Bubble-Anzeige === */}
|
{/* === Bubble-Anzeige === */}
|
||||||
@@ -1863,6 +1932,27 @@ const SettingsScreen: React.FC = () => {
|
|||||||
Warnungen und Fehler bleiben immer aktiv. Bei Bedarf einschalten zum
|
Warnungen und Fehler bleiben immer aktiv. Bei Bedarf einschalten zum
|
||||||
Debuggen via adb logcat.
|
Debuggen via adb logcat.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* Debug-Logs an Bridge: scharf nur wenn aktiv gebraucht */}
|
||||||
|
<View style={[styles.toggleRow, {marginTop: 12, borderTopWidth: 1, borderTopColor: '#1E1E2E', paddingTop: 12}]}>
|
||||||
|
<Text style={styles.toggleLabel}>Debug-Logs an Bridge</Text>
|
||||||
|
<Switch
|
||||||
|
value={debugLogsToBridge}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setDebugLogsToBridge(v);
|
||||||
|
setDebugLogsToBridgeState(v);
|
||||||
|
}}
|
||||||
|
trackColor={{ false: '#3A3A52', true: '#FF9500' }}
|
||||||
|
thumbColor={debugLogsToBridge ? '#FFFFFF' : '#666680'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.toggleHint}>
|
||||||
|
Schickt detaillierte Diagnose-Logs (Wake-Word-Pipeline, Audio-Focus,
|
||||||
|
Background-Service) per RVS an die Bridge — abrufbar via
|
||||||
|
`curl /api/app-log?lines=N` ohne ADB. Default AUS damit kein
|
||||||
|
unnoetiger Traffic + Disk-Schreiben. Crash-Reports (Errors) gehen
|
||||||
|
IMMER, dieser Toggle betrifft nur Info-Logs.
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
|
|||||||
@@ -9,13 +9,14 @@
|
|||||||
* - 'tts' : ARIA spricht
|
* - 'tts' : ARIA spricht
|
||||||
* - 'rec' : Aufnahme laeuft
|
* - 'rec' : Aufnahme laeuft
|
||||||
* - 'wake' : Wake-Word lauscht passiv (Ohr aktiv)
|
* - 'wake' : Wake-Word lauscht passiv (Ohr aktiv)
|
||||||
|
* - 'location' : Background-GPS-Tracking (opt-in in Settings)
|
||||||
* - 'background' : Persistenter Hintergrund-Modus (Settings-Toggle).
|
* - 'background' : Persistenter Hintergrund-Modus (Settings-Toggle).
|
||||||
* Haelt JS-Engine + WebSocket auch ohne Audio am Leben
|
* Haelt JS-Engine + WebSocket auch ohne Audio am Leben
|
||||||
* → Trigger-Replies, Reconnects, Push-Reaktionen.
|
* → Trigger-Replies, Reconnects, Push-Reaktionen.
|
||||||
*
|
*
|
||||||
* Solange mindestens ein Slot aktiv ist, laeuft der Service. Wenn alle
|
* Solange mindestens ein Slot aktiv ist, laeuft der Service. Wenn alle
|
||||||
* Slots leer sind, wird er gestoppt. Der Notification-Text passt sich an
|
* Slots leer sind, wird er gestoppt. Der Notification-Text passt sich an
|
||||||
* den hoechstprioren Slot an (tts > rec > wake > background).
|
* den hoechstprioren Slot an (tts > rec > wake > location > background).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NativeModules } from 'react-native';
|
import { NativeModules } from 'react-native';
|
||||||
@@ -27,13 +28,13 @@ interface BackgroundAudioNative {
|
|||||||
|
|
||||||
const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative };
|
const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative };
|
||||||
|
|
||||||
type Slot = 'tts' | 'rec' | 'wake' | 'background';
|
type Slot = 'tts' | 'rec' | 'wake' | 'location' | 'background';
|
||||||
|
|
||||||
const slots = new Set<Slot>();
|
const slots = new Set<Slot>();
|
||||||
|
|
||||||
// Prioritaet fuer den Notification-Text — hoechste zuerst. 'background'
|
// Prioritaet fuer den Notification-Text — hoechste zuerst. 'background'
|
||||||
// ist die fallback-Anzeige wenn nichts anderes laeuft.
|
// ist die fallback-Anzeige wenn nichts anderes laeuft.
|
||||||
const PRIORITY: Slot[] = ['tts', 'rec', 'wake', 'background'];
|
const PRIORITY: Slot[] = ['tts', 'rec', 'wake', 'location', 'background'];
|
||||||
|
|
||||||
function topReason(): string {
|
function topReason(): string {
|
||||||
for (const s of PRIORITY) {
|
for (const s of PRIORITY) {
|
||||||
@@ -47,6 +48,7 @@ async function applyState(): Promise<void> {
|
|||||||
if (slots.size === 0) {
|
if (slots.size === 0) {
|
||||||
try { await BackgroundAudio.stop(); } catch {}
|
try { await BackgroundAudio.stop(); } catch {}
|
||||||
console.log('[BackgroundAudio] Service gestoppt (keine Slots)');
|
console.log('[BackgroundAudio] Service gestoppt (keine Slots)');
|
||||||
|
import('./logger').then(m => m.reportAppDebug('bg.stop', 'service stopped')).catch(()=>{});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reason = topReason();
|
const reason = topReason();
|
||||||
@@ -54,8 +56,10 @@ async function applyState(): Promise<void> {
|
|||||||
await BackgroundAudio.start(reason);
|
await BackgroundAudio.start(reason);
|
||||||
console.log('[BackgroundAudio] Service aktiv (slot=%s, slots=%s)',
|
console.log('[BackgroundAudio] Service aktiv (slot=%s, slots=%s)',
|
||||||
reason, [...slots].join('+'));
|
reason, [...slots].join('+'));
|
||||||
|
import('./logger').then(m => m.reportAppDebug('bg.start', `slot=${reason} all=[${[...slots].join(',')}]`)).catch(()=>{});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.warn('[BackgroundAudio] start fehlgeschlagen:', err?.message || err);
|
console.warn('[BackgroundAudio] start fehlgeschlagen:', err?.message || err);
|
||||||
|
import('./logger').then(m => m.reportAppDebug('bg.start.fail', err?.message || String(err))).catch(()=>{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,62 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { PermissionsAndroid, Platform, ToastAndroid } from 'react-native';
|
import { Linking, PermissionsAndroid, Platform, ToastAndroid } from 'react-native';
|
||||||
import Geolocation from '@react-native-community/geolocation';
|
import Geolocation from '@react-native-community/geolocation';
|
||||||
import rvs from './rvs';
|
import rvs from './rvs';
|
||||||
|
import { acquireBackgroundAudio, releaseBackgroundAudio } from './backgroundAudio';
|
||||||
|
|
||||||
|
// Opt-in Background-GPS — Settings-Toggle "GPS auch im Hintergrund".
|
||||||
|
// Default AUS. Wenn AN: ACCESS_BACKGROUND_LOCATION-Permission noetig
|
||||||
|
// (kann nicht ueber Standard-Dialog angefordert werden, User muss in
|
||||||
|
// Android-Settings auf "Immer erlauben" gehen) + ForegroundService mit
|
||||||
|
// foregroundServiceType=location wird hochgezogen.
|
||||||
|
export const BG_GPS_STORAGE_KEY = 'aria_gps_background_enabled';
|
||||||
|
|
||||||
|
export async function isBackgroundGpsEnabled(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const v = await AsyncStorage.getItem(BG_GPS_STORAGE_KEY);
|
||||||
|
return v === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setBackgroundGpsEnabled(enabled: boolean): Promise<void> {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(BG_GPS_STORAGE_KEY, String(enabled));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prueft ob ACCESS_BACKGROUND_LOCATION gewaehrt ist und oeffnet sonst die
|
||||||
|
* Android-App-Settings damit der User "Immer erlauben" auswaehlen kann.
|
||||||
|
* Returns true wenn permission ok, false wenn User Settings oeffnen muss. */
|
||||||
|
export async function ensureBackgroundLocationPermission(): Promise<boolean> {
|
||||||
|
if (Platform.OS !== 'android') return true;
|
||||||
|
try {
|
||||||
|
const granted = await PermissionsAndroid.check(
|
||||||
|
'android.permission.ACCESS_BACKGROUND_LOCATION' as any,
|
||||||
|
);
|
||||||
|
if (granted) return true;
|
||||||
|
// Erst FINE_LOCATION anfordern falls noch nicht da
|
||||||
|
const fine = await PermissionsAndroid.request(
|
||||||
|
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
||||||
|
);
|
||||||
|
if (fine !== PermissionsAndroid.RESULTS.GRANTED) return false;
|
||||||
|
// Ab Android 10+ kann BACKGROUND_LOCATION NICHT ueber den normalen
|
||||||
|
// PermissionsAndroid.request abgefragt werden — User muss in Settings
|
||||||
|
// auf "Immer erlauben" wechseln. Wir oeffnen die App-Settings-Seite.
|
||||||
|
ToastAndroid.show(
|
||||||
|
'Bitte in Android-Einstellungen unter Standort "Immer erlauben" auswaehlen',
|
||||||
|
ToastAndroid.LONG,
|
||||||
|
);
|
||||||
|
Linking.openSettings();
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[gps-track] BG-Permission-Check fehlgeschlagen:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Listener = (active: boolean) => void;
|
type Listener = (active: boolean) => void;
|
||||||
|
|
||||||
@@ -86,6 +139,14 @@ class GpsTrackingService {
|
|||||||
ToastAndroid.show('GPS-Tracking: Berechtigung abgelehnt', ToastAndroid.LONG);
|
ToastAndroid.show('GPS-Tracking: Berechtigung abgelehnt', ToastAndroid.LONG);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Background-GPS opt-in: wenn aktiv, ForegroundService mit type=location
|
||||||
|
// hochziehen. Brauche ACCESS_BACKGROUND_LOCATION (User muss in Android-
|
||||||
|
// Settings 'Immer erlauben' aktivieren). Wenn die fehlt, watchPosition
|
||||||
|
// liefert im Hintergrund keine Updates (nur Heartbeat sendet alte Werte).
|
||||||
|
const bgEnabled = await isBackgroundGpsEnabled();
|
||||||
|
if (bgEnabled) {
|
||||||
|
try { await acquireBackgroundAudio('location'); } catch {}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
this.watchId = Geolocation.watchPosition(
|
this.watchId = Geolocation.watchPosition(
|
||||||
(pos) => {
|
(pos) => {
|
||||||
@@ -142,6 +203,8 @@ class GpsTrackingService {
|
|||||||
clearInterval(this.heartbeatTimer);
|
clearInterval(this.heartbeatTimer);
|
||||||
this.heartbeatTimer = null;
|
this.heartbeatTimer = null;
|
||||||
}
|
}
|
||||||
|
// Location-Foreground-Service-Slot freigeben (falls vorher acquired)
|
||||||
|
try { releaseBackgroundAudio('location'); } catch {}
|
||||||
this.active = false;
|
this.active = false;
|
||||||
this.lastChangeAt = Date.now();
|
this.lastChangeAt = Date.now();
|
||||||
this.notify();
|
this.notify();
|
||||||
|
|||||||
@@ -7,10 +7,28 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { Platform } from 'react-native';
|
import { Platform, DeviceEventEmitter } from 'react-native';
|
||||||
import rvs from './rvs';
|
import rvs from './rvs';
|
||||||
|
|
||||||
|
// Lokales Event damit die SettingsScreen Live Logs / Events Tabs
|
||||||
|
// auch das sehen was die App SELBST loggt (reportAppDebug/Error).
|
||||||
|
// Bisher gingen die nur via RVS an die Bridge. Lokal sichtbar = Mama-
|
||||||
|
// tauglich Debug ohne curl.
|
||||||
|
export const APP_LOG_EVENT = 'AriaLocalAppLog';
|
||||||
|
|
||||||
|
interface LocalLogEntry {
|
||||||
|
ts: number;
|
||||||
|
level: 'info' | 'warn' | 'error';
|
||||||
|
scope: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
|
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
|
||||||
|
// Eigener Toggle fuer Debug-Logs die ueber RVS an die Bridge gehen
|
||||||
|
// (/shared/logs/app.log → Diagnostic /api/app-log). Damit der Default-User
|
||||||
|
// nicht stuendlich Traffic + Disk-Schreiben hat, dieser ist DEFAULT AUS.
|
||||||
|
// Stefan schaltet's nur ein wenn er ein konkretes Problem debuggen muss.
|
||||||
|
export const DEBUG_LOGS_TO_BRIDGE_KEY = 'aria_debug_logs_to_bridge';
|
||||||
|
|
||||||
// Original-console.log retten, damit wir die Wrapper jederzeit wieder
|
// Original-console.log retten, damit wir die Wrapper jederzeit wieder
|
||||||
// "scharf" stellen koennen (sonst waere ein Toggle-an nach -aus tot).
|
// "scharf" stellen koennen (sonst waere ein Toggle-an nach -aus tot).
|
||||||
@@ -18,6 +36,7 @@ const originalLog = console.log.bind(console);
|
|||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
let _verbose = true;
|
let _verbose = true;
|
||||||
|
let _debugLogsToBridge = false;
|
||||||
|
|
||||||
function applyState(): void {
|
function applyState(): void {
|
||||||
console.log = _verbose ? originalLog : noop;
|
console.log = _verbose ? originalLog : noop;
|
||||||
@@ -29,6 +48,10 @@ export async function initLogger(): Promise<void> {
|
|||||||
const v = await AsyncStorage.getItem(VERBOSE_LOGGING_KEY);
|
const v = await AsyncStorage.getItem(VERBOSE_LOGGING_KEY);
|
||||||
_verbose = v !== 'false'; // default: true
|
_verbose = v !== 'false'; // default: true
|
||||||
} catch {}
|
} catch {}
|
||||||
|
try {
|
||||||
|
const d = await AsyncStorage.getItem(DEBUG_LOGS_TO_BRIDGE_KEY);
|
||||||
|
_debugLogsToBridge = d === 'true'; // default: false
|
||||||
|
} catch {}
|
||||||
applyState();
|
applyState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +65,15 @@ export function setVerboseLogging(verbose: boolean): void {
|
|||||||
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {});
|
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDebugLogsToBridge(): boolean {
|
||||||
|
return _debugLogsToBridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDebugLogsToBridge(enabled: boolean): void {
|
||||||
|
_debugLogsToBridge = enabled;
|
||||||
|
AsyncStorage.setItem(DEBUG_LOGS_TO_BRIDGE_KEY, String(enabled)).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
// ─── App-Crash-Reporting via RVS ────────────────────────────────────
|
// ─── App-Crash-Reporting via RVS ────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Wenn die App crasht — egal ob React-Render-Fehler (ErrorBoundary) oder
|
// Wenn die App crasht — egal ob React-Render-Fehler (ErrorBoundary) oder
|
||||||
@@ -61,9 +93,10 @@ let _reportingInstalled = false;
|
|||||||
|
|
||||||
/** Schickt einen App-Fehler via RVS an die Bridge. */
|
/** Schickt einen App-Fehler via RVS an die Bridge. */
|
||||||
export function reportAppError(ev: AppErrorEvent): void {
|
export function reportAppError(ev: AppErrorEvent): void {
|
||||||
|
const ts = Date.now();
|
||||||
try {
|
try {
|
||||||
rvs.send('app_log' as any, {
|
rvs.send('app_log' as any, {
|
||||||
ts: Date.now(),
|
ts,
|
||||||
platform: Platform.OS,
|
platform: Platform.OS,
|
||||||
level: ev.level || 'error',
|
level: ev.level || 'error',
|
||||||
scope: ev.scope,
|
scope: ev.scope,
|
||||||
@@ -73,11 +106,49 @@ export function reportAppError(ev: AppErrorEvent): void {
|
|||||||
} catch {
|
} catch {
|
||||||
// RVS noch nicht connected — Fehler geht im console weiter.
|
// RVS noch nicht connected — Fehler geht im console weiter.
|
||||||
}
|
}
|
||||||
|
// Lokal in den App-Logs-Tab emitten — Errors gehen IMMER durch
|
||||||
|
// (unabhaengig vom Debug-Toggle).
|
||||||
|
try {
|
||||||
|
const entry: LocalLogEntry = {
|
||||||
|
ts, level: ev.level || 'error', scope: ev.scope, message: ev.message,
|
||||||
|
};
|
||||||
|
DeviceEventEmitter.emit(APP_LOG_EVENT, entry);
|
||||||
|
} catch {}
|
||||||
// Plus lokal: console.error, damit Stefan's adb (wenn doch mal verfuegbar)
|
// Plus lokal: console.error, damit Stefan's adb (wenn doch mal verfuegbar)
|
||||||
// den Crash sieht.
|
// den Crash sieht.
|
||||||
console.error(`[app-error scope=${ev.scope}]`, ev.message, '\n', ev.stack || '');
|
console.error(`[app-error scope=${ev.scope}]`, ev.message, '\n', ev.stack || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Schickt eine Debug-/Info-Message via RVS an die Bridge. Landet ebenfalls
|
||||||
|
* in /shared/logs/app.log — abrufbar via `curl /api/app-log?lines=N`.
|
||||||
|
* Im Gegensatz zu reportAppError: keine Stacktrace, level=info, kein
|
||||||
|
* console.error. Fuer Live-Diagnose im Hintergrund wenn ADB nicht da ist.
|
||||||
|
*
|
||||||
|
* Nur aktiv wenn Settings → Protokoll → Debug-Logs an Bridge AN ist.
|
||||||
|
* Default aus damit Mama-Modus keine Disk-Schreiblast hat. Error-Reports
|
||||||
|
* (reportAppError) gehen weiterhin IMMER durch. */
|
||||||
|
export function reportAppDebug(scope: string, message: string): void {
|
||||||
|
if (!_debugLogsToBridge) return;
|
||||||
|
const ts = Date.now();
|
||||||
|
const trimmed = String(message).slice(0, 2000);
|
||||||
|
try {
|
||||||
|
rvs.send('app_log' as any, {
|
||||||
|
ts,
|
||||||
|
platform: Platform.OS,
|
||||||
|
level: 'info',
|
||||||
|
scope,
|
||||||
|
message: trimmed,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
// Plus lokal in den App-Logs-Tab emitten — damit Stefan in der App
|
||||||
|
// selbst (Settings → Protokoll → Live Logs) sieht was passiert,
|
||||||
|
// ohne curl gegen Bridge.
|
||||||
|
try {
|
||||||
|
const entry: LocalLogEntry = { ts, level: 'info', scope, message: trimmed };
|
||||||
|
DeviceEventEmitter.emit(APP_LOG_EVENT, entry);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
/** Installiert einen globalen JS-Error-Handler der ungefangene Errors via
|
/** Installiert einen globalen JS-Error-Handler der ungefangene Errors via
|
||||||
* RVS an die Bridge schickt. Beim App-Start aufrufen. */
|
* RVS an die Bridge schickt. Beim App-Start aufrufen. */
|
||||||
export function installGlobalCrashReporter(): void {
|
export function installGlobalCrashReporter(): void {
|
||||||
|
|||||||
@@ -179,6 +179,8 @@ class WakeWordService {
|
|||||||
try {
|
try {
|
||||||
await OpenWakeWord.start();
|
await OpenWakeWord.start();
|
||||||
console.log('[WakeWord] armed — warte auf "%s"', this.keyword);
|
console.log('[WakeWord] armed — warte auf "%s"', this.keyword);
|
||||||
|
// Debug-Log via RVS damit wir auch ohne ADB sehen wann es greift
|
||||||
|
import('./logger').then(m => m.reportAppDebug('wake.start', `armed, keyword=${this.keyword}`)).catch(()=>{});
|
||||||
ToastAndroid.show(`Lausche auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
|
ToastAndroid.show(`Lausche auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
|
||||||
this.setState('armed');
|
this.setState('armed');
|
||||||
return true;
|
return true;
|
||||||
@@ -236,15 +238,24 @@ class WakeWordService {
|
|||||||
}
|
}
|
||||||
console.log('[WakeWord] Wake-Word "%s" erkannt! (state=%s, barge=%s)',
|
console.log('[WakeWord] Wake-Word "%s" erkannt! (state=%s, barge=%s)',
|
||||||
this.keyword, this.state, this.bargeListening);
|
this.keyword, this.state, this.bargeListening);
|
||||||
|
import('./logger').then(m => m.reportAppDebug('wake.detect',
|
||||||
|
`keyword=${this.keyword} state=${this.state} barge=${this.bargeListening}`)).catch(()=>{});
|
||||||
this.lastTriggerAt = now;
|
this.lastTriggerAt = now;
|
||||||
if (this.nativeReady && OpenWakeWord) {
|
if (this.nativeReady && OpenWakeWord) {
|
||||||
try { await OpenWakeWord.stop(); } catch {}
|
try {
|
||||||
|
await OpenWakeWord.stop();
|
||||||
|
import('./logger').then(m => m.reportAppDebug('wake.detect', 'native stop ok')).catch(()=>{});
|
||||||
|
} catch (e: any) {
|
||||||
|
import('./logger').then(m => m.reportAppDebug('wake.detect', `native stop FAIL ${e?.message}`)).catch(()=>{});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.bargeListening = false;
|
this.bargeListening = false;
|
||||||
// Wenn wir bereits in 'conversing' sind und der Trigger waehrend ARIAs TTS
|
// Wenn wir bereits in 'conversing' sind und der Trigger waehrend ARIAs TTS
|
||||||
// kam (Barge-In via Wake-Word), feuern wir einen separaten Callback damit
|
// kam (Barge-In via Wake-Word), feuern wir einen separaten Callback damit
|
||||||
// ChatScreen das TTS abbrechen + neue Aufnahme starten kann. Sonst normal.
|
// ChatScreen das TTS abbrechen + neue Aufnahme starten kann. Sonst normal.
|
||||||
if (this.state === 'conversing') {
|
if (this.state === 'conversing') {
|
||||||
|
import('./logger').then(m => m.reportAppDebug('wake.detect',
|
||||||
|
`barge path: cbs=${this.bargeCallbacks.length}`)).catch(()=>{});
|
||||||
this.bargeCallbacks.forEach(cb => {
|
this.bargeCallbacks.forEach(cb => {
|
||||||
try { cb(); } catch (e) { console.warn('[WakeWord] barge cb err:', e); }
|
try { cb(); } catch (e) { console.warn('[WakeWord] barge cb err:', e); }
|
||||||
});
|
});
|
||||||
@@ -252,7 +263,11 @@ class WakeWordService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState('conversing');
|
this.setState('conversing');
|
||||||
|
import('./logger').then(m => m.reportAppDebug('wake.detect',
|
||||||
|
`state→conversing, wakeCallbacks.length=${this.wakeCallbacks.length}, scheduling 200ms timeout`)).catch(()=>{});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
import('./logger').then(m => m.reportAppDebug('wake.detect',
|
||||||
|
`timeout fired, state=${this.state}, cbs=${this.wakeCallbacks.length}`)).catch(()=>{});
|
||||||
if (this.state === 'conversing') {
|
if (this.state === 'conversing') {
|
||||||
this.wakeCallbacks.forEach(cb => cb());
|
this.wakeCallbacks.forEach(cb => cb());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user