Files
ARIA-AGENT/android/App.tsx
T
duffyduck ad87c807de fix(app): App-Reconnect nach Hintergrund — Sticky-Fallback, Zombie-WS, AppState-Hook
Stefan musste seit der HTTPS-Umstellung nach jedem Hintergrund-Rueckkehr
manuell auf "Verbinden" tippen, meist 3x bis es ging. Gleiche Bug-Klasse
wie auf der Bridge davor (Sticky-Fallback), plus zwei App-spezifische
Symptome.

Drei Ursachen:

1. usingTLSFallback klebt: einmal nach onerror auf true gesetzt, blieb
   es bei allen folgenden Reconnects → App versuchte ws://...:443 gegen
   den TLS-only Caddy → HTTP 400 → endlos. Reset war NUR im manuellen
   connect(), nicht in onclose oder scheduleReconnect.
   Fix: in onclose `usingTLSFallback = false` damit der naechste
   Reconnect wieder primary (wss://) probiert.

2. Zombie-WebSocket: Android kann den TCP-Socket im Background still
   killen, der JS-State zeigt aber noch readyState === OPEN. Stefans
   manueller "Verbinden"-Klick rief connect() → "Bereits verbunden"
   No-Op statt sich neu aufzubauen.
   Fix: connect(force=true) optional, bestehendes WS-Objekt wird hart
   geschlossen (mit onclose=null gegen Doppel-Reconnect) bevor neuer
   Aufbau startet.

3. Keine aktive Reconnect-Sequence bei Foreground-Resume: App war
   abhaengig von onclose-Events die bei Zombie-WS nicht zwingend
   feuern.
   Fix: AppState-Listener in App.tsx, bei background → active
   automatischer rvs.connect(true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:09:30 +02:00

213 lines
7.0 KiB
TypeScript

/**
* ARIA Cockpit - Haupteinstiegspunkt
*
* Stefans primaere Schnittstelle zu ARIA.
* Bottom-Tab-Navigation mit Chat und Einstellungen.
*/
import React, { useEffect } from 'react';
import { AppState, AppStateStatus, 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';
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';
import gpsTrackingService from './src/services/gpsTracking';
// --- Navigation ---
const Tab = createBottomTabNavigator();
// Dunkles Theme fuer die gesamte App
const DarkTheme = {
...DefaultTheme,
dark: true,
colors: {
...DefaultTheme.colors,
primary: '#0096FF',
background: '#0D0D1A',
card: '#12122A',
text: '#FFFFFF',
border: '#1E1E2E',
notification: '#FF3B30',
},
};
// Tab-Icons (Text-basiert, kein Icon-Paket noetig)
const TAB_ICONS: Record<string, { active: string; inactive: string }> = {
Chat: { active: '\uD83D\uDCAC', inactive: '\uD83D\uDCAC' },
Einstellungen: { active: '\u2699\uFE0F', inactive: '\u2699\uFE0F' },
};
// --- App ---
const App: React.FC = () => {
// Beim Start: gespeicherte RVS-Konfiguration laden und verbinden
useEffect(() => {
// Verbose-Logging-Setting laden BEVOR andere Module loslegen.
// initLogger ist async aber blockt nichts — solange er noch laueft,
// loggen wir normal (Default an), danach respektiert console.log das Setting.
initLogger().catch(() => {});
// Crash-Reporter installieren — ungefangene JS-Errors landen via RVS
// bei der Bridge (sichtbar in /shared/logs/app.log + Diagnostic-API)
installGlobalCrashReporter();
const initConnection = async () => {
const config = await rvs.loadConfig();
if (config) {
rvs.setConfig(config);
rvs.connect();
}
};
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();
// GPS-Tracking-Status aus AsyncStorage wiederherstellen (war
// bisher nur an SettingsScreen-Mount gekoppelt; wenn Stefan
// direkt im Chat startete blieb GPS aus bis er Settings oeffnete).
gpsTrackingService.restoreFromStorage().catch((err) => {
console.warn('[App] GPS-Tracking restore fehlgeschlagen:', err?.message || err);
});
// AppState-Listener: nach Hintergrund-Rueckkehr aktiv die WS-
// Verbindung neu aufbauen. Hintergrund: Android kann den TCP-Socket
// im Background killen, JS-State zeigt aber noch OPEN → Stefan musste
// manuell in Settings auf "Verbinden" tippen, oft mehrfach. Mit dem
// force-Reconnect bei "active" greift das automatisch.
let lastAppState: AppStateStatus = AppState.currentState;
const appStateSub = AppState.addEventListener('change', (next) => {
const wasBg = lastAppState !== 'active';
lastAppState = next;
if (next === 'active' && wasBg) {
console.log('[App] Foreground-Resume — force-reconnect zum RVS');
try { rvs.connect(true); } catch (e: any) {
console.warn('[App] force-reconnect fehlgeschlagen:', e?.message || e);
}
}
});
// Beim Beenden: Verbindung sauber trennen
return () => {
appStateSub.remove();
rvs.disconnect();
};
}, []);
return (
<>
<StatusBar barStyle="light-content" backgroundColor="#0D0D1A" />
<NavigationContainer theme={DarkTheme}>
<Tab.Navigator
screenOptions={({ route }) => ({
headerStyle: styles.header,
headerTitleStyle: styles.headerTitle,
headerTintColor: '#FFFFFF',
tabBarStyle: styles.tabBar,
tabBarActiveTintColor: '#0096FF',
tabBarInactiveTintColor: '#555570',
tabBarIcon: ({ focused }) => {
const icons = TAB_ICONS[route.name];
return (
<React.Fragment>
{/* Emoji als Icon */}
{React.createElement(
require('react-native').Text,
{
style: {
fontSize: 22,
opacity: focused ? 1 : 0.5,
},
},
icons ? (focused ? icons.active : icons.inactive) : '?',
)}
</React.Fragment>
);
},
})}
>
<Tab.Screen
name="Chat"
component={ChatScreen}
options={{
title: 'ARIA Chat',
headerTitle: 'ARIA Cockpit',
}}
/>
<Tab.Screen
name="Einstellungen"
component={SettingsScreen}
options={{
title: 'Einstellungen',
}}
/>
</Tab.Navigator>
</NavigationContainer>
</>
);
};
// --- Styles ---
const styles = StyleSheet.create({
header: {
backgroundColor: '#12122A',
elevation: 0,
shadowOpacity: 0,
borderBottomWidth: 1,
borderBottomColor: '#1E1E2E',
},
headerTitle: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '700',
},
tabBar: {
backgroundColor: '#12122A',
borderTopColor: '#1E1E2E',
borderTopWidth: 1,
height: 60,
paddingBottom: 6,
paddingTop: 4,
},
});
export default App;