Files
ARIA-AGENT/android/src/services/gpsTracking.ts
T
duffyduckandClaude Opus 4.8 fd6ba73f59 feat: GPS-Trigger-Kopplung + generatives Cockpit (aria_view)
GPS: near()/entered_near()/left_near()-Watcher schalten das Tracking automatisch an bzw. beim Loeschen des letzten wieder aus; gpsTracking.start() sichert jetzt die Background-Permission. Cockpit-Fundament: neues Brain-Tool present_view emittiert aria_view (Orb + Karten), Bridge/RVS leiten weiter, ariaView.ts haelt die Spec App-seitig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-15 01:21:43 +02:00

237 lines
9.0 KiB
TypeScript

/**
* 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 { Linking, PermissionsAndroid, Platform, ToastAndroid } from 'react-native';
import Geolocation from '@react-native-community/geolocation';
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;
class GpsTrackingService {
private watchId: number | null = null;
private active = false;
private listeners: Set<Listener> = new Set();
// Defensive: nicht zu schnell oeffentlich togglen
private lastChangeAt = 0;
// Letzte bekannte Position — wird vom Heartbeat-Timer alle 60s erneut
// an die Bridge gesendet, sonst veraltet near() im Brain (NEAR_MAX_AGE_SEC
// = 5 min) wenn der User stationaer ist und distanceFilter keine Updates
// mehr triggert.
private lastLat: number | null = null;
private lastLon: number | null = null;
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
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<void> {
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<boolean> {
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<boolean> {
if (this.active) return true;
const ok = await this.ensurePermission();
if (!ok) {
ToastAndroid.show('GPS-Tracking: Berechtigung abgelehnt', ToastAndroid.LONG);
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) {
// Ohne ACCESS_BACKGROUND_LOCATION liefert watchPosition im Hintergrund
// NICHTS (Android 10+) → der Foreground-Service allein bringt nichts, und
// genau der Fall "Ankunft waehrend der Fahrt, Screen aus" faellt durch.
// Deshalb erst die Permission sicherstellen (oeffnet ggf. die Android-
// Settings fuer "Immer erlauben"), DANN den Location-Foreground-Service
// hochziehen — der haelt den Prozess wach, sodass watchPosition + der
// 60s-Heartbeat auch unter Doze weiterlaufen.
const bgOk = await ensureBackgroundLocationPermission();
if (!bgOk) {
console.warn('[gps-track] Background-Permission fehlt — Tracking nur im Vordergrund zuverlaessig');
}
try { await acquireBackgroundAudio('location'); } catch {}
}
try {
this.watchId = Geolocation.watchPosition(
(pos) => {
const lat = pos.coords.latitude;
const lon = pos.coords.longitude;
this.lastLat = lat;
this.lastLon = lon;
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,
);
// Heartbeat: alle 60s die letzte bekannte Position erneut senden.
// Sonst bleibt der Brain-State stale wenn der User stationaer ist
// (distanceFilter blockt watchPosition-Updates) → near()-Watcher
// verwerfen die Position als veraltet (NEAR_MAX_AGE_SEC = 300s).
// Kein neuer GPS-Wakeup, nur Re-Send der letzten Werte → akkufreundlich.
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => {
if (this.lastLat != null && this.lastLon != null) {
rvs.send('location_update' as any, { lat: this.lastLat, lon: this.lastLon });
}
}, 60_000);
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;
}
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
// Location-Foreground-Service-Slot freigeben (falls vorher acquired)
try { releaseBackgroundAudio('location'); } catch {}
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<void> {
if (this.active) this.stop(reason);
else await this.start(reason);
}
}
export default new GpsTrackingService();