npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

Readme

cordova-plugin-location-service

npm version license: MIT platform: android

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: false and startOnBoot: 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: locationTemplate renders 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-service

Or in an Ionic project that uses Cordova:

ionic cordova plugin add cordova-plugin-location-service

Uninstall:

cordova plugin rm cordova-plugin-location-service

The 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 all

Geofences 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 queue

Set 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 geofences

Set 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 location first, explain why you need background access, then request backgroundLocation; 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. Use https://, 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 layers

To 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.

License

MIT