fix(android): track location battery saving
This commit is contained in:
parent
782448af3d
commit
a2acbb6d0b
5 changed files with 203 additions and 13 deletions
13
index.js
13
index.js
|
|
@ -3,6 +3,8 @@ import "./warnFilter";
|
|||
|
||||
import "expo-splash-screen";
|
||||
|
||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||
|
||||
import notifee from "@notifee/react-native";
|
||||
import messaging from "@react-native-firebase/messaging";
|
||||
|
||||
|
|
@ -19,6 +21,17 @@ import onMessageReceived from "~/notifications/onMessageReceived";
|
|||
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
||||
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);
|
||||
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||
// the environment is set up appropriately
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
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.
|
||||
//
|
||||
// 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:
|
||||
// - 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.
|
||||
|
|
@ -18,10 +21,21 @@ export const BASE_GEOLOCATION_CONFIG = {
|
|||
|
||||
// Default to the IDLE profile behaviour: we still want distance-based updates
|
||||
// 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,
|
||||
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
|
||||
locationAuthorizationRequest: "Always",
|
||||
|
|
@ -31,7 +45,9 @@ export const BASE_GEOLOCATION_CONFIG = {
|
|||
startOnBoot: true,
|
||||
|
||||
// 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
|
||||
foregroundService: true,
|
||||
|
|
@ -71,15 +87,52 @@ export const BASE_GEOLOCATION_CONFIG = {
|
|||
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 = {
|
||||
idle: {
|
||||
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
|
||||
distanceFilter: 50,
|
||||
heartbeatInterval: 3600,
|
||||
// Max battery-saving strategy for IDLE:
|
||||
// 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: {
|
||||
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
|
||||
distanceFilter: TRACK_MOVE,
|
||||
heartbeatInterval: 900,
|
||||
// Ensure we exit significant-changes mode when switching from IDLE.
|
||||
useSignificantChangesOnly: false,
|
||||
distanceFilter: 50,
|
||||
heartbeatInterval: 60,
|
||||
|
||||
// Keep default responsiveness during an active alert.
|
||||
stopTimeout: 5,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ export function setBackgroundGeolocationEventHandlers({
|
|||
onLocation,
|
||||
onLocationError,
|
||||
onHttp,
|
||||
onHeartbeat,
|
||||
onSchedule,
|
||||
onMotionChange,
|
||||
onActivityChange,
|
||||
onProviderChange,
|
||||
|
|
@ -65,6 +67,8 @@ export function setBackgroundGeolocationEventHandlers({
|
|||
const sig = [
|
||||
onLocation ? "L1" : "L0",
|
||||
onHttp ? "H1" : "H0",
|
||||
onHeartbeat ? "HB1" : "HB0",
|
||||
onSchedule ? "S1" : "S0",
|
||||
onMotionChange ? "M1" : "M0",
|
||||
onActivityChange ? "A1" : "A0",
|
||||
onProviderChange ? "P1" : "P0",
|
||||
|
|
@ -86,6 +90,22 @@ export function setBackgroundGeolocationEventHandlers({
|
|||
if (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) {
|
||||
subscriptions.push(BackgroundGeolocation.onMotionChange(onMotionChange));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import env from "~/env";
|
|||
|
||||
import {
|
||||
BASE_GEOLOCATION_CONFIG,
|
||||
BASE_GEOLOCATION_INVARIANTS,
|
||||
TRACKING_PROFILES,
|
||||
} from "~/location/backgroundGeolocationConfig";
|
||||
import {
|
||||
|
|
@ -129,6 +130,8 @@ export default function trackLocation() {
|
|||
}
|
||||
if (currentProfile === profileName) return;
|
||||
|
||||
const applyStartedAt = Date.now();
|
||||
|
||||
const profile = TRACKING_PROFILES[profileName];
|
||||
if (!profile) {
|
||||
locationLogger.warn("Unknown tracking profile", { profileName });
|
||||
|
|
@ -147,14 +150,71 @@ export default function trackLocation() {
|
|||
|
||||
// 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).
|
||||
// - IDLE: ensure we are not stuck in moving mode from a prior ACTIVE session.
|
||||
// We explicitly exit moving mode to avoid periodic drift-generated locations
|
||||
// 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") {
|
||||
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;
|
||||
|
||||
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) {
|
||||
locationLogger.error("Failed to apply tracking profile", {
|
||||
profileName,
|
||||
|
|
@ -338,6 +398,40 @@ export default function trackLocation() {
|
|||
status: response?.status,
|
||||
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) => {
|
||||
locationLogger.info("Motion change", {
|
||||
|
|
@ -374,6 +468,17 @@ export default function trackLocation() {
|
|||
locationLogger.info("Initializing background geolocation");
|
||||
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
|
||||
const state = await BackgroundGeolocation.getState();
|
||||
locationLogger.debug("Background geolocation state", {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
// 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_REACH = 25000;
|
||||
export const MAX_BASEUSER_DEVICE_TRACKING = 25000;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue