cordova-plugin-location-service
v1.0.1
Published
Battery-aware background location tracking for Android: motion-aware GPS, offline SQLite queue, HTTP sync with exponential back-off, unlimited geofences, and survival across app termination and reboot.
Maintainers
Readme
cordova-plugin-location-service
Battery-aware background location tracking for Android Cordova apps.
- Offline-safe SQLite queue: every location is written to local storage before any upload, so nothing is lost when the network drops.
- Built-in HTTP sync with exponential back-off and jitter. It retries immediately when connectivity returns, and hands over to WorkManager when the service isn't running.
- Motion-aware battery use: GPS runs only while the device moves. Once it has been still for
stopTimeout, GPS is switched off and the tracker relies on a stationary geofence, Activity Recognition and an occasional low-power probe to wake it again. - Survives termination and reboot:
stopOnTerminate: falseandstartOnBoot: true, backed by an AlarmManager watchdog, an OS-kill restart alarm and a WorkManager keep-alive. - Unlimited geofences: all geofences are stored in SQLite, and only the nearest ones (up to 90 of Android's 100) are armed. The set is re-chosen as the device moves.
- Your payload, your field names:
locationTemplaterenders each record into whatever shape your backend expects, as JSON or form-encoded.
Requirements
| | |
|---|---|
| Platform | Android only |
| Android | API 24 (7.0) or later |
| cordova-android | 10.0.0 or later |
| Dependencies | play-services-location 21.3.0, androidx.work 2.9.0 (added automatically) |
Install
cordova plugin add cordova-plugin-location-serviceOr in an Ionic project that uses Cordova:
ionic cordova plugin add cordova-plugin-location-serviceUninstall:
cordova plugin rm cordova-plugin-location-serviceThe plugin adds every Android permission it needs to AndroidManifest.xml (fine, coarse and background location, activity recognition, foreground service, boot completed, exact alarms, notifications, wake lock, network). You still have to request the runtime permissions from JS, as shown below.
Quick start
document.addEventListener('deviceready', async () => {
const bg = cordova.plugins.locationService;
bg.on('location', loc => console.log('fix', loc.latitude, loc.longitude, loc.recorded));
bg.on('motionchange', e => console.log(e.isMoving ? 'moving' : 'stationary'));
bg.on('http', e => console.log('upload', e.status));
await bg.ready({
url: 'https://example.com/locations',
headers: { Authorization: 'Bearer ' + token },
extras: { user_id: 42 },
notification: { title: 'Tracking', text: 'Recording your trip' }
});
await bg.requestPermission('location');
await bg.requestPermission('backgroundLocation'); // must be asked separately
await bg.requestPermission('activity'); // optional, makes wake-up faster
await bg.requestPermission('notifications');
await bg.start();
});ready() merges config over what is persisted and doesn't start tracking. Config survives restarts, so the service can resume after a reboot without any JS running. To refresh an expiring token, call setConfig({ headers }). Requests rejected with 401/403 stay queued until the new token arrives.
Geofences
const bg = cordova.plugins.locationService;
bg.on('geofence', e => console.log(e.identifier, e.action)); // ENTER | EXIT | DWELL
await bg.addGeofences([
{ identifier: 'office', latitude: 23.0225, longitude: 72.5714, radius: 150, notifyOnEntry: true, notifyOnExit: true },
{ identifier: 'warehouse', latitude: 23.0300, longitude: 72.5800, radius: 200, notifyOnDwell: true, loiteringDelay: 60000,
extras: { site_id: 7 } }
]);
const fences = await bg.getGeofences();
await bg.removeGeofence('warehouse');
await bg.removeGeofences(); // no argument removes allGeofences are only armed while a tracking session is enabled (start() has been called). Geofence transitions are also recorded and uploaded with event: 'geofence', geofence_identifier and geofence_action.
Offline queue and manual sync
const pending = await bg.getCount(); // records waiting to upload
const records = await bg.getLocations(); // inspect them
const sent = await bg.sync(); // upload now, ignoring back-off
await bg.destroyLocations(); // clear the queueSet autoSync: false to keep records queued until you call sync() yourself.
Stopping and diagnostics
await bg.stop(); // ends the session; queued records are kept
const state = await bg.getState(); // enabled, isMoving, pendingRecords, syncFailures, ...
const info = await bg.getDebugInfo(); // last HTTP result, permissions, armed geofencesSet debug: true and run adb logcat -s LocationService to follow what the tracker is doing.
TypeScript
Type definitions ship with the package (types/index.d.ts) and extend CordovaPlugins:
import type { Config, Location, State } from 'cordova-plugin-location-service';
const bg = cordova.plugins.locationService;
const config: Config = { url: 'https://example.com/locations', distanceFilter: 10 };
const state: State = await bg.ready(config);
bg.on('location', (loc: Location) => console.log(loc.latitude, loc.longitude));Android setup notes
- Background location. On Android 11+ the system never shows "Allow all the time" in the first prompt. Request
locationfirst, explain why you need background access, then requestbackgroundLocation; Android sends the user to Settings for it. Without it, tracking stops when the app is backgrounded and geofences don't fire. - Google Play policy. Apps that use background location must fill in the location permissions declaration in Play Console and show a prominent in-app disclosure before asking.
- Battery optimisation. Some OEMs (Xiaomi, Samsung, OnePlus, Oppo and others) kill background services aggressively. Calling
requestBatteryOptimization()greatly improves survival. - Exact alarms. The watchdog uses
SCHEDULE_EXACT_ALARM. When the user denies it, the plugin falls back to inexact alarms automatically. - Cleartext HTTP. Android blocks
http://URLs by default. Usehttps://, or allow cleartext in your app's manifest when testing against a local server. - Notification. A foreground-service notification is always visible while tracking; Android requires it. Customise it with
notification.
Payload templates
Leave locationTemplate unset to send every variable below. Set it to send your own shape:
locationTemplate: {
id: '{{uuid}}',
lat: '{{latitude}}', // a whole-value placeholder keeps its type (number)
lng: '{{longitude}}',
at: '{{timestamp_iso}}',
note: 'battery {{battery_level}}%', // embedded placeholders are interpolated as text
user: '{{extras.user_id}}',
source: 'mobile' // literals are sent as-is
}Variables: uuid, event, latitude, longitude, accuracy, altitude, speed, heading, provider, timestamp, timestamp_iso, is_moving, is_mock, odometer, activity, battery_level, battery_is_charging, is_battery_optimization_on, network_type, network_strength_level, is_internet_available, device_manufacturer, device_model, device_name, os_version, platform, permissions (object), permission_location, permission_background_location, permission_notifications, permission_activity_recognition, permission_camera, permission_nearby_devices, permission_photos, geofence_identifier, geofence_action, extras, extras.<key>.
Permission values are "GRANTED" / "DENIED"; permission_location is "GRANTED_WHILE_IN_USE" / "DENIED" (background access has its own variable). With form encoding, a nested object or array in the template is sent as a JSON string. battery_level renders as empty when unknown.
Templates are rendered at capture time, so battery and network values describe the moment of the fix, not the moment of upload. Each record keeps its uuid across retries, so the server can deduplicate.
Request body:
| httpContentType | batchSync | Body |
|---|---|---|
| json | false | { [httpRootProperty]: record, ...params }, or { ...record, ...params } when httpRootProperty: '.' |
| json | true | { [httpRootProperty]: [record, …], ...params } (up to maxBatchSize) |
| form | (ignored) | one record per request; key=value&… with params appended, booleans sent as 1/0 |
Response handling
What counts as success is set in config, not built in:
// Server answers 200 with { "status": true } on success, { "status": false, ... } on rejection
httpSuccessStatusCodes: ['200-299'], // numbers or inclusive "lo-hi" ranges
httpSuccessBodyPath: 'status', // dot path into the JSON response ('' = status code only)
httpSuccessBodyValues: [true, 1], // loose match: 1 == "1" == 1.0, true == "true"
httpBodyFailureAction: 'drop', // status OK but body says no → 'drop' | 'retry'
httpRetryStatusCodes: [401, 403, 408, 429, '500-599']Each response is handled in one of three ways:
- success: the records are deleted from the queue and the back-off resets.
- retry: the records stay queued and the next attempt backs off exponentially. Network errors are always retried. Retrying on 401 and 403 by default means an expired token never loses data: call
setConfig({ headers })with the new token and the queue drains. - drop: any other status. The server rejected the record itself, so retrying would only block the queue.
Every response is emitted as an http event, { status, success, action, count, responseText, response }, where response is the parsed JSON body when there is one. Your app can react to whatever the API returns.
Config
| Key | Default | |
|---|---|---|
| desiredAccuracy | 'high' | high / balanced / low / passive while moving |
| locationUpdateInterval | 5000 | ms between GPS fixes while moving |
| fastestLocationUpdateInterval | 5000 | ms; the fastest rate fixes are accepted from other apps' requests |
| distanceFilter | 5 | m of real movement (plus the fix's accuracy) between records |
| minTimeBetweenRecords | 4500 | ms |
| maxAcceptableAccuracy | 20 | m; worse fixes are emitted as samples, never recorded |
| maxFixAge | 10000 | ms; older fixes are cached readings and are ignored |
| maxSpeed | 45 | m/s; a faster implied jump is dropped as a position glitch |
| firstFixTargetAccuracy / firstFixMaxWait | 15 / 20000 | the first record of a session waits for a settled fix |
| smoothing | true | light Kalman filter |
| mockLocationPolicy | 'reject' | allow / reject (drop and emit mock) / stop (also stop tracking) |
| stopTimeout | 5 | minutes without movement before GPS switches off |
| stationaryRadius | 25 | m; leaving this radius turns GPS back on |
| stationaryProbeInterval | 300 | s between low-power checks while stationary |
| disableStopDetection | false | true keeps GPS on all the time |
| disableMotionActivityUpdates | false | skip Activity Recognition |
| keepCpuAwake | false | hold a partial wake lock while moving |
| stopOnTerminate | false | stop when the app is swiped away |
| startOnBoot | true | resume an enabled session after reboot |
| heartbeatInterval / watchdogInterval | 60 / 60 | s |
| url, method, headers, params, extras | | HTTP target and static data |
| httpContentType / httpRootProperty / httpTimeout | 'json' / 'location' / 15000 | |
| autoSync / autoSyncThreshold | true / 0 | upload as records arrive, or once N are queued |
| batchSync / maxBatchSize | false / 50 | |
| retryBackoffBase / retryBackoffMax | 5000 / 600000 | ms |
| maxDaysToPersist / maxRecordsToPersist | 7 / 10000 | queue bounds |
| httpSuccessStatusCodes / httpSuccessBodyPath / httpSuccessBodyValues / httpBodyFailureAction / httpRetryStatusCodes | see above | response handling |
| geofenceProximityRadius / maxMonitoredGeofences | 1000 / 90 | m around the device to arm fences in; at most 99 |
| geofenceInitialTriggerEntry | true | fire ENTER when a fence is added while already inside it |
| notification | | { title, text, channelName, smallIcon } |
| debug | false | verbose logcat (adb logcat -s LocationService) |
API
All methods return Promises. Each also accepts optional (success, error) callbacks.
| Method | |
|---|---|
| ready(config) / setConfig(config) / resetConfig() / getState() | configuration and state |
| start() / stop() | begin or end a session; each start() begins a fresh baseline |
| changePace(isMoving) | force GPS on (true) or off (false) |
| getCurrentPosition({ timeout, maximumAge, desiredAccuracy }) | one-shot fix, no session needed |
| on(event, fn) returns { remove() }; un(event, fn); removeListeners() | events |
| getLocations() / getCount() / destroyLocations() / sync() | offline queue |
| addGeofence(g) / addGeofences([g]) / removeGeofence(id) / removeGeofences([ids]) / getGeofences() | geofences, armed only while a session is enabled |
| getOdometer() / setOdometer(m) | distance between recorded fixes |
| requestPermission(name) / hasPermission(name) | location, backgroundLocation, activity, notifications |
| requestBatteryOptimization() / isIgnoringBatteryOptimizations() | |
| getDebugInfo() | last HTTP result, permissions, armed geofences |
Events: location, motionchange, activitychange, geofence, http, heartbeat, enabledchange, providerchange, connectivitychange, mock. Payload shapes are in types/index.d.ts.
Tested
Verified on a moto g05 (Android 15) in a fresh Cordova app (cordova-android 13, targetSdk 35), driven by Play Services mock locations:
- recording and upload
- moving/stationary switching (GPS powers down while stationary)
- offline queue with back-off and in-order recovery
- every response-handling rule
- JSON, form, batch and template payloads
- service recovery after an app update
Geofence transitions, recovery after the app is killed, and reboot recovery are implemented but haven't been verified on a device yet.
Architecture
www/location-service.js JS API (Promises + event fan-out)
src/android/cordova/ Cordova adapter: exec() → Tracker, runtime permissions
src/android/core/ framework-free tracker, no Cordova imports
Tracker public static API every host adapter calls
EventBus in-process events to whichever adapter is alive
Config/TrackerState persisted config and runtime state
TrackingService foreground service, MOVING/STATIONARY state machine
FixFilter freshness, accuracy, plausibility, Kalman, movement gates
Recorder fix → template-rendered record in SQLite
LocationDatabase SQLite: locations queue and geofences
HttpSync/SyncWorker upload, back-off, WorkManager hand-off
ResponsePolicy config-driven success / retry / drop
GeofenceManager/GeofenceReceiver proximity-paged geofences and stationary fence
MotionDetector Activity Recognition transitions
WatchdogReceiver/KeepAliveWorker/BootReceiver recovery layersTo support another framework (Capacitor, React Native, Flutter), reuse core/ unchanged and write an adapter like cordova/LocationServicePlugin.java. The adapter maps its bridge calls onto Tracker and registers an EventBus.Listener.
