fix: track location
This commit is contained in:
parent
9b92fed825
commit
29d7747b51
7 changed files with 595 additions and 302 deletions
32
index.js
32
index.js
|
|
@ -2,9 +2,6 @@
|
||||||
import "./warnFilter";
|
import "./warnFilter";
|
||||||
|
|
||||||
import "expo-splash-screen";
|
import "expo-splash-screen";
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
|
||||||
|
|
||||||
import { Platform } from "react-native";
|
|
||||||
|
|
||||||
import notifee from "@notifee/react-native";
|
import notifee from "@notifee/react-native";
|
||||||
import messaging from "@react-native-firebase/messaging";
|
import messaging from "@react-native-firebase/messaging";
|
||||||
|
|
@ -18,9 +15,6 @@ import App from "~/app";
|
||||||
import { onBackgroundEvent as notificationBackgroundEvent } from "~/notifications/onEvent";
|
import { onBackgroundEvent as notificationBackgroundEvent } from "~/notifications/onEvent";
|
||||||
import onMessageReceived from "~/notifications/onMessageReceived";
|
import onMessageReceived from "~/notifications/onMessageReceived";
|
||||||
|
|
||||||
import { createLogger } from "~/lib/logger";
|
|
||||||
// import { executeHeartbeatSync } from "~/location/backgroundTask";
|
|
||||||
|
|
||||||
// setup notification, this have to stay in index.js
|
// setup notification, this have to stay in index.js
|
||||||
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
||||||
messaging().setBackgroundMessageHandler(onMessageReceived);
|
messaging().setBackgroundMessageHandler(onMessageReceived);
|
||||||
|
|
@ -29,29 +23,3 @@ messaging().setBackgroundMessageHandler(onMessageReceived);
|
||||||
// It also ensures that whether you load the app in Expo Go or in a native build,
|
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||||
// the environment is set up appropriately
|
// the environment is set up appropriately
|
||||||
registerRootComponent(App);
|
registerRootComponent(App);
|
||||||
|
|
||||||
const geolocBgLogger = createLogger({
|
|
||||||
service: "background-geolocation",
|
|
||||||
task: "headless",
|
|
||||||
});
|
|
||||||
|
|
||||||
// const HeadlessTask = async (event) => {
|
|
||||||
// try {
|
|
||||||
// switch (event?.name) {
|
|
||||||
// case "heartbeat":
|
|
||||||
// await executeHeartbeatSync();
|
|
||||||
// break;
|
|
||||||
// default:
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// geolocBgLogger.error("HeadlessTask error", {
|
|
||||||
// error,
|
|
||||||
// event,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (Platform.OS === "android") {
|
|
||||||
// BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
85
src/location/backgroundGeolocationConfig.js
Normal file
85
src/location/backgroundGeolocationConfig.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
|
import { TRACK_MOVE } from "~/misc/devicePrefs";
|
||||||
|
import env from "~/env";
|
||||||
|
|
||||||
|
// Common config: keep always-on tracking enabled, but default to an IDLE low-power profile.
|
||||||
|
// High-accuracy and moving mode are enabled only when an active alert is open.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// - We avoid `reset: true` in production because it can unintentionally wipe persisted / configured state.
|
||||||
|
// In dev, `reset: true` is useful to avoid config drift while iterating.
|
||||||
|
// - `maxRecordsToPersist` must be > 1 to support offline catch-up.
|
||||||
|
export const BASE_GEOLOCATION_CONFIG = {
|
||||||
|
// Android Headless Mode (requires registering a headless task entrypoint in index.js)
|
||||||
|
enableHeadless: true,
|
||||||
|
|
||||||
|
// Default to low-power (idle) profile; will be overridden when needed.
|
||||||
|
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
|
||||||
|
|
||||||
|
// Default to the IDLE profile behaviour: we still want distance-based updates
|
||||||
|
// even with no open alert (see TRACKING_PROFILES.idle).
|
||||||
|
distanceFilter: 50,
|
||||||
|
|
||||||
|
// debug: true,
|
||||||
|
logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE,
|
||||||
|
|
||||||
|
// Permission request strategy
|
||||||
|
locationAuthorizationRequest: "Always",
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
stopOnTerminate: false,
|
||||||
|
startOnBoot: true,
|
||||||
|
|
||||||
|
// Background scheduling
|
||||||
|
heartbeatInterval: 3600,
|
||||||
|
|
||||||
|
// Android foreground service
|
||||||
|
foregroundService: true,
|
||||||
|
notification: {
|
||||||
|
title: "Alerte Secours",
|
||||||
|
text: "Suivi de localisation actif",
|
||||||
|
channelName: "Location tracking",
|
||||||
|
priority: BackgroundGeolocation.NOTIFICATION_PRIORITY_HIGH,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Android 10+ rationale dialog
|
||||||
|
backgroundPermissionRationale: {
|
||||||
|
title:
|
||||||
|
"Autoriser Alerte-Secours à accéder à la localisation en arrière-plan",
|
||||||
|
message:
|
||||||
|
"Alerte-Secours nécessite la localisation en arrière-plan pour vous alerter en temps réel lorsqu'une personne à proximité a besoin d'aide urgente. Cette fonction est essentielle pour permettre une intervention rapide et efficace en cas d'urgence.",
|
||||||
|
positiveAction: "Autoriser",
|
||||||
|
negativeAction: "Désactiver",
|
||||||
|
},
|
||||||
|
|
||||||
|
// HTTP configuration
|
||||||
|
url: env.GEOLOC_SYNC_URL,
|
||||||
|
method: "POST",
|
||||||
|
httpRootProperty: "location",
|
||||||
|
batchSync: false,
|
||||||
|
autoSync: true,
|
||||||
|
|
||||||
|
// Persistence: keep enough records for offline catch-up.
|
||||||
|
// (The SDK already constrains with maxDaysToPersist; records are deleted after successful upload.)
|
||||||
|
maxRecordsToPersist: 1000,
|
||||||
|
maxDaysToPersist: 7,
|
||||||
|
|
||||||
|
// Development convenience
|
||||||
|
reset: !!__DEV__,
|
||||||
|
|
||||||
|
// Behavior tweaks
|
||||||
|
disableProviderChangeRecord: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TRACKING_PROFILES = {
|
||||||
|
idle: {
|
||||||
|
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
|
||||||
|
distanceFilter: 50,
|
||||||
|
heartbeatInterval: 3600,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
|
||||||
|
distanceFilter: TRACK_MOVE,
|
||||||
|
heartbeatInterval: 900,
|
||||||
|
},
|
||||||
|
};
|
||||||
122
src/location/backgroundGeolocationService.js
Normal file
122
src/location/backgroundGeolocationService.js
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
import { BASE_GEOLOCATION_CONFIG } from "./backgroundGeolocationConfig";
|
||||||
|
|
||||||
|
const bgGeoLogger = createLogger({
|
||||||
|
module: BACKGROUND_SCOPES.GEOLOCATION,
|
||||||
|
feature: "bg-geo-service",
|
||||||
|
});
|
||||||
|
|
||||||
|
let readyPromise = null;
|
||||||
|
let lastReadyState = null;
|
||||||
|
|
||||||
|
let subscriptions = [];
|
||||||
|
let handlersSignature = null;
|
||||||
|
|
||||||
|
export async function ensureBackgroundGeolocationReady(
|
||||||
|
config = BASE_GEOLOCATION_CONFIG,
|
||||||
|
) {
|
||||||
|
if (readyPromise) return readyPromise;
|
||||||
|
|
||||||
|
readyPromise = (async () => {
|
||||||
|
bgGeoLogger.info("Calling BackgroundGeolocation.ready");
|
||||||
|
const state = await BackgroundGeolocation.ready(config);
|
||||||
|
lastReadyState = state;
|
||||||
|
bgGeoLogger.info("BackgroundGeolocation is ready", {
|
||||||
|
enabled: state?.enabled,
|
||||||
|
isMoving: state?.isMoving,
|
||||||
|
trackingMode: state?.trackingMode,
|
||||||
|
schedulerEnabled: state?.schedulerEnabled,
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
})().catch((error) => {
|
||||||
|
// Allow retry if ready fails.
|
||||||
|
readyPromise = null;
|
||||||
|
lastReadyState = null;
|
||||||
|
bgGeoLogger.error("BackgroundGeolocation.ready failed", {
|
||||||
|
error: error?.message,
|
||||||
|
stack: error?.stack,
|
||||||
|
code: error?.code,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
return readyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLastReadyState() {
|
||||||
|
return lastReadyState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBackgroundGeolocationEventHandlers({
|
||||||
|
onLocation,
|
||||||
|
onLocationError,
|
||||||
|
onHttp,
|
||||||
|
onMotionChange,
|
||||||
|
onActivityChange,
|
||||||
|
onProviderChange,
|
||||||
|
onConnectivityChange,
|
||||||
|
onEnabledChange,
|
||||||
|
} = {}) {
|
||||||
|
// Avoid duplicate registration when `trackLocation()` is called multiple times.
|
||||||
|
// We use a simple signature so calling with identical functions is a no-op.
|
||||||
|
const sig = [
|
||||||
|
onLocation ? "L1" : "L0",
|
||||||
|
onHttp ? "H1" : "H0",
|
||||||
|
onMotionChange ? "M1" : "M0",
|
||||||
|
onActivityChange ? "A1" : "A0",
|
||||||
|
onProviderChange ? "P1" : "P0",
|
||||||
|
onConnectivityChange ? "C1" : "C0",
|
||||||
|
onEnabledChange ? "E1" : "E0",
|
||||||
|
].join("-");
|
||||||
|
if (handlersSignature === sig && subscriptions.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.forEach((s) => s?.remove?.());
|
||||||
|
subscriptions = [];
|
||||||
|
|
||||||
|
if (onLocation) {
|
||||||
|
subscriptions.push(
|
||||||
|
BackgroundGeolocation.onLocation(onLocation, onLocationError),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (onHttp) {
|
||||||
|
subscriptions.push(BackgroundGeolocation.onHttp(onHttp));
|
||||||
|
}
|
||||||
|
if (onMotionChange) {
|
||||||
|
subscriptions.push(BackgroundGeolocation.onMotionChange(onMotionChange));
|
||||||
|
}
|
||||||
|
if (onActivityChange) {
|
||||||
|
subscriptions.push(
|
||||||
|
BackgroundGeolocation.onActivityChange(onActivityChange),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (onProviderChange) {
|
||||||
|
subscriptions.push(
|
||||||
|
BackgroundGeolocation.onProviderChange(onProviderChange),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (onConnectivityChange) {
|
||||||
|
subscriptions.push(
|
||||||
|
BackgroundGeolocation.onConnectivityChange(onConnectivityChange),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (onEnabledChange) {
|
||||||
|
subscriptions.push(BackgroundGeolocation.onEnabledChange(onEnabledChange));
|
||||||
|
}
|
||||||
|
|
||||||
|
handlersSignature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopBackgroundGeolocation() {
|
||||||
|
await ensureBackgroundGeolocationReady();
|
||||||
|
return BackgroundGeolocation.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startBackgroundGeolocation() {
|
||||||
|
await ensureBackgroundGeolocationReady();
|
||||||
|
return BackgroundGeolocation.start();
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,9 @@ import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
import { ensureBackgroundGeolocationReady } from "~/location/backgroundGeolocationService";
|
||||||
|
import { BASE_GEOLOCATION_CONFIG } from "~/location/backgroundGeolocationConfig";
|
||||||
|
|
||||||
// Global variables
|
// Global variables
|
||||||
let emulatorIntervalId = null;
|
let emulatorIntervalId = null;
|
||||||
let isEmulatorModeEnabled = false;
|
let isEmulatorModeEnabled = false;
|
||||||
|
|
@ -43,6 +46,8 @@ export const enableEmulatorMode = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
||||||
|
|
||||||
// Call immediately once
|
// Call immediately once
|
||||||
await BackgroundGeolocation.changePace(true);
|
await BackgroundGeolocation.changePace(true);
|
||||||
emulatorLogger.debug("Initial changePace call successful");
|
emulatorLogger.debug("Initial changePace call successful");
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import setLocationState from "./setLocationState";
|
||||||
|
|
||||||
import camelCaseKeys from "~/utils/string/camelCaseKeys";
|
import camelCaseKeys from "~/utils/string/camelCaseKeys";
|
||||||
|
|
||||||
|
import { ensureBackgroundGeolocationReady } from "~/location/backgroundGeolocationService";
|
||||||
|
import { BASE_GEOLOCATION_CONFIG } from "~/location/backgroundGeolocationConfig";
|
||||||
|
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
const RETRY_DELAY = 1000; // 1 second
|
const RETRY_DELAY = 1000; // 1 second
|
||||||
|
|
||||||
|
|
@ -17,6 +20,10 @@ export async function getCurrentLocation() {
|
||||||
|
|
||||||
while (retries < MAX_RETRIES) {
|
while (retries < MAX_RETRIES) {
|
||||||
try {
|
try {
|
||||||
|
// Vendor requirement: never call APIs like getState/requestPermission/getCurrentPosition
|
||||||
|
// before `.ready()` has resolved.
|
||||||
|
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
||||||
|
|
||||||
// Check for location permissions and services
|
// Check for location permissions and services
|
||||||
const state = await BackgroundGeolocation.getState();
|
const state = await BackgroundGeolocation.getState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
import { TRACK_MOVE } from "~/misc/devicePrefs";
|
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
import jwtDecode from "jwt-decode";
|
import jwtDecode from "jwt-decode";
|
||||||
|
|
@ -20,304 +19,406 @@ import { storeLocation } from "~/location/storage";
|
||||||
|
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
|
|
||||||
// Common config: keep always-on tracking enabled, but default to an IDLE low-power profile.
|
import {
|
||||||
// High-accuracy and "moving" mode are only enabled when an active alert is open.
|
BASE_GEOLOCATION_CONFIG,
|
||||||
const baseConfig = {
|
TRACKING_PROFILES,
|
||||||
// https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode
|
} from "~/location/backgroundGeolocationConfig";
|
||||||
enableHeadless: true,
|
import {
|
||||||
disableProviderChangeRecord: true,
|
ensureBackgroundGeolocationReady,
|
||||||
// disableMotionActivityUpdates: true,
|
setBackgroundGeolocationEventHandlers,
|
||||||
// Default to low-power (idle) profile; will be overridden when needed.
|
} from "~/location/backgroundGeolocationService";
|
||||||
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
|
|
||||||
// Larger distance filter in idle mode to prevent frequent GPS wakes.
|
|
||||||
distanceFilter: 200,
|
|
||||||
// debug: true, // Enable debug mode for more detailed logs
|
|
||||||
logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE,
|
|
||||||
// Disable automatic permission requests
|
|
||||||
locationAuthorizationRequest: "Always",
|
|
||||||
stopOnTerminate: false,
|
|
||||||
startOnBoot: true,
|
|
||||||
// Keep heartbeat very infrequent in idle mode.
|
|
||||||
heartbeatInterval: 3600,
|
|
||||||
// Force the plugin to start aggressively
|
|
||||||
foregroundService: true,
|
|
||||||
notification: {
|
|
||||||
title: "Alerte Secours",
|
|
||||||
text: "Suivi de localisation actif",
|
|
||||||
channelName: "Location tracking",
|
|
||||||
priority: BackgroundGeolocation.NOTIFICATION_PRIORITY_HIGH,
|
|
||||||
},
|
|
||||||
backgroundPermissionRationale: {
|
|
||||||
title:
|
|
||||||
"Autoriser Alerte-Secours à accéder à la localisation en arrière-plan",
|
|
||||||
message:
|
|
||||||
"Alerte-Secours nécessite la localisation en arrière-plan pour vous alerter en temps réel lorsqu'une personne à proximité a besoin d'aide urgente. Cette fonction est essentielle pour permettre une intervention rapide et efficace en cas d'urgence.",
|
|
||||||
positiveAction: "Autoriser",
|
|
||||||
negativeAction: "Désactiver",
|
|
||||||
},
|
|
||||||
// Enhanced HTTP configuration
|
|
||||||
url: env.GEOLOC_SYNC_URL,
|
|
||||||
method: "POST", // Explicitly set HTTP method
|
|
||||||
httpRootProperty: "location", // Specify the root property for the locations array
|
|
||||||
// Configure persistence
|
|
||||||
maxRecordsToPersist: 1, // Limit the number of records to store
|
|
||||||
maxDaysToPersist: 7, // Limit the age of records to persist
|
|
||||||
batchSync: false,
|
|
||||||
autoSync: true,
|
|
||||||
reset: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const TRACKING_PROFILES = {
|
let trackLocationStartPromise = null;
|
||||||
idle: {
|
|
||||||
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
|
|
||||||
distanceFilter: 200,
|
|
||||||
heartbeatInterval: 3600,
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
|
|
||||||
distanceFilter: TRACK_MOVE,
|
|
||||||
heartbeatInterval: 900,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function trackLocation() {
|
export default function trackLocation() {
|
||||||
const locationLogger = createLogger({
|
if (trackLocationStartPromise) return trackLocationStartPromise;
|
||||||
module: BACKGROUND_SCOPES.GEOLOCATION,
|
|
||||||
feature: "tracking",
|
|
||||||
});
|
|
||||||
|
|
||||||
let currentProfile = null;
|
trackLocationStartPromise = (async () => {
|
||||||
let authReady = false;
|
const locationLogger = createLogger({
|
||||||
let stopAlertSubscription = null;
|
module: BACKGROUND_SCOPES.GEOLOCATION,
|
||||||
let stopSessionSubscription = null;
|
feature: "tracking",
|
||||||
|
|
||||||
const computeHasOwnOpenAlert = () => {
|
|
||||||
try {
|
|
||||||
const { userId } = getSessionState();
|
|
||||||
const { alertingList } = getAlertState();
|
|
||||||
if (!userId || !Array.isArray(alertingList)) return false;
|
|
||||||
return alertingList.some(
|
|
||||||
({ oneAlert }) =>
|
|
||||||
oneAlert?.state === "open" && oneAlert?.userId === userId,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
locationLogger.warn("Failed to compute active-alert state", {
|
|
||||||
error: e?.message,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyProfile = async (profileName) => {
|
|
||||||
if (!authReady) {
|
|
||||||
// We only apply profile once auth headers are configured.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentProfile === profileName) return;
|
|
||||||
|
|
||||||
const profile = TRACKING_PROFILES[profileName];
|
|
||||||
if (!profile) {
|
|
||||||
locationLogger.warn("Unknown tracking profile", { profileName });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
locationLogger.info("Applying tracking profile", {
|
|
||||||
profileName,
|
|
||||||
desiredAccuracy: profile.desiredAccuracy,
|
|
||||||
distanceFilter: profile.distanceFilter,
|
|
||||||
heartbeatInterval: profile.heartbeatInterval,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
let currentProfile = null;
|
||||||
await BackgroundGeolocation.setConfig(profile);
|
let authReady = false;
|
||||||
|
let stopAlertSubscription = null;
|
||||||
|
let stopSessionSubscription = null;
|
||||||
|
|
||||||
// Key battery fix:
|
// One-off startup refresh: when tracking is enabled at app launch, fetch a fresh fix once.
|
||||||
// - IDLE profile forces stationary mode
|
// This follows Transistorsoft docs guidance to use getCurrentPosition rather than forcing
|
||||||
// - ACTIVE profile forces moving mode
|
// the SDK into moving mode with changePace(true).
|
||||||
await BackgroundGeolocation.changePace(profileName === "active");
|
let didRequestStartupFix = false;
|
||||||
|
let startupFixInFlight = null;
|
||||||
|
|
||||||
currentProfile = profileName;
|
// When auth changes, we want a fresh persisted point for the newly effective identity.
|
||||||
} catch (error) {
|
// Debounced to avoid spamming `getCurrentPosition` if auth updates quickly (refresh/renew).
|
||||||
locationLogger.error("Failed to apply tracking profile", {
|
let authFixDebounceTimerId = null;
|
||||||
profileName,
|
let authFixInFlight = null;
|
||||||
error: error?.message,
|
const AUTH_FIX_DEBOUNCE_MS = 1500;
|
||||||
stack: error?.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Log the geolocation sync URL for debugging
|
const scheduleAuthFreshFix = () => {
|
||||||
locationLogger.info("Geolocation sync URL configuration", {
|
if (authFixDebounceTimerId) {
|
||||||
url: env.GEOLOC_SYNC_URL,
|
clearTimeout(authFixDebounceTimerId);
|
||||||
isStaging: env.IS_STAGING,
|
authFixDebounceTimerId = null;
|
||||||
});
|
|
||||||
|
|
||||||
// Handle auth function - no throttling or cooldown
|
|
||||||
async function handleAuth(userToken) {
|
|
||||||
locationLogger.info("Handling auth token update", {
|
|
||||||
hasToken: !!userToken,
|
|
||||||
});
|
|
||||||
if (!userToken) {
|
|
||||||
locationLogger.info("No auth token, stopping location tracking");
|
|
||||||
await BackgroundGeolocation.stop();
|
|
||||||
locationLogger.debug("Location tracking stopped");
|
|
||||||
|
|
||||||
// Cleanup subscriptions when logged out.
|
|
||||||
try {
|
|
||||||
stopAlertSubscription && stopAlertSubscription();
|
|
||||||
stopSessionSubscription && stopSessionSubscription();
|
|
||||||
} finally {
|
|
||||||
stopAlertSubscription = null;
|
|
||||||
stopSessionSubscription = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authReady = false;
|
authFixInFlight = new Promise((resolve) => {
|
||||||
currentProfile = null;
|
authFixDebounceTimerId = setTimeout(resolve, AUTH_FIX_DEBOUNCE_MS);
|
||||||
return;
|
}).then(async () => {
|
||||||
}
|
try {
|
||||||
// unsub();
|
const before = await BackgroundGeolocation.getState();
|
||||||
locationLogger.debug("Updating background geolocation config");
|
locationLogger.info("Requesting auth-change location fix", {
|
||||||
await BackgroundGeolocation.setConfig({
|
enabled: before.enabled,
|
||||||
url: env.GEOLOC_SYNC_URL, // Update the sync URL for when it's changed for staging
|
trackingMode: before.trackingMode,
|
||||||
headers: {
|
isMoving: before.isMoving,
|
||||||
Authorization: `Bearer ${userToken}`,
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
authReady = true;
|
const location = await BackgroundGeolocation.getCurrentPosition({
|
||||||
|
samples: 3,
|
||||||
|
persist: true,
|
||||||
|
timeout: 30,
|
||||||
|
maximumAge: 5000,
|
||||||
|
desiredAccuracy: 50,
|
||||||
|
extras: {
|
||||||
|
auth_token_update: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Log the authorization header that was set
|
locationLogger.info("Auth-change location fix acquired", {
|
||||||
locationLogger.debug(
|
accuracy: location?.coords?.accuracy,
|
||||||
"Set Authorization header for background geolocation",
|
latitude: location?.coords?.latitude,
|
||||||
{
|
longitude: location?.coords?.longitude,
|
||||||
headerSet: true,
|
timestamp: location?.timestamp,
|
||||||
tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null,
|
});
|
||||||
},
|
} catch (error) {
|
||||||
);
|
locationLogger.warn("Auth-change location fix failed", {
|
||||||
|
error: error?.message,
|
||||||
const state = await BackgroundGeolocation.getState();
|
code: error?.code,
|
||||||
try {
|
stack: error?.stack,
|
||||||
const decodedToken = jwtDecode(userToken);
|
});
|
||||||
locationLogger.debug("Decoded JWT token", { decodedToken });
|
} finally {
|
||||||
} catch (error) {
|
authFixDebounceTimerId = null;
|
||||||
locationLogger.error("Failed to decode JWT token", {
|
authFixInFlight = null;
|
||||||
error: error.message,
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.enabled) {
|
return authFixInFlight;
|
||||||
locationLogger.info("Starting location tracking");
|
};
|
||||||
|
|
||||||
|
const computeHasOwnOpenAlert = () => {
|
||||||
try {
|
try {
|
||||||
await BackgroundGeolocation.start();
|
const { userId } = getSessionState();
|
||||||
locationLogger.debug("Location tracking started successfully");
|
const { alertingList } = getAlertState();
|
||||||
|
if (!userId || !Array.isArray(alertingList)) return false;
|
||||||
|
return alertingList.some(
|
||||||
|
({ oneAlert }) =>
|
||||||
|
oneAlert?.state === "open" && oneAlert?.userId === userId,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.warn("Failed to compute active-alert state", {
|
||||||
|
error: e?.message,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyProfile = async (profileName) => {
|
||||||
|
if (!authReady) {
|
||||||
|
// We only apply profile once auth headers are configured.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentProfile === profileName) return;
|
||||||
|
|
||||||
|
const profile = TRACKING_PROFILES[profileName];
|
||||||
|
if (!profile) {
|
||||||
|
locationLogger.warn("Unknown tracking profile", { profileName });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
locationLogger.info("Applying tracking profile", {
|
||||||
|
profileName,
|
||||||
|
desiredAccuracy: profile.desiredAccuracy,
|
||||||
|
distanceFilter: profile.distanceFilter,
|
||||||
|
heartbeatInterval: profile.heartbeatInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await BackgroundGeolocation.setConfig(profile);
|
||||||
|
|
||||||
|
// Motion state strategy:
|
||||||
|
// - ACTIVE: force moving to begin aggressive tracking immediately.
|
||||||
|
// - IDLE: do NOT force stationary. Let the SDK's motion detection manage
|
||||||
|
// moving/stationary transitions so we still get distance-based updates
|
||||||
|
// (target: new point when moved ~50m+ even without an open alert).
|
||||||
|
if (profileName === "active") {
|
||||||
|
await BackgroundGeolocation.changePace(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentProfile = profileName;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
locationLogger.error("Failed to start location tracking", {
|
locationLogger.error("Failed to apply tracking profile", {
|
||||||
error: error.message,
|
profileName,
|
||||||
stack: error.stack,
|
error: error?.message,
|
||||||
|
stack: error?.stack,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Ensure we are NOT forcing "moving" mode by default.
|
// Log the geolocation sync URL for debugging
|
||||||
// Default profile is idle unless an active alert requires higher accuracy.
|
locationLogger.info("Geolocation sync URL configuration", {
|
||||||
const shouldBeActive = computeHasOwnOpenAlert();
|
url: env.GEOLOC_SYNC_URL,
|
||||||
await applyProfile(shouldBeActive ? "active" : "idle");
|
isStaging: env.IS_STAGING,
|
||||||
|
});
|
||||||
|
|
||||||
// Subscribe to changes that may require switching profiles.
|
// Handle auth function - no throttling or cooldown
|
||||||
if (!stopSessionSubscription) {
|
async function handleAuth(userToken) {
|
||||||
stopSessionSubscription = subscribeSessionState(
|
// Defensive: ensure `.ready()` is resolved before any API call.
|
||||||
(s) => s?.userId,
|
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
||||||
() => {
|
|
||||||
const active = computeHasOwnOpenAlert();
|
locationLogger.info("Handling auth token update", {
|
||||||
applyProfile(active ? "active" : "idle");
|
hasToken: !!userToken,
|
||||||
|
});
|
||||||
|
if (!userToken) {
|
||||||
|
locationLogger.info("No auth token, stopping location tracking");
|
||||||
|
|
||||||
|
// Prevent any further uploads before stopping.
|
||||||
|
// This guards against persisted HTTP config continuing to flush queued records.
|
||||||
|
try {
|
||||||
|
await BackgroundGeolocation.setConfig({
|
||||||
|
url: "",
|
||||||
|
autoSync: false,
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.warn("Failed to clear BGGeo HTTP config on logout", {
|
||||||
|
error: e?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await BackgroundGeolocation.stop();
|
||||||
|
locationLogger.debug("Location tracking stopped");
|
||||||
|
|
||||||
|
// Cleanup subscriptions when logged out.
|
||||||
|
try {
|
||||||
|
stopAlertSubscription && stopAlertSubscription();
|
||||||
|
stopSessionSubscription && stopSessionSubscription();
|
||||||
|
} finally {
|
||||||
|
stopAlertSubscription = null;
|
||||||
|
stopSessionSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
authReady = false;
|
||||||
|
currentProfile = null;
|
||||||
|
|
||||||
|
if (authFixDebounceTimerId) {
|
||||||
|
clearTimeout(authFixDebounceTimerId);
|
||||||
|
authFixDebounceTimerId = null;
|
||||||
|
}
|
||||||
|
authFixInFlight = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// unsub();
|
||||||
|
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
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${userToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
authReady = true;
|
||||||
|
|
||||||
|
// Log the authorization header that was set
|
||||||
|
locationLogger.debug(
|
||||||
|
"Set Authorization header for background geolocation",
|
||||||
|
{
|
||||||
|
headerSet: true,
|
||||||
|
tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if (!stopAlertSubscription) {
|
|
||||||
stopAlertSubscription = subscribeAlertState(
|
|
||||||
(s) => s?.alertingList,
|
|
||||||
() => {
|
|
||||||
const active = computeHasOwnOpenAlert();
|
|
||||||
applyProfile(active ? "active" : "idle");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BackgroundGeolocation.onLocation(async (location) => {
|
const state = await BackgroundGeolocation.getState();
|
||||||
locationLogger.debug("Location update received", {
|
try {
|
||||||
coords: location.coords,
|
const decodedToken = jwtDecode(userToken);
|
||||||
timestamp: location.timestamp,
|
locationLogger.debug("Decoded JWT token", { decodedToken });
|
||||||
activity: location.activity,
|
} catch (error) {
|
||||||
battery: location.battery,
|
locationLogger.error("Failed to decode JWT token", {
|
||||||
});
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (!state.enabled) {
|
||||||
location.coords &&
|
locationLogger.info("Starting location tracking");
|
||||||
location.coords.latitude &&
|
try {
|
||||||
location.coords.longitude
|
await BackgroundGeolocation.start();
|
||||||
) {
|
locationLogger.debug("Location tracking started successfully");
|
||||||
setLocationState(location.coords);
|
} catch (error) {
|
||||||
// Also store in AsyncStorage for last known location fallback
|
locationLogger.error("Failed to start location tracking", {
|
||||||
storeLocation(location.coords, location.timestamp);
|
error: error.message,
|
||||||
}
|
stack: error.stack,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BackgroundGeolocation.onHttp(async (response) => {
|
// Always request a fresh persisted point on any token update.
|
||||||
// log status code and response
|
// This ensures a newly connected user gets an immediate point even if they don't move.
|
||||||
locationLogger.debug("HTTP response received", {
|
scheduleAuthFreshFix();
|
||||||
status: response?.status,
|
|
||||||
responseText: response?.responseText,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
// Request a single fresh location-fix on each app launch when tracking is enabled.
|
||||||
locationLogger.info("Initializing background geolocation");
|
// - We do this only after auth headers are configured so the persisted point can sync.
|
||||||
await BackgroundGeolocation.ready(baseConfig);
|
// - We do NOT force moving mode.
|
||||||
await BackgroundGeolocation.setConfig(baseConfig);
|
if (!didRequestStartupFix) {
|
||||||
|
didRequestStartupFix = true;
|
||||||
|
startupFixInFlight = scheduleAuthFreshFix();
|
||||||
|
} else if (authFixInFlight) {
|
||||||
|
// Avoid concurrent fix calls if auth updates race.
|
||||||
|
await authFixInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
// Only set the permission state if we already have the permission
|
// Ensure we are NOT forcing "moving" mode by default.
|
||||||
const state = await BackgroundGeolocation.getState();
|
// Default profile is idle unless an active alert requires higher accuracy.
|
||||||
locationLogger.debug("Background geolocation state", {
|
const shouldBeActive = computeHasOwnOpenAlert();
|
||||||
enabled: state.enabled,
|
await applyProfile(shouldBeActive ? "active" : "idle");
|
||||||
trackingMode: state.trackingMode,
|
|
||||||
isMoving: state.isMoving,
|
|
||||||
schedulerEnabled: state.schedulerEnabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (state.enabled) {
|
// Subscribe to changes that may require switching profiles.
|
||||||
locationLogger.info("Background location permission confirmed");
|
if (!stopSessionSubscription) {
|
||||||
permissionsActions.setLocationBackground(true);
|
stopSessionSubscription = subscribeSessionState(
|
||||||
} else {
|
(s) => s?.userId,
|
||||||
locationLogger.warn(
|
() => {
|
||||||
"Background location not enabled in geolocation state",
|
const active = computeHasOwnOpenAlert();
|
||||||
);
|
applyProfile(active ? "active" : "idle");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!stopAlertSubscription) {
|
||||||
|
stopAlertSubscription = subscribeAlertState(
|
||||||
|
(s) => s?.alertingList,
|
||||||
|
() => {
|
||||||
|
const active = computeHasOwnOpenAlert();
|
||||||
|
applyProfile(active ? "active" : "idle");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (LOCAL_DEV) {
|
setBackgroundGeolocationEventHandlers({
|
||||||
// // fixing issue on android emulator (which doesn't have accelerometer or gyroscope) by manually enabling location updates
|
onLocation: async (location) => {
|
||||||
// setInterval(
|
locationLogger.debug("Location update received", {
|
||||||
// () => {
|
coords: location.coords,
|
||||||
// BackgroundGeolocation.changePace(true);
|
timestamp: location.timestamp,
|
||||||
// },
|
activity: location.activity,
|
||||||
// 30 * 60 * 1000,
|
battery: location.battery,
|
||||||
// );
|
sample: location.sample,
|
||||||
// }
|
});
|
||||||
} catch (error) {
|
|
||||||
locationLogger.error("Location tracking initialization failed", {
|
|
||||||
error: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
code: error.code,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const { userToken } = getAuthState();
|
|
||||||
locationLogger.debug("Setting up auth state subscription");
|
|
||||||
subscribeAuthState(({ userToken }) => userToken, handleAuth);
|
|
||||||
locationLogger.debug("Performing initial auth handling");
|
|
||||||
handleAuth(userToken);
|
|
||||||
|
|
||||||
// Initialize emulator mode only in dev/staging to avoid accidental production battery drain.
|
// Ignore sampling locations (eg, emitted during getCurrentPosition) to avoid UI/storage churn.
|
||||||
if (__DEV__ || env.IS_STAGING) {
|
// The final persisted location will arrive with sample=false.
|
||||||
initEmulatorMode();
|
if (location.sample) return;
|
||||||
}
|
|
||||||
|
if (
|
||||||
|
location.coords &&
|
||||||
|
location.coords.latitude &&
|
||||||
|
location.coords.longitude
|
||||||
|
) {
|
||||||
|
setLocationState(location.coords);
|
||||||
|
// Also store in AsyncStorage for last known location fallback
|
||||||
|
storeLocation(location.coords, location.timestamp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLocationError: (error) => {
|
||||||
|
locationLogger.warn("Location error", {
|
||||||
|
error: error?.message,
|
||||||
|
code: error?.code,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onHttp: async (response) => {
|
||||||
|
// Log success/failure for visibility into token expiry, server errors, etc.
|
||||||
|
locationLogger.debug("HTTP response received", {
|
||||||
|
success: response?.success,
|
||||||
|
status: response?.status,
|
||||||
|
responseText: response?.responseText,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onMotionChange: (event) => {
|
||||||
|
locationLogger.info("Motion change", {
|
||||||
|
isMoving: event?.isMoving,
|
||||||
|
location: event?.location?.coords,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onActivityChange: (event) => {
|
||||||
|
locationLogger.info("Activity change", {
|
||||||
|
activity: event?.activity,
|
||||||
|
confidence: event?.confidence,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onProviderChange: (event) => {
|
||||||
|
locationLogger.info("Provider change", {
|
||||||
|
status: event?.status,
|
||||||
|
enabled: event?.enabled,
|
||||||
|
network: event?.network,
|
||||||
|
gps: event?.gps,
|
||||||
|
accuracyAuthorization: event?.accuracyAuthorization,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onConnectivityChange: (event) => {
|
||||||
|
locationLogger.info("Connectivity change", {
|
||||||
|
connected: event?.connected,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onEnabledChange: (enabled) => {
|
||||||
|
locationLogger.info("Enabled change", { enabled });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
locationLogger.info("Initializing background geolocation");
|
||||||
|
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
||||||
|
|
||||||
|
// Only set the permission state if we already have the permission
|
||||||
|
const state = await BackgroundGeolocation.getState();
|
||||||
|
locationLogger.debug("Background geolocation state", {
|
||||||
|
enabled: state.enabled,
|
||||||
|
trackingMode: state.trackingMode,
|
||||||
|
isMoving: state.isMoving,
|
||||||
|
schedulerEnabled: state.schedulerEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.enabled) {
|
||||||
|
locationLogger.info("Background location permission confirmed");
|
||||||
|
permissionsActions.setLocationBackground(true);
|
||||||
|
} else {
|
||||||
|
locationLogger.warn(
|
||||||
|
"Background location not enabled in geolocation state",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (LOCAL_DEV) {
|
||||||
|
// // fixing issue on android emulator (which doesn't have accelerometer or gyroscope) by manually enabling location updates
|
||||||
|
// setInterval(
|
||||||
|
// () => {
|
||||||
|
// BackgroundGeolocation.changePace(true);
|
||||||
|
// },
|
||||||
|
// 30 * 60 * 1000,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
locationLogger.error("Location tracking initialization failed", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { userToken } = getAuthState();
|
||||||
|
locationLogger.debug("Setting up auth state subscription");
|
||||||
|
subscribeAuthState(({ userToken }) => userToken, handleAuth);
|
||||||
|
locationLogger.debug("Performing initial auth handling");
|
||||||
|
handleAuth(userToken);
|
||||||
|
|
||||||
|
// Initialize emulator mode only in dev/staging to avoid accidental production battery drain.
|
||||||
|
if (__DEV__ || env.IS_STAGING) {
|
||||||
|
initEmulatorMode();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return trackLocationStartPromise;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ import {
|
||||||
import { LOG_LEVELS, setMinLogLevel } from "~/lib/logger";
|
import { LOG_LEVELS, setMinLogLevel } from "~/lib/logger";
|
||||||
import { config as loggerConfig } from "~/lib/logger/config";
|
import { config as loggerConfig } from "~/lib/logger/config";
|
||||||
|
|
||||||
|
import { ensureBackgroundGeolocationReady } from "~/location/backgroundGeolocationService";
|
||||||
|
import { BASE_GEOLOCATION_CONFIG } from "~/location/backgroundGeolocationConfig";
|
||||||
|
|
||||||
const reset = async () => {
|
const reset = async () => {
|
||||||
await authActions.logout();
|
await authActions.logout();
|
||||||
};
|
};
|
||||||
|
|
@ -75,6 +78,8 @@ export default function Developer() {
|
||||||
setSyncStatus("syncing");
|
setSyncStatus("syncing");
|
||||||
setSyncResult("");
|
setSyncResult("");
|
||||||
|
|
||||||
|
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
||||||
|
|
||||||
// Get the count of pending records first
|
// Get the count of pending records first
|
||||||
const count = await BackgroundGeolocation.getCount();
|
const count = await BackgroundGeolocation.getCount();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue