diff --git a/app/app.json b/app/app.json index 361913f..27cdffe 100644 --- a/app/app.json +++ b/app/app.json @@ -3,14 +3,14 @@ "name": "BigBrainParking", "slug": "bigbrainparking", "scheme": "bigbrainparking", - "version": "0.6.4", + "version": "0.6.3", "orientation": "portrait", "userInterfaceStyle": "automatic", "newArchEnabled": true, "icon": "./assets/icon.png", "android": { "package": "top.mowden.bigbrainparking", - "versionCode": 25, + "versionCode": 24, "edgeToEdgeEnabled": true, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", diff --git a/app/src/features/location/useLocation.ts b/app/src/features/location/useLocation.ts index c546634..43eb230 100644 --- a/app/src/features/location/useLocation.ts +++ b/app/src/features/location/useLocation.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import * as Location from 'expo-location'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -7,20 +7,6 @@ export interface Coords { longitude: number; } -export interface UseLocationOptions { - /** - * Re-read the OS fix this often, in ms. Omit (or 0) for a single fix at mount. - * A fix goes stale as soon as you drive a block, so any screen that shows - * "where am I" for more than a moment wants this. - */ - intervalMs?: number; - /** - * Poll only while true. Callers pass screen focus AND app foreground: polling - * a map nobody is looking at spends battery on an answer no one reads. - */ - active?: boolean; -} - const LAST_LOC_KEY = 'ps_last_location'; /** Persist the most recent fix so the "near my last location" button works cold. */ @@ -35,54 +21,32 @@ export async function getLastKnownSavedLocation(): Promise { /** * Foreground location. On GrapheneOS this uses the OS location provider directly * (no Google Play Services). We prefer a fast last-known fix, then refine. - * - * `updatedAt` is when `coords` was actually read, so callers can tell a fresh fix - * from one that has been sitting there since the screen opened. */ -export function useLocation({ intervalMs = 0, active = true }: UseLocationOptions = {}) { +export function useLocation() { const [coords, setCoords] = useState(null); - const [updatedAt, setUpdatedAt] = useState(0); const [granted, setGranted] = useState(null); const [error, setError] = useState(null); - // Read inside refresh() without making it a dependency — refresh is the - // interval's callback, and a changing identity would restart the timer on - // every fix, so it would never actually reach the interval. - const haveFix = useRef(false); - const permitted = useRef(false); const refresh = useCallback(async () => { try { - if (!permitted.current) { - const { status } = await Location.requestForegroundPermissionsAsync(); - const ok = status === 'granted'; - permitted.current = ok; - setGranted(ok); - if (!ok) { - setError('Location permission denied.'); - return null; - } + const { status } = await Location.requestForegroundPermissionsAsync(); + const ok = status === 'granted'; + setGranted(ok); + if (!ok) { + setError('Location permission denied.'); + return null; } - // Only worth it before we have anything to show: on a later poll the - // last-known fix is usually older than the one we already hold, and - // publishing it would make the dot jump backwards. - if (!haveFix.current) { - const last = await Location.getLastKnownPositionAsync(); - if (last) { - const c = { latitude: last.coords.latitude, longitude: last.coords.longitude }; - haveFix.current = true; - setCoords(c); - setUpdatedAt(Date.now()); - void saveLastLocation(c); - } + const last = await Location.getLastKnownPositionAsync(); + if (last) { + const c = { latitude: last.coords.latitude, longitude: last.coords.longitude }; + setCoords(c); + void saveLastLocation(c); } const cur = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced, }); const c = { latitude: cur.coords.latitude, longitude: cur.coords.longitude }; - haveFix.current = true; setCoords(c); - setUpdatedAt(Date.now()); - setError(null); void saveLastLocation(c); return c; } catch (e: any) { @@ -92,19 +56,8 @@ export function useLocation({ intervalMs = 0, active = true }: UseLocationOption }, []); useEffect(() => { - if (!active) return; - // Denied is denied — polling it every 30s just burns wake-ups to be told no. - if (granted === false) return; - // Re-activating (screen focused, app foregrounded) is exactly when the held - // fix is most likely to be stale, so read one straight away rather than - // waiting out a whole interval. void refresh(); - if (!intervalMs) return; - const id = setInterval(() => { - void refresh(); - }, intervalMs); - return () => clearInterval(id); - }, [active, granted, intervalMs, refresh]); + }, [refresh]); - return { coords, updatedAt, granted, error, refresh }; + return { coords, granted, error, refresh }; } diff --git a/app/src/screens/MapScreen.tsx b/app/src/screens/MapScreen.tsx index bc6acef..e1cce18 100644 --- a/app/src/screens/MapScreen.tsx +++ b/app/src/screens/MapScreen.tsx @@ -1,13 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - ActivityIndicator, - Alert, - AppState, - StyleSheet, - Text, - TouchableOpacity, - View, -} from 'react-native'; +import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Constants from 'expo-constants'; import { MapView, @@ -53,20 +45,6 @@ const MAP_STYLE_DARK = const DEFAULT_CENTER: Coords = { latitude: 48.2766, longitude: -116.5533 }; const DEFAULT_ZOOM = 14; -/** - * How often to re-read the GPS while the map is the screen you're looking at. - * A single fix at mount goes stale the moment you walk a block, which is the - * whole time you'd be looking at this screen. - */ -const GPS_REFRESH_MS = 30_000; - -/** - * How old a fix can be before "Park here" / "My location" stops trusting it and - * goes and asks again. A poll and a half, so a fix arriving on schedule is never - * treated as stale. - */ -const FIX_MAX_AGE_MS = 45_000; - type Nav = NativeStackNavigationProp; /** @@ -107,19 +85,7 @@ export function MapScreen() { const insets = useSafeAreaInsets(); const { mode } = useTheme(); const mapStyle = mode === 'dark' ? MAP_STYLE_DARK : MAP_STYLE_LIGHT; - // Poll the GPS while this screen is actually in front of someone. Focus alone - // isn't enough: a backgrounded app stays "focused" on its last tab, and Android - // won't give a foreground app's location out to one that isn't. - const [focused, setFocused] = useState(true); - const [foreground, setForeground] = useState(AppState.currentState === 'active'); - useEffect(() => { - const sub = AppState.addEventListener('change', (next) => setForeground(next === 'active')); - return () => sub.remove(); - }, []); - const { coords, updatedAt, refresh } = useLocation({ - intervalMs: GPS_REFRESH_MS, - active: focused && foreground, - }); + const { coords, refresh } = useLocation(); const { isAnonymous } = useAuth(); const [zones, setZones] = useState([]); const [status, setStatus] = useState('Pan to an area and tap “Search this area”.'); @@ -147,9 +113,7 @@ export function MapScreen() { const viewRef = useRef<{ center: [number, number]; zoom: number } | null>(null); // The native UserLocation dot has its own GPS feed — capture it so "My // location" works even when expo-location can't get a fix (e.g. indoors). - // Stamped, because that feed goes quiet whenever the map isn't drawing and a - // silently stale fix is worse than no fix. - const nativeFix = useRef<(Coords & { at: number }) | null>(null); + const nativeFix = useRef(null); // Look up the LAST session's parking-lot coordinate (the meter's own location // from history — never the user's GPS). Used to open the map and by "Last lot". @@ -252,32 +216,13 @@ export function MapScreen() { }, []); // Re-read the pin on every focus: the session may have ended on another screen - // (or from the notification), which clears it. Focus also gates the GPS poll. + // (or from the notification), which clears it. useFocusEffect( useCallback(() => { void getParkedPin().then(setSpot); - setFocused(true); - return () => setFocused(false); }, []), ); - /** - * The freshest fix we can get, in the order it's cheapest to get it: the map's - * own dot, then the polled expo fix, then a forced read. Whichever we hold is - * only used if it's recent — the point of the poll is that "where am I" answers - * with where you are now, not where you were when the screen opened. - */ - const bestFix = useCallback(async (): Promise => { - const now = Date.now(); - const n = nativeFix.current; - if (n && now - n.at < FIX_MAX_AGE_MS) return { latitude: n.latitude, longitude: n.longitude }; - if (coords && now - updatedAt < FIX_MAX_AGE_MS) return coords; - const fresh = await refresh(); - if (fresh) return fresh; - // Nothing current and nothing new — a stale fix still beats no answer. - return n ? { latitude: n.latitude, longitude: n.longitude } : coords; - }, [coords, updatedAt, refresh]); - // Search whatever the map is currently centered on. This only ever sends the // map's center point — never the device GPS. (If you want to search your own // location, tap "My location" to center there first, then Search this area.) @@ -301,9 +246,10 @@ export function MapScreen() { await searchAt({ latitude: center[1], longitude: center[0] }, 'this area'); }; - // Recenter on the live GPS fix (if available) and search there. + // Recenter on the live GPS fix (if available) and search there. Prefer the + // native map fix (the blue dot), then expo-location, then a forced refresh. const goToMyLocation = async () => { - const c = await bestFix(); + const c = nativeFix.current ?? coords ?? (await refresh()); if (!c) { setStatus('No location fix yet — GPS may be unavailable (e.g. indoors).'); return; @@ -395,7 +341,7 @@ export function MapScreen() { setStatus('Pin cancelled.'); return; } - const c = await bestFix(); + const c = nativeFix.current ?? coords ?? (await refresh()); if (!c) { setPinning(true); setStatus('No GPS fix — tap the map where you parked.'); @@ -512,7 +458,6 @@ export function MapScreen() { nativeFix.current = { latitude: loc.coords.latitude, longitude: loc.coords.longitude, - at: Date.now(), }; } }}