diff --git a/src/location/backgroundGeolocationConfig.js b/src/location/backgroundGeolocationConfig.js index 909be64..1904dc3 100644 --- a/src/location/backgroundGeolocationConfig.js +++ b/src/location/backgroundGeolocationConfig.js @@ -74,7 +74,10 @@ export const BASE_GEOLOCATION_CONFIG = { method: "POST", httpRootProperty: "location", batchSync: false, - autoSync: true, + // We intentionally disable autoSync and perform controlled uploads from explicit triggers + // (startup, identity-change, moving-edge, active-alert). This prevents stationary "ghost" + // uploads from low-quality locations produced by some Android devices. + autoSync: false, // Persistence: keep enough records for offline catch-up. // (The SDK already constrains with maxDaysToPersist; records are deleted after successful upload.) @@ -101,6 +104,7 @@ export const BASE_GEOLOCATION_INVARIANTS = { speedJumpFilter: 100, method: "POST", httpRootProperty: "location", + autoSync: false, maxRecordsToPersist: 1000, maxDaysToPersist: 7, }; @@ -113,10 +117,9 @@ export const TRACKING_PROFILES = { // only periodic fixes (several times/hour). Note many config options like // `distanceFilter` / `stationaryRadius` are documented as having little/no // effect in this mode. - // Some iOS devices / user settings can result in unreliable significant-change wakeups. - // We keep SLC for Android (battery), but fall back to standard motion tracking on iOS - // with a conservative distanceFilter. - useSignificantChangesOnly: Platform.OS !== "ios", + // Some devices / OEMs can be unreliable with significant-change only. + // Use standard motion tracking for reliability, with conservative distanceFilter. + useSignificantChangesOnly: false, // Defensive: if some devices/platform conditions fall back to standard tracking, // keep the distanceFilter conservative to avoid battery drain. diff --git a/src/location/trackLocation.js b/src/location/trackLocation.js index 99b78d3..0018131 100644 --- a/src/location/trackLocation.js +++ b/src/location/trackLocation.js @@ -50,12 +50,46 @@ export default function trackLocation() { let didDisableUploadsForAnonymous = false; let didSyncAfterAuth = false; let didSyncAfterStartupFix = false; + let lastMovingEdgeAt = 0; + const MOVING_EDGE_COOLDOWN_MS = 5 * 60 * 1000; + + const BAD_ACCURACY_THRESHOLD_M = 200; // Track identity so we can force a first geopoint when the effective user changes. let lastSessionUserId = null; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const pruneBadLocations = async () => { + try { + const locations = await BackgroundGeolocation.getLocations(); + if (!Array.isArray(locations) || !locations.length) return 0; + + let deleted = 0; + // Defensive: only scan a bounded amount to avoid heavy work. + const toScan = locations.slice(-200); + for (const loc of toScan) { + const acc = loc?.coords?.accuracy; + const uuid = loc?.uuid; + if ( + typeof acc === "number" && + acc > BAD_ACCURACY_THRESHOLD_M && + uuid + ) { + try { + await BackgroundGeolocation.destroyLocation(uuid); + deleted++; + } catch (e) { + // ignore + } + } + } + return deleted; + } catch (e) { + return 0; + } + }; + const safeSync = async (reason) => { // Sync can fail transiently (SDK busy, network warming up, etc). Retry a few times. for (let attempt = 1; attempt <= 3; attempt++) { @@ -65,6 +99,8 @@ export default function trackLocation() { BackgroundGeolocation.getCount(), ]); + const pruned = await pruneBadLocations(); + locationLogger.info("Attempting BGGeo sync", { reason, attempt, @@ -72,6 +108,7 @@ export default function trackLocation() { isMoving: state?.isMoving, trackingMode: state?.trackingMode, pendingBefore, + pruned, }); const records = await BackgroundGeolocation.sync(); @@ -421,7 +458,7 @@ export default function trackLocation() { } catch (e) { currentSessionUserId = null; } - if (!userToken) { + if (!userToken && !currentSessionUserId) { // Pre-login mode: keep tracking enabled but disable uploads. // Also applies to logout: keep tracking on (per product requirement: track all the time), // but stop sending anything to server without auth. @@ -491,7 +528,7 @@ export default function trackLocation() { locationLogger.debug("Updating background geolocation config"); await BackgroundGeolocation.setConfig({ url: env.GEOLOC_SYNC_URL, // Update the sync URL for when it's changed for staging - autoSync: true, + autoSync: false, headers: { Authorization: `Bearer ${userToken}`, }, @@ -618,6 +655,30 @@ export default function trackLocation() { // The final persisted location will arrive with sample=false. if (location.sample) return; + // Quality gate: delete very poor-accuracy locations to prevent them from being synced + // and ignore them for UI/state. + const acc = location?.coords?.accuracy; + if (typeof acc === "number" && acc > BAD_ACCURACY_THRESHOLD_M) { + locationLogger.info("Ignoring poor-accuracy location", { + accuracy: acc, + uuid: location?.uuid, + }); + if (location?.uuid) { + try { + await BackgroundGeolocation.destroyLocation(location.uuid); + locationLogger.debug("Destroyed poor-accuracy location", { + uuid: location.uuid, + }); + } catch (e) { + locationLogger.warn("Failed to destroy poor-accuracy location", { + uuid: location?.uuid, + error: e?.message, + }); + } + } + return; + } + if ( location.coords && location.coords.latitude && @@ -681,6 +742,46 @@ export default function trackLocation() { isMoving: event?.isMoving, location: event?.location?.coords, }); + + // Moving-edge strategy: when we enter moving state, force one persisted high-quality + // point + sync so the server gets a quick update. + // + // IMPORTANT: Restrict this to ACTIVE tracking only. On Android, motion detection can + // produce false-positive moving transitions while the device is stationary (screen-off), + // which would otherwise trigger unwanted background uploads. + // Cooldown to avoid repeated work due to motion jitter. + if (event?.isMoving && authReady && currentProfile === "active") { + const now = Date.now(); + if (now - lastMovingEdgeAt >= MOVING_EDGE_COOLDOWN_MS) { + lastMovingEdgeAt = now; + (async () => { + try { + const fix = await BackgroundGeolocation.getCurrentPosition({ + samples: 1, + persist: true, + timeout: 30, + maximumAge: 0, + desiredAccuracy: 50, + extras: { + moving_edge: true, + }, + }); + locationLogger.info("Moving-edge fix acquired", { + accuracy: fix?.coords?.accuracy, + latitude: fix?.coords?.latitude, + longitude: fix?.coords?.longitude, + timestamp: fix?.timestamp, + }); + } catch (e) { + locationLogger.warn("Moving-edge fix failed", { + error: e?.message, + stack: e?.stack, + }); + } + await safeSync("moving-edge"); + })(); + } + } }, onActivityChange: (event) => { locationLogger.info("Activity change", {