Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bac9c26ca | |||
| c758727345 | |||
| cb0e879118 | |||
| ce6f5b551e |
+39
-1
@@ -6,7 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from 'react';
|
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 { NavigationContainer, DefaultTheme } from '@react-navigation/native';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ import ChatScreen from './src/screens/ChatScreen';
|
|||||||
import SettingsScreen from './src/screens/SettingsScreen';
|
import SettingsScreen from './src/screens/SettingsScreen';
|
||||||
import rvs from './src/services/rvs';
|
import rvs from './src/services/rvs';
|
||||||
import { initLogger, installGlobalCrashReporter } from './src/services/logger';
|
import { initLogger, installGlobalCrashReporter } from './src/services/logger';
|
||||||
|
import { acquireBackgroundAudio } from './src/services/backgroundAudio';
|
||||||
|
|
||||||
// --- Navigation ---
|
// --- Navigation ---
|
||||||
|
|
||||||
@@ -61,6 +63,42 @@ const App: React.FC = () => {
|
|||||||
};
|
};
|
||||||
initConnection();
|
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
|
// Beim Beenden: Verbindung sauber trennen
|
||||||
return () => {
|
return () => {
|
||||||
rvs.disconnect();
|
rvs.disconnect();
|
||||||
|
|||||||
@@ -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 10504
|
versionCode 10506
|
||||||
versionName "0.1.5.4"
|
versionName "0.1.5.6"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.5.4",
|
"version": "0.1.5.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -1415,7 +1415,10 @@ const ChatScreen: React.FC = () => {
|
|||||||
// verfuegbar wird (z.B. weil setMessages mitten in der Sequenz die
|
// verfuegbar wird (z.B. weil setMessages mitten in der Sequenz die
|
||||||
// FlatList re-rendert).
|
// FlatList re-rendert).
|
||||||
const scrollRetryCount = useRef<number>(0);
|
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 = () => {
|
const clearPendingScrollRetry = () => {
|
||||||
if (pendingScrollRetry.current) {
|
if (pendingScrollRetry.current) {
|
||||||
clearTimeout(pendingScrollRetry.current);
|
clearTimeout(pendingScrollRetry.current);
|
||||||
@@ -1459,13 +1462,18 @@ const ChatScreen: React.FC = () => {
|
|||||||
// averageItemLength im Failed-Handler nur auf den ersten ~10 Items
|
// averageItemLength im Failed-Handler nur auf den ersten ~10 Items
|
||||||
// und liefert einen voellig falschen Sprung).
|
// und liefert einen voellig falschen Sprung).
|
||||||
// Offset = Summe echter Hoehen (aus itemHeights-Cache, gefuettert per
|
// Offset = Summe echter Hoehen (aus itemHeights-Cache, gefuettert per
|
||||||
// onLayout) + Fallback AVG fuer noch nicht gemessene. Bei „cold start"
|
// onLayout) + dynamischer Fallback aus dem Mittel der bisher
|
||||||
// ist der Cache leer → AVG fuer alle → grob. Beim zweiten Such-Versuch
|
// gemessenen Items. Beim Cold-Start gibt's nur 10 Messungen (die
|
||||||
// sind die Bubbles in der Naehe gemessen → genauer.
|
// 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;
|
let preOffset = 0;
|
||||||
const inv = invertedMessagesRef.current;
|
const inv = invertedMessagesRef.current;
|
||||||
for (let i = 0; i < idx; i++) {
|
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 {
|
try {
|
||||||
flatListRef.current?.scrollToOffset({
|
flatListRef.current?.scrollToOffset({
|
||||||
@@ -1473,9 +1481,10 @@ const ChatScreen: React.FC = () => {
|
|||||||
animated: false,
|
animated: false,
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
// Nach kurzer Render-Pause praezise nachsetzen. 200 ms statt 80 ms —
|
// Nach Render-Pause praezise nachsetzen. 350 ms — bei weiten Spruengen
|
||||||
// bei Cold-Start braucht FlatList laenger fuer das Item-Layout, das
|
// (Pre-Scroll 5000+ px) braucht FlatList Zeit die Items dort zu
|
||||||
// war Stefans „erst beim zweiten Versuch klappt's"-Bug.
|
// mounten und onLayout zu feuern. Zu kurz → averageItemLength im
|
||||||
|
// Failed-Handler basiert noch auf den falschen Items.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
@@ -1483,7 +1492,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
// onScrollToIndexFailed-Handler uebernimmt den Fallback
|
// onScrollToIndexFailed-Handler uebernimmt den Fallback
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 350);
|
||||||
});
|
});
|
||||||
}, [searchIndex, searchMatchIds]);
|
}, [searchIndex, searchMatchIds]);
|
||||||
|
|
||||||
@@ -2411,19 +2420,19 @@ const ChatScreen: React.FC = () => {
|
|||||||
transparent
|
transparent
|
||||||
onRequestClose={() => setThoughtsVisible(false)}
|
onRequestClose={() => setThoughtsVisible(false)}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<View style={{flex:1, backgroundColor:'rgba(0,0,0,0.5)', justifyContent:'flex-end'}}>
|
||||||
style={{flex:1, backgroundColor:'rgba(0,0,0,0.5)', justifyContent:'flex-end'}}
|
{/* Tap-Outside-Bereich oberhalb des Sheets — separater Touchable
|
||||||
activeOpacity={1}
|
damit das Sheet-View NICHT als Responder den FlatList-Scroll
|
||||||
onPress={() => setThoughtsVisible(false)}
|
blockiert. Frueher hatten wir den ganzen Hintergrund als
|
||||||
>
|
TouchableOpacity + inneren View mit onStartShouldSetResponder
|
||||||
{/* View statt TouchableOpacity, sonst konsumiert das die Touch-
|
= das hat alle Touch-Events kassiert. */}
|
||||||
Events und die FlatList laesst sich nicht scrollen.
|
<TouchableOpacity
|
||||||
onStartShouldSetResponder={true} blockt aber die Propagation
|
style={{flex:1}}
|
||||||
an das aeussere TouchableOpacity (close-on-tap-outside). */}
|
activeOpacity={1}
|
||||||
|
onPress={() => setThoughtsVisible(false)}
|
||||||
|
/>
|
||||||
<View
|
<View
|
||||||
style={{height:'60%', backgroundColor:'#0D0D1A', borderTopLeftRadius:16, borderTopRightRadius:16}}
|
style={{height:'60%', backgroundColor:'#0D0D1A', borderTopLeftRadius:16, borderTopRightRadius:16}}
|
||||||
onStartShouldSetResponder={() => true}
|
|
||||||
onResponderTerminationRequest={() => false}
|
|
||||||
>
|
>
|
||||||
{/* Drag-Indicator */}
|
{/* Drag-Indicator */}
|
||||||
<View style={{alignItems:'center', paddingTop:8, paddingBottom:4}}>
|
<View style={{alignItems:'center', paddingTop:8, paddingBottom:4}}>
|
||||||
@@ -2504,7 +2513,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
|
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import {
|
|||||||
} from '../services/audio';
|
} from '../services/audio';
|
||||||
import audioService from '../services/audio';
|
import audioService from '../services/audio';
|
||||||
import gpsTrackingService from '../services/gpsTracking';
|
import gpsTrackingService from '../services/gpsTracking';
|
||||||
|
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 { isVerboseLogging, setVerboseLogging } from '../services/logger';
|
import { isVerboseLogging, setVerboseLogging } from '../services/logger';
|
||||||
@@ -129,6 +130,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 [backgroundMode, setBackgroundMode] = useState(true); // Default an
|
||||||
const [scannerVisible, setScannerVisible] = useState(false);
|
const [scannerVisible, setScannerVisible] = useState(false);
|
||||||
const [logTab, setLogTab] = useState<LogTab>('live');
|
const [logTab, setLogTab] = useState<LogTab>('live');
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
@@ -196,6 +198,10 @@ const SettingsScreen: React.FC = () => {
|
|||||||
AsyncStorage.getItem('aria_gps_enabled').then(saved => {
|
AsyncStorage.getItem('aria_gps_enabled').then(saved => {
|
||||||
if (saved !== null) setGpsEnabled(saved === 'true');
|
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
|
// gpsTrackingService status syncen + auf Aenderungen lauschen
|
||||||
setGpsTracking(gpsTrackingService.isActive());
|
setGpsTracking(gpsTrackingService.isActive());
|
||||||
const offGps = gpsTrackingService.onChange(setGpsTracking);
|
const offGps = gpsTrackingService.onChange(setGpsTracking);
|
||||||
@@ -579,6 +585,37 @@ const SettingsScreen: React.FC = () => {
|
|||||||
AsyncStorage.setItem('aria_gps_enabled', String(value)).catch(() => {});
|
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 ---
|
// --- XTTS Voice ---
|
||||||
|
|
||||||
const selectVoice = useCallback((voiceName: string) => {
|
const selectVoice = useCallback((voiceName: string) => {
|
||||||
@@ -1065,6 +1102,33 @@ const SettingsScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</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) === */}
|
{/* === Spracheingabe (geraetelokal) === */}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Background-Audio: ARIAs TTS, Mic-Aufnahme und Wake-Word-Lauschen sollen
|
* Background-Audio + Hintergrund-Persistenz: ARIAs TTS, Mic-Aufnahme,
|
||||||
* auch bei minimierter App weiterlaufen. Wir starten dafuer einen Foreground-
|
* 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
|
* 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":
|
* Mehrere Komponenten koennen den Service unabhaengig "halten":
|
||||||
* - '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)
|
||||||
|
* - '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
|
* 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).
|
* den hoechstprioren Slot an (tts > rec > wake > background).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NativeModules } from 'react-native';
|
import { NativeModules } from 'react-native';
|
||||||
@@ -23,12 +27,13 @@ interface BackgroundAudioNative {
|
|||||||
|
|
||||||
const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative };
|
const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative };
|
||||||
|
|
||||||
type Slot = 'tts' | 'rec' | 'wake';
|
type Slot = 'tts' | 'rec' | 'wake' | 'background';
|
||||||
|
|
||||||
const slots = new Set<Slot>();
|
const slots = new Set<Slot>();
|
||||||
|
|
||||||
// Prioritaet fuer den Notification-Text — hoechste zuerst.
|
// Prioritaet fuer den Notification-Text — hoechste zuerst. 'background'
|
||||||
const PRIORITY: Slot[] = ['tts', 'rec', 'wake'];
|
// ist die fallback-Anzeige wenn nichts anderes laeuft.
|
||||||
|
const PRIORITY: Slot[] = ['tts', 'rec', 'wake', 'background'];
|
||||||
|
|
||||||
function topReason(): string {
|
function topReason(): string {
|
||||||
for (const s of PRIORITY) {
|
for (const s of PRIORITY) {
|
||||||
|
|||||||
Reference in New Issue
Block a user