/** * GPS-Tracking-Service. * * Wenn aktiv: pushed alle paar Sekunden die aktuelle Position als * `location_update {lat, lon}` an den RVS-Server, damit Brain-Watcher * mit `near()`-Conditions etwas zum Vergleichen haben. * * Default: AUS. Wird entweder vom User manuell in Settings angeschaltet * oder von ARIA via location_tracking-RVS-Message (Brain-Tool * `request_location_tracking`). * * Energie-Schutz: distanceFilter 30m, interval 15s. Echte Fahrt-Updates * (Geschwindigkeit) kommen sauber durch, stationaer wird kaum gesendet. */ import AsyncStorage from '@react-native-async-storage/async-storage'; import { PermissionsAndroid, Platform, ToastAndroid } from 'react-native'; import Geolocation from '@react-native-community/geolocation'; import rvs from './rvs'; type Listener = (active: boolean) => void; class GpsTrackingService { private watchId: number | null = null; private active = false; private listeners: Set = new Set(); // Defensive: nicht zu schnell oeffentlich togglen private lastChangeAt = 0; isActive(): boolean { return this.active; } onChange(cb: Listener): () => void { this.listeners.add(cb); return () => { this.listeners.delete(cb); }; } private notify() { for (const cb of this.listeners) { try { cb(this.active); } catch {} } } /** Beim App-Start: gespeicherten Zustand wiederherstellen (Default off). */ async restoreFromStorage(): Promise { try { const v = await AsyncStorage.getItem('aria_gps_tracking'); if (v === 'true') { console.log('[gps-track] Restore: war an, starte wieder'); this.start('Beim Start wiederhergestellt'); } } catch {} } private async ensurePermission(): Promise { if (Platform.OS !== 'android') return true; try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, { title: 'GPS-Tracking', message: 'ARIA braucht laufende Standort-Updates damit GPS-Watcher (Blitzer-Warner, near()) funktionieren.', buttonPositive: 'Erlauben', buttonNegative: 'Abbrechen', }, ); return granted === PermissionsAndroid.RESULTS.GRANTED; } catch (e) { console.warn('[gps-track] Permission-Fehler:', e); return false; } } async start(reason: string = ''): Promise { if (this.active) return true; const ok = await this.ensurePermission(); if (!ok) { ToastAndroid.show('GPS-Tracking: Berechtigung abgelehnt', ToastAndroid.LONG); return false; } try { this.watchId = Geolocation.watchPosition( (pos) => { const lat = pos.coords.latitude; const lon = pos.coords.longitude; rvs.send('location_update' as any, { lat, lon }); }, (err) => { console.warn('[gps-track] watchPosition error:', err?.code, err?.message); }, { enableHighAccuracy: true, distanceFilter: 30, // erst senden wenn 30m gewandert interval: 15000, // (Android) gewuenschte Frequenz fastestInterval: 10000, // (Android) max Frequenz } as any, ); this.active = true; this.lastChangeAt = Date.now(); this.notify(); AsyncStorage.setItem('aria_gps_tracking', 'true').catch(() => {}); ToastAndroid.show( reason ? `GPS-Tracking aktiv (${reason})` : 'GPS-Tracking aktiv', ToastAndroid.SHORT, ); console.log('[gps-track] gestartet', reason ? `(${reason})` : ''); return true; } catch (e: any) { console.warn('[gps-track] start fehlgeschlagen:', e?.message); return false; } } stop(reason: string = ''): void { if (!this.active) return; if (this.watchId !== null) { try { Geolocation.clearWatch(this.watchId); } catch {} this.watchId = null; } this.active = false; this.lastChangeAt = Date.now(); this.notify(); AsyncStorage.setItem('aria_gps_tracking', 'false').catch(() => {}); ToastAndroid.show( reason ? `GPS-Tracking aus (${reason})` : 'GPS-Tracking aus', ToastAndroid.SHORT, ); console.log('[gps-track] gestoppt', reason ? `(${reason})` : ''); } async toggle(reason: string = ''): Promise { if (this.active) this.stop(reason); else await this.start(reason); } } export default new GpsTrackingService();