Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c758727345 | |||
| cb0e879118 | |||
| ce6f5b551e |
+39
-1
@@ -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();
|
||||
|
||||
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10504
|
||||
versionName "0.1.5.4"
|
||||
versionCode 10505
|
||||
versionName "0.1.5.5"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.5.4",
|
||||
"version": "0.1.5.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -1415,7 +1415,10 @@ const ChatScreen: React.FC = () => {
|
||||
// verfuegbar wird (z.B. weil setMessages mitten in der Sequenz die
|
||||
// FlatList re-rendert).
|
||||
const scrollRetryCount = useRef<number>(0);
|
||||
const MAX_SCROLL_RETRIES = 3;
|
||||
// 6 Retries: bei weiten Spruengen (Suche auf Bubble #150 von Position 0)
|
||||
// kann FlatList mehrere Iterationen brauchen bis die Items in der Naehe
|
||||
// gemessen sind. Vorher 3 = vorzeitig aufgegeben.
|
||||
const MAX_SCROLL_RETRIES = 6;
|
||||
const clearPendingScrollRetry = () => {
|
||||
if (pendingScrollRetry.current) {
|
||||
clearTimeout(pendingScrollRetry.current);
|
||||
@@ -1459,13 +1462,18 @@ const ChatScreen: React.FC = () => {
|
||||
// averageItemLength im Failed-Handler nur auf den ersten ~10 Items
|
||||
// und liefert einen voellig falschen Sprung).
|
||||
// Offset = Summe echter Hoehen (aus itemHeights-Cache, gefuettert per
|
||||
// onLayout) + Fallback AVG fuer noch nicht gemessene. Bei „cold start"
|
||||
// ist der Cache leer → AVG fuer alle → grob. Beim zweiten Such-Versuch
|
||||
// sind die Bubbles in der Naehe gemessen → genauer.
|
||||
// onLayout) + dynamischer Fallback aus dem Mittel der bisher
|
||||
// gemessenen Items. Beim Cold-Start gibt's nur 10 Messungen (die
|
||||
// neuesten unten in der invertierten Liste) — der Mittel daraus ist
|
||||
// immer noch besser als die Pauschal-150.
|
||||
const measured = Array.from(itemHeights.current.values());
|
||||
const dynamicAvg = measured.length >= 5
|
||||
? measured.reduce((a, b) => a + b, 0) / measured.length
|
||||
: AVG_BUBBLE_HEIGHT;
|
||||
let preOffset = 0;
|
||||
const inv = invertedMessagesRef.current;
|
||||
for (let i = 0; i < idx; i++) {
|
||||
preOffset += itemHeights.current.get(inv[i].id) || AVG_BUBBLE_HEIGHT;
|
||||
preOffset += itemHeights.current.get(inv[i].id) || dynamicAvg;
|
||||
}
|
||||
try {
|
||||
flatListRef.current?.scrollToOffset({
|
||||
@@ -1473,9 +1481,10 @@ const ChatScreen: React.FC = () => {
|
||||
animated: false,
|
||||
});
|
||||
} catch {}
|
||||
// Nach kurzer Render-Pause praezise nachsetzen. 200 ms statt 80 ms —
|
||||
// bei Cold-Start braucht FlatList laenger fuer das Item-Layout, das
|
||||
// war Stefans „erst beim zweiten Versuch klappt's"-Bug.
|
||||
// Nach Render-Pause praezise nachsetzen. 350 ms — bei weiten Spruengen
|
||||
// (Pre-Scroll 5000+ px) braucht FlatList Zeit die Items dort zu
|
||||
// mounten und onLayout zu feuern. Zu kurz → averageItemLength im
|
||||
// Failed-Handler basiert noch auf den falschen Items.
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
@@ -1483,7 +1492,7 @@ const ChatScreen: React.FC = () => {
|
||||
} catch {
|
||||
// onScrollToIndexFailed-Handler uebernimmt den Fallback
|
||||
}
|
||||
}, 200);
|
||||
}, 350);
|
||||
});
|
||||
}, [searchIndex, searchMatchIds]);
|
||||
|
||||
@@ -2411,19 +2420,19 @@ const ChatScreen: React.FC = () => {
|
||||
transparent
|
||||
onRequestClose={() => setThoughtsVisible(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{flex:1, backgroundColor:'rgba(0,0,0,0.5)', justifyContent:'flex-end'}}
|
||||
activeOpacity={1}
|
||||
onPress={() => setThoughtsVisible(false)}
|
||||
>
|
||||
{/* View statt TouchableOpacity, sonst konsumiert das die Touch-
|
||||
Events und die FlatList laesst sich nicht scrollen.
|
||||
onStartShouldSetResponder={true} blockt aber die Propagation
|
||||
an das aeussere TouchableOpacity (close-on-tap-outside). */}
|
||||
<View style={{flex:1, backgroundColor:'rgba(0,0,0,0.5)', justifyContent:'flex-end'}}>
|
||||
{/* Tap-Outside-Bereich oberhalb des Sheets — separater Touchable
|
||||
damit das Sheet-View NICHT als Responder den FlatList-Scroll
|
||||
blockiert. Frueher hatten wir den ganzen Hintergrund als
|
||||
TouchableOpacity + inneren View mit onStartShouldSetResponder
|
||||
= das hat alle Touch-Events kassiert. */}
|
||||
<TouchableOpacity
|
||||
style={{flex:1}}
|
||||
activeOpacity={1}
|
||||
onPress={() => setThoughtsVisible(false)}
|
||||
/>
|
||||
<View
|
||||
style={{height:'60%', backgroundColor:'#0D0D1A', borderTopLeftRadius:16, borderTopRightRadius:16}}
|
||||
onStartShouldSetResponder={() => true}
|
||||
onResponderTerminationRequest={() => false}
|
||||
>
|
||||
{/* Drag-Indicator */}
|
||||
<View style={{alignItems:'center', paddingTop:8, paddingBottom:4}}>
|
||||
@@ -2504,7 +2513,7 @@ const ChatScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
|
||||
|
||||
@@ -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<LogTab>('live');
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
@@ -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 = () => {
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* === Hintergrund-Modus === */}
|
||||
<Text style={styles.sectionTitle}>Hintergrund-Modus</Text>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.toggleRow}>
|
||||
<View style={styles.toggleInfo}>
|
||||
<Text style={styles.toggleLabel}>App im Hintergrund weiterlaufen</Text>
|
||||
<Text style={styles.toggleHint}>
|
||||
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.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={backgroundMode}
|
||||
onValueChange={handleBackgroundModeToggle}
|
||||
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
|
||||
thumbColor={backgroundMode ? '#FFFFFF' : '#666680'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>)}
|
||||
|
||||
{/* === Spracheingabe (geraetelokal) === */}
|
||||
|
||||
@@ -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<Slot>();
|
||||
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user