Compare commits

...

2 commits

Author SHA1 Message Date
8e81b1fa73
chore(release): 1.16.5 2026-01-20 23:25:29 +01:00
a2acbb6d0b
fix(android): track location battery saving 2026-01-20 23:25:09 +01:00
9 changed files with 217 additions and 20 deletions

View file

@ -2,6 +2,13 @@
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.5](https://github.com/alerte-secours/as-app/compare/v1.16.4...v1.16.5) (2026-01-20)
### Bug Fixes
* **android:** track location battery saving ([a2acbb6](https://github.com/alerte-secours/as-app/commit/a2acbb6d0b19d36803b4130c7360ba0a090ee5cb))
## [1.16.4](https://github.com/alerte-secours/as-app/compare/v1.16.3...v1.16.4) (2026-01-20) ## [1.16.4](https://github.com/alerte-secours/as-app/compare/v1.16.3...v1.16.4) (2026-01-20)

View file

@ -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 235 versionCode 236
versionName "1.16.4" versionName "1.16.5"
multiDexEnabled true multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -3,6 +3,8 @@ import "./warnFilter";
import "expo-splash-screen"; import "expo-splash-screen";
import BackgroundGeolocation from "react-native-background-geolocation";
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";
@ -19,6 +21,17 @@ import onMessageReceived from "~/notifications/onMessageReceived";
notifee.onBackgroundEvent(notificationBackgroundEvent); notifee.onBackgroundEvent(notificationBackgroundEvent);
messaging().setBackgroundMessageHandler(onMessageReceived); messaging().setBackgroundMessageHandler(onMessageReceived);
// Android Headless Mode for react-native-background-geolocation.
// Required because [`enableHeadless`](src/location/backgroundGeolocationConfig.js:16) is enabled and
// we run with [`stopOnTerminate: false`](src/location/backgroundGeolocationConfig.js:40).
//
// IMPORTANT: keep this handler lightweight. In headless state, the JS runtime may be launched
// briefly and then torn down; long tasks can be terminated by the OS.
BackgroundGeolocation.registerHeadlessTask(async (event) => {
// eslint-disable-next-line no-console
console.log("[BGGeo HeadlessTask]", event?.name, event?.params);
});
// registerRootComponent calls AppRegistry.registerComponent('main', () => App); // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// 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

View file

@ -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.4</string> <string>1.16.5</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>235</string> <string>236</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>

View file

@ -1,6 +1,6 @@
{ {
"name": "alerte-secours", "name": "alerte-secours",
"version": "1.16.4", "version": "1.16.5",
"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",
@ -53,8 +53,8 @@
"screenshot:android": "scripts/screenshot-android.sh" "screenshot:android": "scripts/screenshot-android.sh"
}, },
"customExpoVersioning": { "customExpoVersioning": {
"versionCode": 235, "versionCode": 236,
"buildNumber": 235 "buildNumber": 236
}, },
"commit-and-tag-version": { "commit-and-tag-version": {
"scripts": { "scripts": {

View file

@ -1,10 +1,13 @@
import BackgroundGeolocation from "react-native-background-geolocation"; import BackgroundGeolocation from "react-native-background-geolocation";
import { TRACK_MOVE } from "~/misc/devicePrefs";
import env from "~/env"; import env from "~/env";
// Common config: keep always-on tracking enabled, but default to an IDLE low-power profile. // 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. // High-accuracy and moving mode are enabled only when an active alert is open.
// //
// Product goals:
// - IDLE (no open alert): minimize battery; server updates are acceptable only on OS-level significant movement.
// - ACTIVE (open alert): first location should reach server within seconds, then continuous distance-based updates.
//
// Notes: // Notes:
// - We avoid `reset: true` in production because it can unintentionally wipe persisted / configured state. // - 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. // In dev, `reset: true` is useful to avoid config drift while iterating.
@ -18,10 +21,21 @@ export const BASE_GEOLOCATION_CONFIG = {
// Default to the IDLE profile behaviour: we still want distance-based updates // Default to the IDLE profile behaviour: we still want distance-based updates
// even with no open alert (see TRACKING_PROFILES.idle). // even with no open alert (see TRACKING_PROFILES.idle).
distanceFilter: 50, distanceFilter: 200,
// Activity-recognition stop-detection.
// NOTE: Transistorsoft defaults `stopTimeout` to 5 minutes (see
// [`node_modules/react-native-background-geolocation/src/declarations/interfaces/Config.d.ts:79`](node_modules/react-native-background-geolocation/src/declarations/interfaces/Config.d.ts:79)).
// We keep the default in BASE and override it in the IDLE profile to reduce
// 5-minute stationary cycles observed on Android.
stopTimeout: 5,
// debug: true, // debug: true,
logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE, // Logging can become large and also adds overhead; keep verbose logs to dev/staging.
logLevel:
__DEV__ || env.IS_STAGING
? BackgroundGeolocation.LOG_LEVEL_VERBOSE
: BackgroundGeolocation.LOG_LEVEL_ERROR,
// Permission request strategy // Permission request strategy
locationAuthorizationRequest: "Always", locationAuthorizationRequest: "Always",
@ -31,7 +45,9 @@ export const BASE_GEOLOCATION_CONFIG = {
startOnBoot: true, startOnBoot: true,
// Background scheduling // Background scheduling
heartbeatInterval: 3600, // Disable heartbeats by default to avoid periodic background wakeups while stationary.
// ACTIVE profile will explicitly enable a fast heartbeat when needed.
heartbeatInterval: 0,
// Android foreground service // Android foreground service
foregroundService: true, foregroundService: true,
@ -71,15 +87,52 @@ export const BASE_GEOLOCATION_CONFIG = {
disableProviderChangeRecord: true, disableProviderChangeRecord: true,
}; };
// Options we want to be stable across launches even when the plugin loads a persisted config.
// NOTE: We intentionally do *not* include HTTP auth headers here.
export const BASE_GEOLOCATION_INVARIANTS = {
enableHeadless: true,
stopOnTerminate: false,
startOnBoot: true,
foregroundService: true,
disableProviderChangeRecord: true,
// Filter extreme GPS teleports that can create false uploads while stationary.
// Units: meters/second. 100 m/s ~= 360 km/h.
speedJumpFilter: 100,
method: "POST",
httpRootProperty: "location",
maxRecordsToPersist: 1000,
maxDaysToPersist: 7,
};
export const TRACKING_PROFILES = { export const TRACKING_PROFILES = {
idle: { idle: {
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW, desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
distanceFilter: 50, // Max battery-saving strategy for IDLE:
heartbeatInterval: 3600, // Use Android/iOS low-power significant-change tracking where the OS produces
// only periodic fixes (several times/hour). Note many config options like
// `distanceFilter` / `stationaryRadius` are documented as having little/no
// effect in this mode.
useSignificantChangesOnly: true,
// Defensive: if some devices/platform conditions fall back to standard tracking,
// keep the distanceFilter conservative to avoid battery drain.
distanceFilter: 200,
// Keep the default stop-detection timing (minutes). In significant-changes
// mode, stop-detection is not the primary driver of updates.
stopTimeout: 5,
// No periodic wakeups while idle.
heartbeatInterval: 0,
}, },
active: { active: {
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH, desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
distanceFilter: TRACK_MOVE, // Ensure we exit significant-changes mode when switching from IDLE.
heartbeatInterval: 900, useSignificantChangesOnly: false,
distanceFilter: 50,
heartbeatInterval: 60,
// Keep default responsiveness during an active alert.
stopTimeout: 5,
}, },
}; };

View file

@ -54,6 +54,8 @@ export function setBackgroundGeolocationEventHandlers({
onLocation, onLocation,
onLocationError, onLocationError,
onHttp, onHttp,
onHeartbeat,
onSchedule,
onMotionChange, onMotionChange,
onActivityChange, onActivityChange,
onProviderChange, onProviderChange,
@ -65,6 +67,8 @@ export function setBackgroundGeolocationEventHandlers({
const sig = [ const sig = [
onLocation ? "L1" : "L0", onLocation ? "L1" : "L0",
onHttp ? "H1" : "H0", onHttp ? "H1" : "H0",
onHeartbeat ? "HB1" : "HB0",
onSchedule ? "S1" : "S0",
onMotionChange ? "M1" : "M0", onMotionChange ? "M1" : "M0",
onActivityChange ? "A1" : "A0", onActivityChange ? "A1" : "A0",
onProviderChange ? "P1" : "P0", onProviderChange ? "P1" : "P0",
@ -86,6 +90,22 @@ export function setBackgroundGeolocationEventHandlers({
if (onHttp) { if (onHttp) {
subscriptions.push(BackgroundGeolocation.onHttp(onHttp)); subscriptions.push(BackgroundGeolocation.onHttp(onHttp));
} }
if (onHeartbeat) {
if (typeof BackgroundGeolocation.onHeartbeat === "function") {
subscriptions.push(BackgroundGeolocation.onHeartbeat(onHeartbeat));
} else {
bgGeoLogger.warn("BackgroundGeolocation.onHeartbeat is not available");
}
}
if (onSchedule) {
if (typeof BackgroundGeolocation.onSchedule === "function") {
subscriptions.push(BackgroundGeolocation.onSchedule(onSchedule));
} else {
bgGeoLogger.warn("BackgroundGeolocation.onSchedule is not available");
}
}
if (onMotionChange) { if (onMotionChange) {
subscriptions.push(BackgroundGeolocation.onMotionChange(onMotionChange)); subscriptions.push(BackgroundGeolocation.onMotionChange(onMotionChange));
} }

View file

@ -21,6 +21,7 @@ import env from "~/env";
import { import {
BASE_GEOLOCATION_CONFIG, BASE_GEOLOCATION_CONFIG,
BASE_GEOLOCATION_INVARIANTS,
TRACKING_PROFILES, TRACKING_PROFILES,
} from "~/location/backgroundGeolocationConfig"; } from "~/location/backgroundGeolocationConfig";
import { import {
@ -129,6 +130,8 @@ export default function trackLocation() {
} }
if (currentProfile === profileName) return; if (currentProfile === profileName) return;
const applyStartedAt = Date.now();
const profile = TRACKING_PROFILES[profileName]; const profile = TRACKING_PROFILES[profileName];
if (!profile) { if (!profile) {
locationLogger.warn("Unknown tracking profile", { profileName }); locationLogger.warn("Unknown tracking profile", { profileName });
@ -147,14 +150,71 @@ export default function trackLocation() {
// Motion state strategy: // Motion state strategy:
// - ACTIVE: force moving to begin aggressive tracking immediately. // - ACTIVE: force moving to begin aggressive tracking immediately.
// - IDLE: do NOT force stationary. Let the SDK's motion detection manage // - IDLE: ensure we are not stuck in moving mode from a prior ACTIVE session.
// moving/stationary transitions so we still get distance-based updates // We explicitly exit moving mode to avoid periodic drift-generated locations
// (target: new point when moved ~50m+ even without an open alert). // being produced + uploaded while the user is stationary (reported on Android).
// After that, let the SDK's motion detection manage moving/stationary
// transitions so we still get distance-based updates when the user truly moves.
if (profileName === "active") { if (profileName === "active") {
await BackgroundGeolocation.changePace(true); const state = await BackgroundGeolocation.getState();
if (!state?.isMoving) {
await BackgroundGeolocation.changePace(true);
}
// Guarantee a rapid first fix for ACTIVE: request a high-accuracy persisted location
// immediately after entering moving mode. This is preferred over relying solely on
// motion-detection / distanceFilter to produce the first point.
try {
const beforeFix = Date.now();
const fix = await BackgroundGeolocation.getCurrentPosition({
samples: 3,
persist: true,
timeout: 30,
maximumAge: 0,
desiredAccuracy: 10,
extras: {
active_profile_enter: true,
},
});
locationLogger.info("ACTIVE immediate fix acquired", {
ms: Date.now() - beforeFix,
accuracy: fix?.coords?.accuracy,
latitude: fix?.coords?.latitude,
longitude: fix?.coords?.longitude,
timestamp: fix?.timestamp,
});
} catch (error) {
locationLogger.warn("ACTIVE immediate fix failed", {
error: error?.message,
code: error?.code,
stack: error?.stack,
});
}
} else {
const state = await BackgroundGeolocation.getState();
if (state?.isMoving) {
await BackgroundGeolocation.changePace(false);
}
} }
currentProfile = profileName; currentProfile = profileName;
try {
const state = await BackgroundGeolocation.getState();
locationLogger.info("Tracking profile applied", {
profileName,
ms: Date.now() - applyStartedAt,
enabled: state?.enabled,
isMoving: state?.isMoving,
trackingMode: state?.trackingMode,
});
} catch (e) {
locationLogger.debug("Tracking profile applied (state unavailable)", {
profileName,
ms: Date.now() - applyStartedAt,
error: e?.message,
});
}
} catch (error) { } catch (error) {
locationLogger.error("Failed to apply tracking profile", { locationLogger.error("Failed to apply tracking profile", {
profileName, profileName,
@ -338,6 +398,40 @@ export default function trackLocation() {
status: response?.status, status: response?.status,
responseText: response?.responseText, responseText: response?.responseText,
}); });
// Instrumentation: when we see periodic HTTP without a corresponding location event,
// we want to know if BGGeo is retrying an upload queue or flushing new records.
// This helps diagnose reports like "server receives updates every ~5 minutes while stationary".
try {
const [state, count] = await Promise.all([
BackgroundGeolocation.getState(),
BackgroundGeolocation.getCount(),
]);
locationLogger.debug("HTTP instrumentation", {
enabled: state?.enabled,
isMoving: state?.isMoving,
trackingMode: state?.trackingMode,
schedulerEnabled: state?.schedulerEnabled,
pendingCount: count,
});
} catch (e) {
locationLogger.warn("Failed HTTP instrumentation", {
error: e?.message,
});
}
},
onHeartbeat: (event) => {
// If heartbeat is configured, it can trigger sync attempts even without new locations.
locationLogger.info("Heartbeat", {
enabled: event?.state?.enabled,
isMoving: event?.state?.isMoving,
location: event?.location?.coords,
});
},
onSchedule: (event) => {
locationLogger.info("Schedule", {
state: event?.state,
});
}, },
onMotionChange: (event) => { onMotionChange: (event) => {
locationLogger.info("Motion change", { locationLogger.info("Motion change", {
@ -374,6 +468,17 @@ export default function trackLocation() {
locationLogger.info("Initializing background geolocation"); locationLogger.info("Initializing background geolocation");
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG); await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
// Ensure critical config cannot drift due to persisted plugin state.
// (We intentionally keep auth headers separate and set them in handleAuth.)
try {
await BackgroundGeolocation.setConfig(BASE_GEOLOCATION_INVARIANTS);
} catch (e) {
locationLogger.warn("Failed to apply BGGeo base invariants", {
error: e?.message,
stack: e?.stack,
});
}
// Only set the permission state if we already have the permission // Only set the permission state if we already have the permission
const state = await BackgroundGeolocation.getState(); const state = await BackgroundGeolocation.getState();
locationLogger.debug("Background geolocation state", { locationLogger.debug("Background geolocation state", {

View file

@ -1,5 +1,4 @@
// related to services/tasks/src/geocode/config.js // related to services/tasks/src/geocode/config.js
export const TRACK_MOVE = 10;
export const DEFAULT_DEVICE_RADIUS_ALL = 500; export const DEFAULT_DEVICE_RADIUS_ALL = 500;
export const DEFAULT_DEVICE_RADIUS_REACH = 25000; export const DEFAULT_DEVICE_RADIUS_REACH = 25000;
export const MAX_BASEUSER_DEVICE_TRACKING = 25000; export const MAX_BASEUSER_DEVICE_TRACKING = 25000;