Compare commits
No commits in common. "21e121ea05be4e12d17184229c7523a38452e65e" and "b8c57520dfc6bc93973f049ec680ad0c9cb9aebe" have entirely different histories.
21e121ea05
...
b8c57520df
13 changed files with 1459 additions and 855 deletions
|
|
@ -2,13 +2,6 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||||
|
|
||||||
## [1.16.9](https://github.com/alerte-secours/as-app/compare/v1.16.8...v1.16.9) (2026-02-08)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **track-location:** try 11 ([88fbd72](https://github.com/alerte-secours/as-app/commit/88fbd72e5144c36c01d9e047ea43faf69014c9f6))
|
|
||||||
|
|
||||||
## [1.16.8](https://github.com/alerte-secours/as-app/compare/v1.16.7...v1.16.8) (2026-02-05)
|
## [1.16.8](https://github.com/alerte-secours/as-app/compare/v1.16.7...v1.16.8) (2026-02-05)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@ android {
|
||||||
applicationId 'com.alertesecours'
|
applicationId 'com.alertesecours'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 240
|
versionCode 239
|
||||||
versionName "1.16.9"
|
versionName "1.16.8"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
testBuildType System.getProperty('testBuildType', 'debug')
|
testBuildType System.getProperty('testBuildType', 'debug')
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
# Location tracking QA checklist
|
# Location tracking QA checklist
|
||||||
|
|
||||||
Applies to the BackgroundGeolocation integration:
|
Applies to the BackgroundGeolocation integration:
|
||||||
- [`trackLocation()`](src/location/trackLocation.js:11)
|
- [`trackLocation()`](src/location/trackLocation.js:34)
|
||||||
- [`createTrackingController()`](src/location/bggeo/createTrackingController.js:1)
|
- [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:126)
|
||||||
- [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:190)
|
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
|
|
@ -21,12 +20,12 @@ Applies to the BackgroundGeolocation integration:
|
||||||
- If the SDK later reports a real move (`onMotionChange(isMoving:true)`), JS may request **one** persisted fix as a fallback.
|
- If the SDK later reports a real move (`onMotionChange(isMoving:true)`), JS may request **one** persisted fix as a fallback.
|
||||||
- We intentionally do not rely on time-based updates.
|
- We intentionally do not rely on time-based updates.
|
||||||
- ACTIVE uses `geolocation.distanceFilter: 25`.
|
- ACTIVE uses `geolocation.distanceFilter: 25`.
|
||||||
- JS may request a persisted fix when entering ACTIVE (see [`applyProfile()`](src/location/bggeo/createTrackingController.js:170)).
|
- JS may request a persisted fix when entering ACTIVE (see [`applyProfile()`](src/location/trackLocation.js:351)).
|
||||||
- Upload strategy is intentionally simple:
|
- Upload strategy is intentionally simple:
|
||||||
- Keep only the latest persisted geopoint: `persistence.maxRecordsToPersist: 1`.
|
- Keep only the latest persisted geopoint: `persistence.maxRecordsToPersist: 1`.
|
||||||
- No batching / thresholds: `batchSync: false`, `autoSyncThreshold: 0`.
|
- No batching / thresholds: `batchSync: false`, `autoSyncThreshold: 0`.
|
||||||
- When authenticated, each persisted location should upload immediately via native HTTP (works while JS is suspended).
|
- When authenticated, each persisted location should upload immediately via native HTTP (works while JS is suspended).
|
||||||
- Pre-auth: BGGeo tracking is disabled (do not start). UI-only location uses `expo-location`.
|
- Pre-auth: tracking may persist locally but `http.url` is empty so nothing is uploaded until auth is ready.
|
||||||
|
|
||||||
- Stationary noise suppression:
|
- Stationary noise suppression:
|
||||||
- Native accuracy gate for persisted/uploaded locations: `geolocation.filter.trackingAccuracyThreshold: 100`.
|
- Native accuracy gate for persisted/uploaded locations: `geolocation.filter.trackingAccuracyThreshold: 100`.
|
||||||
|
|
@ -130,7 +129,7 @@ Applies to the BackgroundGeolocation integration:
|
||||||
|
|
||||||
| Platform | App state | Profile | Move | Expected signals |
|
| Platform | App state | Profile | Move | Expected signals |
|
||||||
|---|---|---|---:|---|
|
|---|---|---|---:|---|
|
||||||
| Android | foreground | IDLE | ~250m | [`onMotionChange`](src/location/bggeo/createTrackingController.js:311) then [`onLocation`](src/location/bggeo/createTrackingController.js:286) (sample=false), then [`onHttp`](src/location/bggeo/createTrackingController.js:302) |
|
| Android | foreground | IDLE | ~250m | [`onMotionChange`](src/location/trackLocation.js:1192) then [`onLocation`](src/location/trackLocation.js:1085) (sample=false), then [`onHttp`](src/location/trackLocation.js:1150) |
|
||||||
| Android | background | IDLE | ~250m | same as above |
|
| Android | background | IDLE | ~250m | same as above |
|
||||||
| Android | swipe-away | IDLE | ~250m | native geofence triggers; verify server update; app may relaunch to deliver JS logs |
|
| Android | swipe-away | IDLE | ~250m | native geofence triggers; verify server update; app may relaunch to deliver JS logs |
|
||||||
| Android | foreground | ACTIVE | ~30m | location + upload continues |
|
| Android | foreground | ACTIVE | ~30m | location + upload continues |
|
||||||
|
|
@ -139,9 +138,10 @@ Applies to the BackgroundGeolocation integration:
|
||||||
|
|
||||||
## What to look for in logs
|
## What to look for in logs
|
||||||
|
|
||||||
|
- App lifecycle tagging: [`updateTrackingContextExtras()`](src/location/trackLocation.js:63) should update `tracking_ctx.app_state` on AppState changes.
|
||||||
- No time-based uploads: heartbeat is disabled (`heartbeatInterval: 0`).
|
- No time-based uploads: heartbeat is disabled (`heartbeatInterval: 0`).
|
||||||
- Movement-only uploads:
|
- Movement-only uploads:
|
||||||
- IDLE: look for `Motion change` (isMoving=true).
|
- IDLE: look for `Motion change` (isMoving=true) and (in rare cases) `IDLE movement fallback fix`.
|
||||||
- ACTIVE distance threshold: `distanceFilter: 25` in [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:148).
|
- ACTIVE distance threshold: `distanceFilter: 25` in [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:148).
|
||||||
|
|
||||||
- Attribution for `getCurrentPosition`:
|
- Attribution for `getCurrentPosition`:
|
||||||
|
|
@ -154,10 +154,13 @@ Applies to the BackgroundGeolocation integration:
|
||||||
|
|
||||||
## Debugging tips
|
## Debugging tips
|
||||||
|
|
||||||
- Observe logs in app (dev/staging):
|
- Observe logs in app:
|
||||||
- `Motion change` edges
|
- `tracking_ctx` extras are updated on AppState changes and profile changes.
|
||||||
- `HTTP response` when uploads fail or in dev/staging
|
- See [`updateTrackingContextExtras()`](src/location/trackLocation.js:63).
|
||||||
- pending queue (`BackgroundGeolocation.getCount()` via [`bggeoGetStatusSnapshot()`](src/location/bggeo/diagnostics.js:15))
|
- Correlate:
|
||||||
|
- `onLocation` events
|
||||||
|
- `onHttp` events
|
||||||
|
- pending queue (`BackgroundGeolocation.getCount()` in logs)
|
||||||
|
|
||||||
## Android-specific note (stationary-geofence EXIT loop)
|
## Android-specific note (stationary-geofence EXIT loop)
|
||||||
|
|
||||||
|
|
@ -169,12 +172,10 @@ Mitigation applied:
|
||||||
- Android IDLE disables `geolocation.stopOnStationary` (we do **not** rely on stationary-geofence mode in IDLE on Android).
|
- Android IDLE disables `geolocation.stopOnStationary` (we do **not** rely on stationary-geofence mode in IDLE on Android).
|
||||||
- See [`BASE_GEOLOCATION_CONFIG.geolocation.stopOnStationary`](src/location/backgroundGeolocationConfig.js:1) and [`TRACKING_PROFILES.idle.geolocation.stopOnStationary`](src/location/backgroundGeolocationConfig.js:1).
|
- See [`BASE_GEOLOCATION_CONFIG.geolocation.stopOnStationary`](src/location/backgroundGeolocationConfig.js:1) and [`TRACKING_PROFILES.idle.geolocation.stopOnStationary`](src/location/backgroundGeolocationConfig.js:1).
|
||||||
|
|
||||||
- Android IDLE no longer uses `geolocation.useSignificantChangesOnly`.
|
- Android IDLE uses `geolocation.useSignificantChangesOnly: true` to rely on OS-level significant movement events.
|
||||||
- Reason: this mode can record only "several times/hour" and was observed to miss timely updates
|
- See [`TRACKING_PROFILES.idle.geolocation.useSignificantChangesOnly`](src/location/backgroundGeolocationConfig.js:1).
|
||||||
after moving ~200–300m while the app is backgrounded on some devices.
|
|
||||||
- IDLE now relies on `distanceFilter: 200` plus native drift filtering.
|
|
||||||
- See [`TRACKING_PROFILES.idle`](src/location/backgroundGeolocationConfig.js:190).
|
|
||||||
|
|
||||||
Diagnostics:
|
Diagnostics:
|
||||||
|
|
||||||
- `onGeofence` events are not explicitly logged anymore (we rely on motion/location/http + the in-app diagnostics helpers).
|
- `onGeofence` events are logged (identifier/action/accuracy + current BGGeo state) to confirm whether the SDK is emitting stationary geofence events.
|
||||||
|
- See [`setBackgroundGeolocationEventHandlers({ onGeofence })`](src/location/trackLocation.js:1).
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.16.9</string>
|
<string>1.16.8</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>240</string>
|
<string>239</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "alerte-secours",
|
"name": "alerte-secours",
|
||||||
"version": "1.16.9",
|
"version": "1.16.8",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
|
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
|
||||||
|
|
@ -54,8 +54,8 @@
|
||||||
"screenshot:android": "scripts/screenshot-android.sh"
|
"screenshot:android": "scripts/screenshot-android.sh"
|
||||||
},
|
},
|
||||||
"customExpoVersioning": {
|
"customExpoVersioning": {
|
||||||
"versionCode": 240,
|
"versionCode": 239,
|
||||||
"buildNumber": 240
|
"buildNumber": 239
|
||||||
},
|
},
|
||||||
"commit-and-tag-version": {
|
"commit-and-tag-version": {
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import env from "~/env";
|
||||||
|
|
||||||
const LOCATION_ACCURACY_GATE_M = 100;
|
const LOCATION_ACCURACY_GATE_M = 100;
|
||||||
const IS_ANDROID = Platform.OS === "android";
|
const IS_ANDROID = Platform.OS === "android";
|
||||||
const IS_DEBUG_LOGGING = __DEV__ || env.IS_STAGING;
|
|
||||||
|
|
||||||
// Native filter to reduce GPS drift and suppress stationary jitter.
|
// Native filter to reduce GPS drift and suppress stationary jitter.
|
||||||
// This is the primary mechanism to prevent unwanted persisted/uploaded points while the device
|
// This is the primary mechanism to prevent unwanted persisted/uploaded points while the device
|
||||||
|
|
@ -42,12 +41,12 @@ export const BASE_GEOLOCATION_CONFIG = {
|
||||||
|
|
||||||
// Logger config
|
// Logger config
|
||||||
logger: {
|
logger: {
|
||||||
// Logging can become large and also adds overhead.
|
// debug: true,
|
||||||
// Keep verbose logs to dev/staging.
|
// Logging can become large and also adds overhead; keep verbose logs to dev/staging.
|
||||||
debug: IS_DEBUG_LOGGING,
|
logLevel:
|
||||||
logLevel: IS_DEBUG_LOGGING
|
__DEV__ || env.IS_STAGING
|
||||||
? BackgroundGeolocation.LogLevel.Verbose
|
? BackgroundGeolocation.LogLevel.Verbose
|
||||||
: BackgroundGeolocation.LogLevel.Error,
|
: BackgroundGeolocation.LogLevel.Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Geolocation config
|
// Geolocation config
|
||||||
|
|
@ -201,10 +200,7 @@ export const TRACKING_PROFILES = {
|
||||||
// Android IDLE: rely on OS-level significant movement only.
|
// Android IDLE: rely on OS-level significant movement only.
|
||||||
// This avoids periodic wakeups/records due to poor fused-location fixes while the phone
|
// This avoids periodic wakeups/records due to poor fused-location fixes while the phone
|
||||||
// is stationary (screen-off / locked scenarios).
|
// is stationary (screen-off / locked scenarios).
|
||||||
// However, this mode can also delay updates for many minutes ("several times / hour"),
|
useSignificantChangesOnly: IS_ANDROID,
|
||||||
// resulting in missed updates after moving ~200-300m while backgrounded.
|
|
||||||
// Product requirement prefers reliability of distance-based updates in IDLE.
|
|
||||||
useSignificantChangesOnly: false,
|
|
||||||
|
|
||||||
// QA helper: allow easier validation in dev/staging while keeping production at 200m.
|
// QA helper: allow easier validation in dev/staging while keeping production at 200m.
|
||||||
stationaryRadius: 200,
|
stationaryRadius: 200,
|
||||||
|
|
@ -216,17 +212,8 @@ export const TRACKING_PROFILES = {
|
||||||
activity: {
|
activity: {
|
||||||
// Android-only: reduce false-positive motion triggers due to screen-on/unlock.
|
// Android-only: reduce false-positive motion triggers due to screen-on/unlock.
|
||||||
// (This is ignored on iOS.)
|
// (This is ignored on iOS.)
|
||||||
// 5 minutes was observed to be too aggressive and can prevent a moving transition during
|
motionTriggerDelay: 300000,
|
||||||
// normal short trips, leading to "moving but no updates".
|
|
||||||
motionTriggerDelay: IS_ANDROID ? 60000 : 0,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Android-only: require meaningful motion-activity transitions before engaging moving-state.
|
|
||||||
// This helps avoid false positives while still allowing IDLE distance-based updates.
|
|
||||||
// (Ignored on iOS.)
|
|
||||||
triggerActivities: IS_ANDROID
|
|
||||||
? "in_vehicle,on_foot,waking,running,walking,cycling"
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
geolocation: {
|
geolocation: {
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,6 @@ let lastReadyState = null;
|
||||||
let subscriptions = [];
|
let subscriptions = [];
|
||||||
let handlersSignature = null;
|
let handlersSignature = null;
|
||||||
|
|
||||||
export function clearBackgroundGeolocationEventHandlers() {
|
|
||||||
subscriptions.forEach((s) => s?.remove?.());
|
|
||||||
subscriptions = [];
|
|
||||||
handlersSignature = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ensureBackgroundGeolocationReady(
|
export async function ensureBackgroundGeolocationReady(
|
||||||
config = BASE_GEOLOCATION_CONFIG,
|
config = BASE_GEOLOCATION_CONFIG,
|
||||||
) {
|
) {
|
||||||
|
|
@ -87,7 +81,8 @@ export function setBackgroundGeolocationEventHandlers({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearBackgroundGeolocationEventHandlers();
|
subscriptions.forEach((s) => s?.remove?.());
|
||||||
|
subscriptions = [];
|
||||||
|
|
||||||
if (onLocation) {
|
if (onLocation) {
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
|
|
|
||||||
|
|
@ -1,560 +0,0 @@
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
|
||||||
import { AppState } from "react-native";
|
|
||||||
|
|
||||||
import { createLogger } from "~/lib/logger";
|
|
||||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
|
||||||
import env from "~/env";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getAlertState,
|
|
||||||
getAuthState,
|
|
||||||
getSessionState,
|
|
||||||
subscribeAlertState,
|
|
||||||
subscribeSessionState,
|
|
||||||
} from "~/stores";
|
|
||||||
|
|
||||||
import setLocationState from "~/location/setLocationState";
|
|
||||||
import { storeLocation } from "~/location/storage";
|
|
||||||
|
|
||||||
import {
|
|
||||||
BASE_GEOLOCATION_CONFIG,
|
|
||||||
BASE_GEOLOCATION_INVARIANTS,
|
|
||||||
TRACKING_PROFILES,
|
|
||||||
} from "~/location/backgroundGeolocationConfig";
|
|
||||||
|
|
||||||
import buildBackgroundGeolocationSetConfigPayload from "~/location/buildBackgroundGeolocationSetConfigPayload";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ensureBackgroundGeolocationReady,
|
|
||||||
clearBackgroundGeolocationEventHandlers,
|
|
||||||
setBackgroundGeolocationEventHandlers,
|
|
||||||
} from "~/location/backgroundGeolocationService";
|
|
||||||
|
|
||||||
// Correlation ID to differentiate multiple JS runtimes (eg full `Updates.reloadAsync()`).
|
|
||||||
const TRACKING_INSTANCE_ID = `${Date.now().toString(36)}-${Math.random()
|
|
||||||
.toString(16)
|
|
||||||
.slice(2, 8)}`;
|
|
||||||
|
|
||||||
const MOVING_EDGE_COOLDOWN_MS = 5 * 60 * 1000;
|
|
||||||
const PERSISTED_ACCURACY_GATE_M = 100;
|
|
||||||
const UI_ACCURACY_GATE_M = 200;
|
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
const shouldAllowPersistedFix = (location) => {
|
|
||||||
const acc = location?.coords?.accuracy;
|
|
||||||
return !(typeof acc === "number" && acc > PERSISTED_ACCURACY_GATE_M);
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldUseLocationForUi = (location) => {
|
|
||||||
const acc = location?.coords?.accuracy;
|
|
||||||
return !(typeof acc === "number" && acc > UI_ACCURACY_GATE_M);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a BGGeo tracking controller.
|
|
||||||
*
|
|
||||||
* Policy constraints enforced:
|
|
||||||
* - Pre-auth: BGGeo must remain stopped (no tracking). We also avoid calling `.ready()` pre-auth.
|
|
||||||
* - Authenticated: BGGeo configured with `http.url` + `Authorization` header + `autoSync:true`.
|
|
||||||
* - No time-based polling (heartbeat remains disabled).
|
|
||||||
*/
|
|
||||||
export function createTrackingController() {
|
|
||||||
const log = createLogger({
|
|
||||||
module: BACKGROUND_SCOPES.GEOLOCATION,
|
|
||||||
feature: "tracking-controller",
|
|
||||||
});
|
|
||||||
|
|
||||||
/** @type {ReturnType<typeof AppState.addEventListener> | null} */
|
|
||||||
let appStateSub = null;
|
|
||||||
let appState = AppState.currentState;
|
|
||||||
|
|
||||||
let currentProfile = null;
|
|
||||||
let authReady = false;
|
|
||||||
|
|
||||||
// Vendor constraint: never call BGGeo APIs before `.ready()`.
|
|
||||||
// This flag tracks whether we've successfully executed `.ready()` in this JS runtime.
|
|
||||||
let didReady = false;
|
|
||||||
|
|
||||||
let stopAlertSubscription = null;
|
|
||||||
let stopSessionSubscription = null;
|
|
||||||
|
|
||||||
let lastMovingEdgeAt = 0;
|
|
||||||
|
|
||||||
// Track identity so we can force a first geopoint when the effective user changes.
|
|
||||||
let lastSessionUserId = null;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
log.warn("Failed to compute active-alert state", { error: e?.message });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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++) {
|
|
||||||
try {
|
|
||||||
const [state, pendingBefore] = await Promise.all([
|
|
||||||
BackgroundGeolocation.getState(),
|
|
||||||
BackgroundGeolocation.getCount(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
log.info("Attempting BGGeo sync", {
|
|
||||||
reason,
|
|
||||||
attempt,
|
|
||||||
enabled: state?.enabled,
|
|
||||||
isMoving: state?.isMoving,
|
|
||||||
trackingMode: state?.trackingMode,
|
|
||||||
pendingBefore,
|
|
||||||
});
|
|
||||||
|
|
||||||
const records = await BackgroundGeolocation.sync();
|
|
||||||
const pendingAfter = await BackgroundGeolocation.getCount();
|
|
||||||
|
|
||||||
log.info("BGGeo sync success", {
|
|
||||||
reason,
|
|
||||||
attempt,
|
|
||||||
synced: records?.length,
|
|
||||||
pendingAfter,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("BGGeo sync failed", {
|
|
||||||
reason,
|
|
||||||
attempt,
|
|
||||||
error: e?.message,
|
|
||||||
stack: e?.stack,
|
|
||||||
});
|
|
||||||
await sleep(attempt * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCurrentPositionWithDiagnostics = async (
|
|
||||||
options,
|
|
||||||
{ reason, persist },
|
|
||||||
) => {
|
|
||||||
const opts = {
|
|
||||||
...options,
|
|
||||||
persist,
|
|
||||||
extras: {
|
|
||||||
...(options?.extras || {}),
|
|
||||||
req_reason: reason,
|
|
||||||
req_persist: !!persist,
|
|
||||||
req_at: new Date().toISOString(),
|
|
||||||
req_app_state: appState,
|
|
||||||
req_profile: currentProfile,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
log.debug("Requesting getCurrentPosition", {
|
|
||||||
reason,
|
|
||||||
persist: !!persist,
|
|
||||||
desiredAccuracy: opts?.desiredAccuracy,
|
|
||||||
samples: opts?.samples,
|
|
||||||
maximumAge: opts?.maximumAge,
|
|
||||||
timeout: opts?.timeout,
|
|
||||||
});
|
|
||||||
return BackgroundGeolocation.getCurrentPosition(opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyProfile = async (profileName) => {
|
|
||||||
if (!authReady) return;
|
|
||||||
if (currentProfile === profileName) {
|
|
||||||
// Ensure we're not stuck in geofence-only mode.
|
|
||||||
try {
|
|
||||||
const s = await BackgroundGeolocation.getState();
|
|
||||||
if (s?.trackingMode === 0) {
|
|
||||||
await BackgroundGeolocation.start();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = TRACKING_PROFILES[profileName];
|
|
||||||
if (!profile) {
|
|
||||||
log.warn("Unknown tracking profile", { profileName });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = await buildBackgroundGeolocationSetConfigPayload(profile);
|
|
||||||
await BackgroundGeolocation.setConfig(payload);
|
|
||||||
|
|
||||||
const state = await BackgroundGeolocation.getState();
|
|
||||||
if (state?.trackingMode === 0) {
|
|
||||||
await BackgroundGeolocation.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profileName === "active") {
|
|
||||||
if (!state?.isMoving) {
|
|
||||||
await BackgroundGeolocation.changePace(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACTIVE: request one immediate persisted fix to ensure first point reaches server quickly.
|
|
||||||
try {
|
|
||||||
const fix = await getCurrentPositionWithDiagnostics(
|
|
||||||
{
|
|
||||||
samples: 3,
|
|
||||||
timeout: 30,
|
|
||||||
maximumAge: 0,
|
|
||||||
desiredAccuracy: 10,
|
|
||||||
extras: { active_profile_enter: true },
|
|
||||||
},
|
|
||||||
{ reason: "active_profile_enter", persist: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!shouldAllowPersistedFix(fix)) {
|
|
||||||
log.info("ACTIVE immediate persisted fix ignored (poor accuracy)", {
|
|
||||||
accuracy: fix?.coords?.accuracy,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
lastMovingEdgeAt = Date.now();
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("ACTIVE immediate fix failed", {
|
|
||||||
error: e?.message,
|
|
||||||
stack: e?.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// IDLE: explicitly exit moving mode if needed.
|
|
||||||
if (state?.isMoving) {
|
|
||||||
await BackgroundGeolocation.changePace(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentProfile = profileName;
|
|
||||||
|
|
||||||
log.info("Tracking profile applied", {
|
|
||||||
profileName,
|
|
||||||
instanceId: TRACKING_INSTANCE_ID,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
log.error("Failed to apply tracking profile", {
|
|
||||||
profileName,
|
|
||||||
error: e?.message,
|
|
||||||
stack: e?.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeProfileInputs = () => {
|
|
||||||
if (stopSessionSubscription || stopAlertSubscription) return;
|
|
||||||
|
|
||||||
stopSessionSubscription = subscribeSessionState(
|
|
||||||
(s) => s?.userId,
|
|
||||||
() => {
|
|
||||||
const active = computeHasOwnOpenAlert();
|
|
||||||
applyProfile(active ? "active" : "idle");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
stopAlertSubscription = subscribeAlertState(
|
|
||||||
(s) => s?.alertingList,
|
|
||||||
() => {
|
|
||||||
const active = computeHasOwnOpenAlert();
|
|
||||||
applyProfile(active ? "active" : "idle");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const unsubscribeProfileInputs = () => {
|
|
||||||
try {
|
|
||||||
stopAlertSubscription && stopAlertSubscription();
|
|
||||||
} finally {
|
|
||||||
stopAlertSubscription = null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
stopSessionSubscription && stopSessionSubscription();
|
|
||||||
} finally {
|
|
||||||
stopSessionSubscription = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerEventHandlersOnceReady = () => {
|
|
||||||
setBackgroundGeolocationEventHandlers({
|
|
||||||
onLocation: async (location) => {
|
|
||||||
// Ignore sampling locations (eg, emitted during getCurrentPosition).
|
|
||||||
if (location?.sample) return;
|
|
||||||
if (!shouldUseLocationForUi(location)) return;
|
|
||||||
|
|
||||||
if (location?.coords?.latitude && location?.coords?.longitude) {
|
|
||||||
setLocationState(location.coords);
|
|
||||||
storeLocation(location.coords, location.timestamp);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLocationError: (error) => {
|
|
||||||
log.warn("Location error", {
|
|
||||||
error: error?.message,
|
|
||||||
code: error?.code,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onHttp: (response) => {
|
|
||||||
// Keep minimal; noisy logs only in dev/staging.
|
|
||||||
if (!response?.success || __DEV__ || env.IS_STAGING) {
|
|
||||||
log.debug("HTTP response", {
|
|
||||||
success: response?.success,
|
|
||||||
status: response?.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMotionChange: (event) => {
|
|
||||||
log.info("Motion change", {
|
|
||||||
instanceId: TRACKING_INSTANCE_ID,
|
|
||||||
profile: currentProfile,
|
|
||||||
appState,
|
|
||||||
authReady,
|
|
||||||
isMoving: event?.isMoving,
|
|
||||||
accuracy: event?.location?.coords?.accuracy,
|
|
||||||
speed: event?.location?.coords?.speed,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ACTIVE only: on moving edge, force one persisted fix + sync (cooldown).
|
|
||||||
if (event?.isMoving && authReady && currentProfile === "active") {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastMovingEdgeAt < MOVING_EDGE_COOLDOWN_MS) return;
|
|
||||||
lastMovingEdgeAt = now;
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const fix = await getCurrentPositionWithDiagnostics(
|
|
||||||
{
|
|
||||||
samples: 1,
|
|
||||||
timeout: 30,
|
|
||||||
maximumAge: 0,
|
|
||||||
desiredAccuracy: 50,
|
|
||||||
extras: { moving_edge: true },
|
|
||||||
},
|
|
||||||
{ reason: "moving_edge", persist: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!shouldAllowPersistedFix(fix)) {
|
|
||||||
log.info("Moving-edge persisted fix ignored (poor accuracy)", {
|
|
||||||
accuracy: fix?.coords?.accuracy,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("Moving-edge fix failed", {
|
|
||||||
error: e?.message,
|
|
||||||
stack: e?.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await safeSync("moving-edge");
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onProviderChange: (event) => {
|
|
||||||
log.info("Provider change", {
|
|
||||||
status: event?.status,
|
|
||||||
enabled: event?.enabled,
|
|
||||||
network: event?.network,
|
|
||||||
gps: event?.gps,
|
|
||||||
accuracyAuthorization: event?.accuracyAuthorization,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureReadyAndApplyInvariants = async () => {
|
|
||||||
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
|
||||||
didReady = true;
|
|
||||||
|
|
||||||
// Ensure critical config cannot drift due to persisted plugin state.
|
|
||||||
// (We intentionally keep auth headers separate and set them in handleAuthToken.)
|
|
||||||
const payload = await buildBackgroundGeolocationSetConfigPayload(
|
|
||||||
BASE_GEOLOCATION_INVARIANTS,
|
|
||||||
);
|
|
||||||
await BackgroundGeolocation.setConfig(payload);
|
|
||||||
|
|
||||||
registerEventHandlersOnceReady();
|
|
||||||
};
|
|
||||||
|
|
||||||
const configureUploadsForAuth = async (token) => {
|
|
||||||
const payload = await buildBackgroundGeolocationSetConfigPayload({
|
|
||||||
http: {
|
|
||||||
url: env.GEOLOC_SYNC_URL,
|
|
||||||
autoSync: true,
|
|
||||||
batchSync: false,
|
|
||||||
autoSyncThreshold: 0,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await BackgroundGeolocation.setConfig(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableUploads = async () => {
|
|
||||||
const payload = await buildBackgroundGeolocationSetConfigPayload({
|
|
||||||
http: {
|
|
||||||
url: "",
|
|
||||||
autoSync: false,
|
|
||||||
batchSync: false,
|
|
||||||
autoSyncThreshold: 0,
|
|
||||||
headers: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await BackgroundGeolocation.setConfig(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureStarted = async () => {
|
|
||||||
const state = await BackgroundGeolocation.getState();
|
|
||||||
if (!state?.enabled) {
|
|
||||||
await BackgroundGeolocation.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra guard against geofence-only mode.
|
|
||||||
const s2 = await BackgroundGeolocation.getState();
|
|
||||||
if (s2?.trackingMode === 0) {
|
|
||||||
await BackgroundGeolocation.start();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopAndDetach = async () => {
|
|
||||||
try {
|
|
||||||
// Stop native service first (policy: no tracking while logged-out).
|
|
||||||
if (didReady) {
|
|
||||||
await BackgroundGeolocation.stop();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log.debug("BGGeo stop failed (ignored)", { error: e?.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
unsubscribeProfileInputs();
|
|
||||||
clearBackgroundGeolocationEventHandlers();
|
|
||||||
authReady = false;
|
|
||||||
currentProfile = null;
|
|
||||||
lastSessionUserId = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAuthToken = async (token) => {
|
|
||||||
const sessionUserId = (() => {
|
|
||||||
try {
|
|
||||||
return getSessionState()?.userId ?? null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (!token || !sessionUserId) {
|
|
||||||
// Pre-auth policy: BGGeo must remain stopped.
|
|
||||||
log.info("No auth: ensuring BGGeo is stopped", {
|
|
||||||
hasToken: !!token,
|
|
||||||
hasSessionUserId: !!sessionUserId,
|
|
||||||
instanceId: TRACKING_INSTANCE_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Safety net: if BGGeo was previously enabled (eg user logs out, or a prior run left
|
|
||||||
// tracking enabled), remove upload credentials and stop native tracking.
|
|
||||||
//
|
|
||||||
// NOTE: This calls `.ready()` to comply with vendor rules, but does NOT start tracking.
|
|
||||||
try {
|
|
||||||
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
|
||||||
didReady = true;
|
|
||||||
await disableUploads();
|
|
||||||
} catch (e) {
|
|
||||||
log.debug("Failed to ready/disable uploads during logout", {
|
|
||||||
error: e?.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await stopAndDetach();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticated path.
|
|
||||||
log.info("Auth ready: configuring and starting BGGeo", {
|
|
||||||
instanceId: TRACKING_INSTANCE_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
await ensureReadyAndApplyInvariants();
|
|
||||||
await configureUploadsForAuth(token);
|
|
||||||
|
|
||||||
authReady = true;
|
|
||||||
|
|
||||||
await ensureStarted();
|
|
||||||
|
|
||||||
// Identity change: force a persisted fix + sync for a fast first point.
|
|
||||||
if (sessionUserId !== lastSessionUserId) {
|
|
||||||
const reason = lastSessionUserId ? "user-switch" : "first-login";
|
|
||||||
lastSessionUserId = sessionUserId;
|
|
||||||
try {
|
|
||||||
const fix = await getCurrentPositionWithDiagnostics(
|
|
||||||
{
|
|
||||||
samples: 1,
|
|
||||||
timeout: 30,
|
|
||||||
maximumAge: 0,
|
|
||||||
desiredAccuracy: 50,
|
|
||||||
extras: {
|
|
||||||
identity_fix: true,
|
|
||||||
identity_reason: reason,
|
|
||||||
session_user_id: sessionUserId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ reason: `identity_fix:${reason}`, persist: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!shouldAllowPersistedFix(fix)) {
|
|
||||||
log.info("Identity persisted fix ignored (poor accuracy)", {
|
|
||||||
accuracy: fix?.coords?.accuracy,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("Identity persisted fix failed", {
|
|
||||||
error: e?.message,
|
|
||||||
stack: e?.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await safeSync(`identity-fix:${reason}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the right profile and subscribe to future changes.
|
|
||||||
await applyProfile(computeHasOwnOpenAlert() ? "active" : "idle");
|
|
||||||
subscribeProfileInputs();
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
log.info("Tracking controller init", {
|
|
||||||
instanceId: TRACKING_INSTANCE_ID,
|
|
||||||
appState,
|
|
||||||
});
|
|
||||||
|
|
||||||
// AppState listener does not call BGGeo; safe pre-auth.
|
|
||||||
try {
|
|
||||||
appStateSub = AppState.addEventListener("change", (next) => {
|
|
||||||
appState = next;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
log.debug("Failed to register AppState listener", { error: e?.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: we intentionally do NOT call `.ready()` here (pre-auth policy).
|
|
||||||
};
|
|
||||||
|
|
||||||
const destroy = async () => {
|
|
||||||
try {
|
|
||||||
appStateSub?.remove?.();
|
|
||||||
} finally {
|
|
||||||
appStateSub = null;
|
|
||||||
}
|
|
||||||
await stopAndDetach();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
init,
|
|
||||||
destroy,
|
|
||||||
handleAuthToken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
|
||||||
|
|
||||||
import { ensureBackgroundGeolocationReady } from "~/location/backgroundGeolocationService";
|
|
||||||
import { BASE_GEOLOCATION_CONFIG } from "~/location/backgroundGeolocationConfig";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BGGeo diagnostics helpers.
|
|
||||||
*
|
|
||||||
* Intentionally lives outside UI code so we don't scatter direct BGGeo calls.
|
|
||||||
*
|
|
||||||
* NOTE: Calling these will execute `.ready()` (vendor requirement), but they do not start
|
|
||||||
* tracking by themselves.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function bggeoGetStatusSnapshot() {
|
|
||||||
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
|
||||||
const [state, count] = await Promise.all([
|
|
||||||
BackgroundGeolocation.getState(),
|
|
||||||
BackgroundGeolocation.getCount(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: !!state?.enabled,
|
|
||||||
isMoving: !!state?.isMoving,
|
|
||||||
trackingMode: state?.trackingMode ?? null,
|
|
||||||
schedulerEnabled: !!state?.schedulerEnabled,
|
|
||||||
pending: count ?? null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bggeoSyncNow() {
|
|
||||||
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
|
||||||
const pendingBefore = await BackgroundGeolocation.getCount();
|
|
||||||
const records = await BackgroundGeolocation.sync();
|
|
||||||
const pendingAfter = await BackgroundGeolocation.getCount();
|
|
||||||
|
|
||||||
return {
|
|
||||||
pendingBefore: pendingBefore ?? null,
|
|
||||||
synced: records?.length ?? 0,
|
|
||||||
pendingAfter: pendingAfter ?? null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bggeoGetDiagnosticsSnapshot() {
|
|
||||||
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
|
||||||
|
|
||||||
const [state, count, locations] = await Promise.all([
|
|
||||||
BackgroundGeolocation.getState(),
|
|
||||||
BackgroundGeolocation.getCount(),
|
|
||||||
BackgroundGeolocation.getLocations(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const last =
|
|
||||||
Array.isArray(locations) && locations.length
|
|
||||||
? locations[locations.length - 1]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
state: {
|
|
||||||
enabled: !!state?.enabled,
|
|
||||||
isMoving: !!state?.isMoving,
|
|
||||||
trackingMode: state?.trackingMode ?? null,
|
|
||||||
schedulerEnabled: !!state?.schedulerEnabled,
|
|
||||||
},
|
|
||||||
pending: count ?? null,
|
|
||||||
lastLocation: last
|
|
||||||
? {
|
|
||||||
latitude: last?.coords?.latitude ?? null,
|
|
||||||
longitude: last?.coords?.longitude ?? null,
|
|
||||||
accuracy: last?.coords?.accuracy ?? null,
|
|
||||||
timestamp: last?.timestamp ?? null,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -4,8 +4,6 @@ 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 { getAuthState } from "~/stores";
|
|
||||||
|
|
||||||
import { ensureBackgroundGeolocationReady } from "~/location/backgroundGeolocationService";
|
import { ensureBackgroundGeolocationReady } from "~/location/backgroundGeolocationService";
|
||||||
import { BASE_GEOLOCATION_CONFIG } from "~/location/backgroundGeolocationConfig";
|
import { BASE_GEOLOCATION_CONFIG } from "~/location/backgroundGeolocationConfig";
|
||||||
|
|
||||||
|
|
@ -48,16 +46,6 @@ export const enableEmulatorMode = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { userToken } = getAuthState();
|
|
||||||
if (!userToken) {
|
|
||||||
emulatorLogger.warn(
|
|
||||||
"Emulator mode requires authentication (BGGeo is disabled pre-auth)",
|
|
||||||
);
|
|
||||||
isEmulatorModeEnabled = false;
|
|
||||||
await AsyncStorage.setItem(STORAGE_KEYS.EMULATOR_MODE_ENABLED, "false");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
||||||
|
|
||||||
// Call immediately once
|
// Call immediately once
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
import { Alert } from "react-native";
|
import { Alert } from "react-native";
|
||||||
import * as Location from "expo-location";
|
|
||||||
|
|
||||||
import { getLocationState } from "~/stores";
|
import { getLocationState } from "~/stores";
|
||||||
|
|
||||||
|
|
@ -7,6 +7,11 @@ import openSettings from "~/lib/native/openSettings";
|
||||||
|
|
||||||
import setLocationState from "./setLocationState";
|
import setLocationState from "./setLocationState";
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
@ -15,11 +20,15 @@ export async function getCurrentLocation() {
|
||||||
|
|
||||||
while (retries < MAX_RETRIES) {
|
while (retries < MAX_RETRIES) {
|
||||||
try {
|
try {
|
||||||
// UI-only location must NOT depend on BGGeo.
|
// Vendor requirement: never call APIs like getState/requestPermission/getCurrentPosition
|
||||||
// Policy: pre-auth, BGGeo remains completely unused.
|
// before `.ready()` has resolved.
|
||||||
|
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
||||||
|
|
||||||
const servicesEnabled = await Location.hasServicesEnabledAsync();
|
// Check for location permissions and services
|
||||||
if (!servicesEnabled) {
|
const state = await BackgroundGeolocation.getState();
|
||||||
|
|
||||||
|
if (!state.enabled) {
|
||||||
|
// Prompt the user to enable location services manually
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Services de localisation désactivés",
|
"Services de localisation désactivés",
|
||||||
"Veuillez activer les services de localisation pour utiliser cette fonctionnalité.",
|
"Veuillez activer les services de localisation pour utiliser cette fonctionnalité.",
|
||||||
|
|
@ -30,16 +39,17 @@ export async function getCurrentLocation() {
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const authorizationStatus =
|
||||||
|
await BackgroundGeolocation.requestPermission();
|
||||||
|
|
||||||
const perm = await Location.getForegroundPermissionsAsync();
|
const isAuthorized =
|
||||||
let status = perm?.status;
|
authorizationStatus ===
|
||||||
|
BackgroundGeolocation.AuthorizationStatus?.Always ||
|
||||||
|
authorizationStatus ===
|
||||||
|
BackgroundGeolocation.AuthorizationStatus?.WhenInUse;
|
||||||
|
|
||||||
if (status !== "granted") {
|
if (!isAuthorized) {
|
||||||
const req = await Location.requestForegroundPermissionsAsync();
|
// If unable to get permissions, provide a link to settings
|
||||||
status = req?.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status !== "granted") {
|
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Autorisation de localisation requise",
|
"Autorisation de localisation requise",
|
||||||
"Veuillez accorder l'autorisation de localisation pour utiliser cette fonctionnalité.",
|
"Veuillez accorder l'autorisation de localisation pour utiliser cette fonctionnalité.",
|
||||||
|
|
@ -51,27 +61,18 @@ export async function getCurrentLocation() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a lightweight timeout wrapper to avoid hanging UI.
|
// UI lookup: do not persist. Persisting can create a DB record and trigger
|
||||||
const TIMEOUT_MS = 30000;
|
// native HTTP upload even if the user has not moved.
|
||||||
const timeout = new Promise((_, reject) =>
|
const location = await BackgroundGeolocation.getCurrentPosition({
|
||||||
setTimeout(() => reject(new Error("Location timeout")), TIMEOUT_MS),
|
timeout: 30,
|
||||||
);
|
persist: false,
|
||||||
|
maximumAge: 5000,
|
||||||
const loc = await Promise.race([
|
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
|
||||||
Location.getCurrentPositionAsync({
|
samples: 1,
|
||||||
accuracy: Location.Accuracy.High,
|
});
|
||||||
mayShowUserSettingsDialog: false,
|
const coords = camelCaseKeys(location.coords);
|
||||||
}),
|
setLocationState(coords);
|
||||||
timeout,
|
return coords;
|
||||||
]);
|
|
||||||
|
|
||||||
const coords = loc?.coords;
|
|
||||||
if (coords) {
|
|
||||||
setLocationState(coords);
|
|
||||||
return coords;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(
|
console.log(
|
||||||
`Erreur lors de l'obtention de la position actuelle (tentative ${
|
`Erreur lors de l'obtention de la position actuelle (tentative ${
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { View, StyleSheet, ScrollView } from "react-native";
|
import { View, StyleSheet, ScrollView } from "react-native";
|
||||||
import * as Sentry from "@sentry/react-native";
|
import * as Sentry from "@sentry/react-native";
|
||||||
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -21,11 +22,8 @@ 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 {
|
import { ensureBackgroundGeolocationReady } from "~/location/backgroundGeolocationService";
|
||||||
bggeoGetDiagnosticsSnapshot,
|
import { BASE_GEOLOCATION_CONFIG } from "~/location/backgroundGeolocationConfig";
|
||||||
bggeoGetStatusSnapshot,
|
|
||||||
bggeoSyncNow,
|
|
||||||
} from "~/location/bggeo/diagnostics";
|
|
||||||
|
|
||||||
const reset = async () => {
|
const reset = async () => {
|
||||||
await authActions.logout();
|
await authActions.logout();
|
||||||
|
|
@ -50,8 +48,6 @@ export default function Developer() {
|
||||||
const [syncResult, setSyncResult] = useState("");
|
const [syncResult, setSyncResult] = useState("");
|
||||||
const [bgGeoStatus, setBgGeoStatus] = useState(null); // null, 'loading', 'success', 'error'
|
const [bgGeoStatus, setBgGeoStatus] = useState(null); // null, 'loading', 'success', 'error'
|
||||||
const [bgGeoResult, setBgGeoResult] = useState("");
|
const [bgGeoResult, setBgGeoResult] = useState("");
|
||||||
const [bgGeoDiagStatus, setBgGeoDiagStatus] = useState(null);
|
|
||||||
const [bgGeoDiagResult, setBgGeoDiagResult] = useState("");
|
|
||||||
const [logLevel, setLogLevel] = useState(LOG_LEVELS.DEBUG);
|
const [logLevel, setLogLevel] = useState(LOG_LEVELS.DEBUG);
|
||||||
|
|
||||||
// Initialize emulator mode and log level when component mounts
|
// Initialize emulator mode and log level when component mounts
|
||||||
|
|
@ -84,18 +80,25 @@ export default function Developer() {
|
||||||
setSyncStatus("syncing");
|
setSyncStatus("syncing");
|
||||||
setSyncResult("");
|
setSyncResult("");
|
||||||
|
|
||||||
const [{ enabled, isMoving, trackingMode }, sync] = await Promise.all([
|
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
||||||
bggeoGetStatusSnapshot(),
|
|
||||||
bggeoSyncNow(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = `Synced ${String(sync?.synced)} records (${String(
|
const state = await BackgroundGeolocation.getState();
|
||||||
sync?.pendingBefore,
|
|
||||||
)} pending before, ${String(
|
// Get the count of pending records first
|
||||||
sync?.pendingAfter,
|
const count = await BackgroundGeolocation.getCount();
|
||||||
)} pending after). enabled=${String(enabled)} isMoving=${String(
|
|
||||||
isMoving,
|
// Perform the sync
|
||||||
)} trackingMode=${String(trackingMode)}`;
|
const records = await BackgroundGeolocation.sync();
|
||||||
|
|
||||||
|
const pendingAfter = await BackgroundGeolocation.getCount();
|
||||||
|
|
||||||
|
const result = `Synced ${
|
||||||
|
records?.length || 0
|
||||||
|
} records (${count} pending before, ${pendingAfter} pending after). enabled=${String(
|
||||||
|
state?.enabled,
|
||||||
|
)} isMoving=${String(state?.isMoving)} trackingMode=${String(
|
||||||
|
state?.trackingMode,
|
||||||
|
)}`;
|
||||||
setSyncResult(result);
|
setSyncResult(result);
|
||||||
setSyncStatus("success");
|
setSyncStatus("success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -110,12 +113,17 @@ export default function Developer() {
|
||||||
setBgGeoStatus("loading");
|
setBgGeoStatus("loading");
|
||||||
setBgGeoResult("");
|
setBgGeoResult("");
|
||||||
|
|
||||||
const snap = await bggeoGetStatusSnapshot();
|
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
|
||||||
const result = `enabled=${String(snap?.enabled)} isMoving=${String(
|
const [state, count] = await Promise.all([
|
||||||
snap?.isMoving,
|
BackgroundGeolocation.getState(),
|
||||||
)} trackingMode=${String(snap?.trackingMode)} schedulerEnabled=${String(
|
BackgroundGeolocation.getCount(),
|
||||||
snap?.schedulerEnabled,
|
]);
|
||||||
)} pending=${String(snap?.pending)}`;
|
|
||||||
|
const result = `enabled=${String(state?.enabled)} isMoving=${String(
|
||||||
|
state?.isMoving,
|
||||||
|
)} trackingMode=${String(state?.trackingMode)} schedulerEnabled=${String(
|
||||||
|
state?.schedulerEnabled,
|
||||||
|
)} pending=${String(count)}`;
|
||||||
setBgGeoResult(result);
|
setBgGeoResult(result);
|
||||||
setBgGeoStatus("success");
|
setBgGeoStatus("success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -124,40 +132,6 @@ export default function Developer() {
|
||||||
setBgGeoStatus("error");
|
setBgGeoStatus("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Diagnostics: provide a single snapshot that helps debug "no updates" without logcat.
|
|
||||||
// Includes state + pending queue + last persisted locations (if any).
|
|
||||||
const showBgGeoDiagnostics = async () => {
|
|
||||||
try {
|
|
||||||
setBgGeoDiagStatus("loading");
|
|
||||||
setBgGeoDiagResult("");
|
|
||||||
|
|
||||||
const diag = await bggeoGetDiagnosticsSnapshot();
|
|
||||||
|
|
||||||
const last = diag?.lastLocation;
|
|
||||||
const lastStr = last
|
|
||||||
? `last={lat:${String(last.latitude).slice(0, 10)} lng:${String(
|
|
||||||
last.longitude,
|
|
||||||
).slice(0, 10)} acc:${String(last.accuracy)} ts:${String(
|
|
||||||
last.timestamp,
|
|
||||||
)}}`
|
|
||||||
: "last=null";
|
|
||||||
|
|
||||||
const state = diag?.state;
|
|
||||||
const result = `enabled=${String(state?.enabled)} isMoving=${String(
|
|
||||||
state?.isMoving,
|
|
||||||
)} trackingMode=${String(state?.trackingMode)} schedulerEnabled=${String(
|
|
||||||
state?.schedulerEnabled,
|
|
||||||
)} pending=${String(diag?.pending)} ${lastStr}`;
|
|
||||||
|
|
||||||
setBgGeoDiagResult(result);
|
|
||||||
setBgGeoDiagStatus("success");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("BGGeo diagnostics failed:", error);
|
|
||||||
setBgGeoDiagResult(`Diagnostics failed: ${error.message}`);
|
|
||||||
setBgGeoDiagStatus("error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const triggerNullError = () => {
|
const triggerNullError = () => {
|
||||||
try {
|
try {
|
||||||
// Wrap the null error in try-catch
|
// Wrap the null error in try-catch
|
||||||
|
|
@ -238,23 +212,6 @@ export default function Developer() {
|
||||||
</View>
|
</View>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="BGGeo Diagnostics (no logcat)">
|
|
||||||
<Button
|
|
||||||
mode="contained"
|
|
||||||
onPress={showBgGeoDiagnostics}
|
|
||||||
loading={bgGeoDiagStatus === "loading"}
|
|
||||||
disabled={bgGeoDiagStatus === "loading"}
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
>
|
|
||||||
Show BGGeo diagnostics
|
|
||||||
</Button>
|
|
||||||
{bgGeoDiagResult ? (
|
|
||||||
<Text selectable variant="bodySmall">
|
|
||||||
{bgGeoDiagResult}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section title="Logging Controls">
|
<Section title="Logging Controls">
|
||||||
<Text variant="bodyLarge" style={styles.sectionLabel}>
|
<Text variant="bodyLarge" style={styles.sectionLabel}>
|
||||||
Log Level
|
Log Level
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue