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 "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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
||||||
|
const state = await BackgroundGeolocation.getState();
|
||||||
|
if (!state?.isMoving) {
|
||||||
await BackgroundGeolocation.changePace(true);
|
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", {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue