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

elsapiens-background-location

v0.1.1

Published

Save background location in sql lite and make available to cpacitor subscription when app is active

Readme

Elsapiens Background Location Plugin

A comprehensive Capacitor plugin for background location tracking with support for both task-based tracking and work hour monitoring. Features intelligent location management, offline queuing, and battery optimization.

Developed by Elsapiens - Innovative mobile solutions for enterprise location tracking and workforce management.

📚 Documentation

✨ Features

  • 🎯 Task-Based Tracking: Detailed location tracking for specific tasks or routes
  • ⏰ Work Hour Tracking: Periodic location uploads during work hours
  • 🔋 Battery Optimized: Intelligent coordination to minimize battery drain
  • 📱 Offline Support: Queue locations when offline, upload when connected
  • 🔒 Permission Management: Comprehensive Android permission handling
  • 📊 Real-time Events: Live location updates via Capacitor events
  • 💾 Local Storage: SQLite database for reliable data persistence

🚀 Quick Reference

| Use Case | Tracking Mode | Key Features | Best For | |----------|---------------|--------------|----------| | Delivery Routes | Task-Based | High accuracy, route recording, distance calculation | Short-term detailed tracking | | Field Service | Task-Based | Real-time updates, offline storage, complete route history | Service calls, repairs | | Employee Monitoring | Work Hour | Periodic uploads, battery optimized, offline queue | All-day location monitoring | | Fleet Management | Both | Combined detailed + periodic tracking | Comprehensive vehicle tracking | | Time & Attendance | Work Hour | Clock in/out locations, compliance tracking | Workforce management |

Installation

npm install elsapiens-background-location
npx cap sync

Android Configuration

The plugin's own manifest declares everything it needs (permissions, the two foreground services, and the restart receiver) and Android merges it into your app automatically — no manual manifest edits are required.

For reference, the merged permissions are:

<!-- Location permissions -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

<!-- Foreground service (Android 14+ requires the typed permission) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />

<!-- Network access for work hour uploads -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

If your app previously declared com.elsapiens.backgroundlocation.LocationBroadcastReceiver or LocationStateReceiver in its own manifest, remove those entries — the plugin registers them at runtime, and duplicate manifest registration causes duplicate events.

Quick Start

Permission model (read this first)

Android has two independent location permission tiers and they must be requested in order:

  1. Foreground ("While using the app") — enough to start tracking. A location foreground service started while the app is visible keeps receiving fixes after the app is backgrounded.
  2. Background ("Allow all the time") — needed only so tracking can recover when the system restarts it while the app is not visible (device reboot, watchdog restart, app swiped away). Android grants it on a separate settings screen, never in the first dialog.

The plugin never crashes the app over permissions: calls reject with a typed error.code, and asynchronous problems (permission revoked mid-session, GPS switched off) arrive on the error event.

Basic Task Tracking

import { BackgroundLocation } from 'elsapiens-background-location';

async function startTaskTracking() {
  // 1. Foreground permission first.
  let status = await BackgroundLocation.checkPermissions();
  if (status.location !== 'granted') {
    status = await BackgroundLocation.requestPermissions({ permissions: ['location'] });
    if (status.location !== 'granted') {
      // 'denied' means Android will not show the dialog again — send the user to settings.
      await BackgroundLocation.openLocationSettings();
      return;
    }
  }

  // 2. Listen for updates and errors.
  BackgroundLocation.addListener('locationUpdate', (location) => {
    console.log('New location:', location.latitude, location.longitude);
    console.log('Distance traveled:', location.totalDistance, 'km');
  });
  BackgroundLocation.addListener('error', (err) => {
    if (err.code === 'BACKGROUND_PERMISSION_DENIED' && !err.fatal) {
      // Tracking still runs — explain, then let the user pick "Allow all the time".
      askUserForAlwaysPermission();
    } else if (err.code === 'LOCATION_SERVICES_DISABLED') {
      BackgroundLocation.openDeviceLocationSettings();
    } else if (err.fatal) {
      console.error('Tracking stopped:', err.message);
    }
  });

  // 3. Start tracking — works with "While using the app" alone.
  const result = await BackgroundLocation.startTracking({
    reference: 'task_123',
    interval: 3000,        // Update every 3 seconds
    minDistance: 10,       // Minimum 10 meters movement
    highAccuracy: true,    // Use GPS for high accuracy
    maxAccuracy: 30,       // Discard fixes worse than 30 m
    notificationTitle: 'Tracking your route',
  });

  // 4. Upgrade to background permission when the user agrees.
  if (!result.backgroundLocationGranted) {
    askUserForAlwaysPermission();
  }
}

async function askUserForAlwaysPermission() {
  // Show your own explanation UI first (required by Play policy), then:
  const status = await BackgroundLocation.requestPermissions({ permissions: ['backgroundLocation'] });
  if (status.backgroundLocation !== 'granted') {
    // Android opens the settings screen; if the user backed out, offer it again later.
  }
}

async function stopTaskTracking() {
  await BackgroundLocation.stopTracking();
  
  // Retrieve stored locations
  const result = await BackgroundLocation.getStoredLocations({
    reference: 'task_123'
  });
  console.log('Total locations:', result.locations.length);
}

Current location with live accuracy refinement

When you need one good fix (e.g. stamping a form), request a target accuracy: the plugin streams every fix as a currentLocation event so the UI can show the position refining live, and resolves as soon as the target is met.

const handle = await BackgroundLocation.addListener('currentLocation', (fix) => {
  updateMapPin(fix.latitude, fix.longitude, fix.accuracy); // live, isFinal=false
});

try {
  // Resolves with the first fix at <= 15 m accuracy; on timeout resolves the best
  // fix seen with timedOut=true; rejects LOCATION_UNAVAILABLE if nothing arrived.
  const fix = await BackgroundLocation.getCurrentLocation({ targetAccuracy: 15, timeout: 20000 });
  console.log('Final fix:', fix.latitude, fix.longitude, `±${fix.accuracy}m`, fix.timedOut);
} finally {
  handle.remove();
}

Error codes

Branch on error.code (rejections) and the error event — never on message text:

| Code | Meaning | Recommended reaction | |------|---------|----------------------| | PERMISSION_DENIED | Foreground location permission missing | requestPermissions({permissions: ['location']}); if still denied → openLocationSettings() | | BACKGROUND_PERMISSION_DENIED | "Allow all the time" missing (non-fatal while tracking runs) | Explain, then requestPermissions({permissions: ['backgroundLocation']}) | | LOCATION_SERVICES_DISABLED | Device GPS toggle off | openDeviceLocationSettings() | | MISSING_PARAMETER | Required option absent | Fix the call site | | SERVICE_START_FAILED | Android refused the foreground service start | Retry from the foreground | | LOCATION_UNAVAILABLE | No usable fix within timeout | Retry / move to open sky | | NOT_FOUND | No stored data for the reference | Treat as empty | | CANCELLED | Request superseded or cancelled | Usually ignorable | | INTERNAL_ERROR | Unexpected native failure | Log and report |

Work Hour Tracking

async function startWorkDay() {
  // Listen for work hour location updates
  BackgroundLocation.addListener('workHourLocationUpdate', (location) => {
    console.log('Work location captured:', location);
  });

  // Start work hour tracking
  await BackgroundLocation.startWorkHourTracking({
    engineerId: 'engineer_123',
    serverUrl: 'https://api.company.com/work-locations',
    uploadInterval: 300000,  // Upload every 5 minutes
    authToken: 'your-auth-token',
    enableOfflineQueue: true
  });
}

async function endWorkDay() {
  await BackgroundLocation.stopWorkHourTracking();
  
  // Check for any queued locations
  const queued = await BackgroundLocation.getQueuedWorkHourLocations();
  if (queued.locations.length > 0) {
    console.log('Queued locations will be uploaded when online');
  }
}

Core Concepts

Task-Based Tracking

  • Purpose: Detailed route tracking for specific tasks
  • Data Storage: Local SQLite database
  • Use Cases: Delivery routes, service calls, field work
  • Features: High-precision GPS, distance calculation, complete route history

Work Hour Tracking

  • Purpose: Periodic location monitoring during work hours
  • Data Storage: Server uploads with offline queuing
  • Use Cases: Employee monitoring, time tracking, compliance
  • Features: Battery-optimized intervals, automatic uploads, offline support

Session Persistence & Recovery

Tracking sessions are persisted natively and recover automatically:

  • Single Pipeline: Each tracking mode is owned by exactly one foreground service — no duplicate writers
  • Survives Process Death: Session parameters persist in SharedPreferences; the sticky service and a 30-minute watchdog alarm restore tracking (background permission required for background restarts)
  • GPS Toggle Aware: If the user disables location services mid-session, the service waits and resumes automatically when they return
  • Accuracy Guarded: Fixes worse than maxAccuracy are discarded, and travelled distance is computed over accepted fixes only

Advanced Usage

Complete Service Implementation

import { BackgroundLocation } from 'elsapiens-background-location';

export class LocationTrackingService {
  private isTracking = false;
  private currentTaskId: string | null = null;

  async initialize(): Promise<boolean> {
    // Check if location services are enabled
    const serviceStatus = await BackgroundLocation.isLocationServiceEnabled();
    if (!serviceStatus.enabled) {
      console.log('Location services disabled');
      await BackgroundLocation.openLocationSettings();
      return false;
    }

    // Check and request permissions
    const permissions = await BackgroundLocation.checkPermissions();
    if (permissions.location !== 'granted' || permissions.backgroundLocation !== 'granted') {
      const newPermissions = await BackgroundLocation.requestPermissions();
      if (newPermissions.location !== 'granted') {
        throw new Error('Location permissions are required');
      }
    }

    // Set up event listeners
    this.setupEventListeners();
    return true;
  }

  private setupEventListeners() {
    // Task location updates
    BackgroundLocation.addListener('locationUpdate', (location) => {
      this.handleLocationUpdate(location);
    });

    // Work hour location updates
    BackgroundLocation.addListener('workHourLocationUpdate', (location) => {
      this.handleWorkHourUpdate(location);
    });

    // Location service status changes
    BackgroundLocation.addListener('locationStatus', (status) => {
      if (!status.enabled && this.isTracking) {
        this.handleLocationServiceDisabled();
      }
    });
  }

  async startTaskTracking(taskId: string, options?: {
    interval?: number;
    minDistance?: number;
    highAccuracy?: boolean;
  }) {
    if (!await this.initialize()) {
      throw new Error('Failed to initialize location services');
    }

    this.currentTaskId = taskId;
    this.isTracking = true;

    await BackgroundLocation.startTracking({
      reference: taskId,
      interval: options?.interval || 3000,
      minDistance: options?.minDistance || 10,
      highAccuracy: options?.highAccuracy ?? true
    });

    console.log(`Started tracking task: ${taskId}`);
  }

  async stopTaskTracking(): Promise<LocationData[]> {
    if (!this.isTracking || !this.currentTaskId) {
      return [];
    }

    await BackgroundLocation.stopTracking();
    
    // Retrieve all stored locations
    const result = await BackgroundLocation.getStoredLocations({
      reference: this.currentTaskId
    });

    this.isTracking = false;
    this.currentTaskId = null;

    console.log(`Stopped tracking. Captured ${result.locations.length} locations`);
    return result.locations;
  }

  async startWorkHourTracking(engineerId: string, serverUrl: string) {
    if (!await this.initialize()) {
      throw new Error('Failed to initialize location services');
    }

    await BackgroundLocation.startWorkHourTracking({
      engineerId,
      serverUrl,
      uploadInterval: 300000, // 5 minutes
      authToken: await this.getAuthToken(),
      enableOfflineQueue: true
    });

    // Start monitoring location service status
    await BackgroundLocation.startLocationStatusTracking();
    
    console.log(`Started work hour tracking for ${engineerId}`);
  }

  async stopWorkHourTracking() {
    await BackgroundLocation.stopWorkHourTracking();
    await BackgroundLocation.stopLocationStatusTracking();
    
    console.log('Stopped work hour tracking');
  }

  private handleLocationUpdate(location: LocationData) {
    console.log(`Task location update:`, {
      lat: location.latitude,
      lng: location.longitude,
      accuracy: location.accuracy,
      distance: location.totalDistance
    });

    // Save to local storage or send to your backend
    this.saveLocationUpdate(location);
  }

  private handleWorkHourUpdate(location: WorkHourLocationData) {
    console.log(`Work hour location:`, {
      lat: location.latitude,
      lng: location.longitude,
      engineerId: location.engineerId,
      timestamp: new Date(location.timestamp)
    });
  }

  private handleLocationServiceDisabled() {
    console.warn('Location services disabled during tracking');
    // Handle gracefully - perhaps notify user
    this.notifyLocationServiceDisabled();
  }

  private async getAuthToken(): Promise<string> {
    // Implement your authentication logic
    return localStorage.getItem('authToken') || '';
  }

  private saveLocationUpdate(location: LocationData) {
    // Implement your local storage logic
    const stored = JSON.parse(localStorage.getItem('taskLocations') || '[]');
    stored.push(location);
    localStorage.setItem('taskLocations', JSON.stringify(stored));
  }

  private notifyLocationServiceDisabled() {
    // Implement user notification
    console.warn('Please enable location services to continue tracking');
  }
}

Error Handling Best Practices

async function robustLocationTracking() {
  try {
    await BackgroundLocation.startTracking({
      reference: 'task_123',
      interval: 3000,
      minDistance: 10,
      highAccuracy: true
    });
  } catch (error) {
    console.error('Failed to start tracking:', error);
    
    // Handle specific error cases
    if (error.message.includes('permission')) {
      // Guide user to grant permissions
      const permissions = await BackgroundLocation.requestPermissions();
      if (permissions.location !== 'granted') {
        // Show user-friendly permission explanation
        showPermissionExplanation();
      }
    } else if (error.message.includes('location service')) {
      // Guide user to enable location services
      await BackgroundLocation.openLocationSettings();
    } else {
      // Handle other errors
      showGenericError(error.message);
    }
  }
}

Performance Optimization

// Optimize for different use cases
const trackingConfigs = {
  // High precision for detailed mapping
  highPrecision: {
    interval: 1000,      // 1 second
    minDistance: 1,      // 1 meter
    highAccuracy: true
  },
  
  // Balanced for general tracking
  balanced: {
    interval: 3000,      // 3 seconds
    minDistance: 10,     // 10 meters
    highAccuracy: true
  },
  
  // Battery saving for long-term tracking
  batterySaver: {
    interval: 15000,     // 15 seconds
    minDistance: 25,     // 25 meters
    highAccuracy: false
  }
};

// Use appropriate config based on requirements
await BackgroundLocation.startTracking({
  reference: 'task_123',
  ...trackingConfigs.balanced
});

Troubleshooting

Common Issues

1. "Permission denied" errors

Problem: Location permissions not granted or insufficient permissions.

Solutions:

  • Check all required permissions are declared in AndroidManifest.xml
  • Ensure background location permission is granted for work hour tracking
  • Test permission flow on different Android versions (API 23+, 29+)
// Check specific permission status
const permissions = await BackgroundLocation.checkPermissions();
console.log('Permissions:', permissions);

if (permissions.backgroundLocation !== 'granted') {
  console.log('Background location permission required for work hour tracking');
}

2. "Location service disabled" errors

Problem: Device location services are turned off.

Solutions:

  • Guide users to device settings
  • Check location service status before starting tracking
  • Monitor status changes during tracking
const status = await BackgroundLocation.isLocationServiceEnabled();
if (!status.enabled) {
  await BackgroundLocation.openLocationSettings();
}

3. No location updates received

Problem: Tracking started but no location events received.

Solutions:

  • Verify device has GPS signal (test outdoors)
  • Check interval and distance settings aren't too restrictive
  • Ensure event listeners are set up before starting tracking
  • Test with different accuracy settings
// Debug location updates
let updateCount = 0;
BackgroundLocation.addListener('locationUpdate', (location) => {
  updateCount++;
  console.log(`Update #${updateCount}:`, location);
});

4. Work hour locations not uploading

Problem: Locations captured but not reaching server.

Solutions:

  • Verify server URL is accessible and accepts POST requests
  • Check authentication token is valid and not expired
  • Review server logs for request errors
  • Test network connectivity
// Check queued locations
const queued = await BackgroundLocation.getQueuedWorkHourLocations();
console.log(`${queued.locations.length} locations queued for upload`);

// Clear queue if needed (for testing)
// await BackgroundLocation.clearQueuedWorkHourLocations();

Performance Issues

High Battery Usage

  • Reduce update frequency: Increase interval between location updates
  • Lower accuracy: Use balanced power mode instead of high accuracy
  • Increase minimum distance: Only update when moved significant distance
  • Verify cleanup: Ensure tracking is properly stopped when not needed

Slow Performance

  • Database cleanup: Regularly clear old location data
  • Memory management: Remove event listeners when not needed
  • Batch operations: Group database operations where possible

Testing Guidelines

Local Testing

// Test permission flow
async function testPermissions() {
  console.log('Initial permissions:', await BackgroundLocation.checkPermissions());
  
  const requested = await BackgroundLocation.requestPermissions();
  console.log('After request:', requested);
}

// Test location accuracy
async function testLocationAccuracy() {
  const current = await BackgroundLocation.getCurrentLocation();
  console.log('Current location accuracy:', current.accuracy, 'meters');
}

// Test offline queue
async function testOfflineQueue() {
  // Start work hour tracking
  await BackgroundLocation.startWorkHourTracking({
    engineerId: 'test_engineer',
    serverUrl: 'http://invalid-url.com', // Intentionally invalid
    uploadInterval: 10000, // 10 seconds for testing
    enableOfflineQueue: true
  });
  
  // Wait and check queue
  setTimeout(async () => {
    const queued = await BackgroundLocation.getQueuedWorkHourLocations();
    console.log('Queued locations:', queued.locations.length);
  }, 30000);
}

Platform Support

| Platform | Support Status | Notes | |----------|---------------|-------| | Android | ✅ Full Support | All features available | | iOS | ⏳ Planned | Not currently implemented | | Web | 🔧 Development Only | Stub implementation for testing |

Android Requirements

  • Minimum SDK: API 31 (Android 12)
  • Target SDK: API 34+ recommended
  • Google Play Services: Location services required
  • Permissions: Declared by the plugin's manifest; granted by the user at runtime

Android Version Compatibility

Location tracking works on every supported Android version. The differences between OS releases are in how the user grants access — the plugin absorbs those differences and guides the user through the right screen, so from the app's point of view the API and behaviour are identical everywhere.

| Android Version | API Level | Tracking while app in use | Tracking after app closed | How the plugin handles this version | |----------------|-----------|---------------------------|---------------------------|-------------------------------------| | 12 | 31-32 | ✅ Works with one dialog tap | ✅ Works after one settings tap | "Allow all the time" is granted on a settings screen the plugin opens for the user. If only "Approximate" accuracy is chosen, tracking still runs and the app is told (accuracy: 'coarse') so it can ask for precise accuracy. | | 13 | 33 | ✅ Works with one dialog tap | ✅ Works after one settings tap | Same as above. The tracking notification additionally needs the app to request notification permission — tracking runs either way; the notification is just hidden without it. | | 14+ | 34+ | ✅ Works with one dialog tap | ✅ Works after one settings tap | Same as above. The typed foreground-service permission Android 14 requires is declared by the plugin's manifest automatically — nothing for the app to do. |

The one-time user steps, in plain terms:

  1. "While using the app" — a single tap in the standard system dialog. This alone is enough for full tracking during and after app use, as long as the tracking session was started while the app was open.
  2. "Allow all the time" — one extra tap on the settings screen the plugin opens (requestPermissions({permissions: ['backgroundLocation']})). This only adds automatic recovery when the system restarts tracking while the app is closed (device reboot, memory pressure, app swiped away).

If the user skips step 2, nothing breaks: tracking runs, and the app receives a non-fatal BACKGROUND_PERMISSION_DENIED event it can use to ask again at a better moment.

Architecture Overview

The plugin is built with a modular architecture for maintainability and extensibility:

BackgroundLocationPlugin (Main API — bridge translation only)
├── LocationPermissionManager (Permission checks, accuracy tier)
├── LocationTrackingManager (Service lifecycle, validation)
├── TrackingStateStore (Session persistence across restarts)
├── LocationFilter / DistanceTracker (Fix validation, distance math)
├── CurrentLocationWatcher (Progressive-accuracy current location)
├── LocationDataManager (Bridge conversion of stored fixes)
└── WorkHourLocationUploader + WorkHourLocationQueue (Batch uploads)

Key Components

  • LocationPermissionManager: Handles all Android location permission requests and validation
  • LocationDataManager: Processes location data, performs validation, and manages SQLite storage
  • LocationTrackingManager: Coordinates different tracking modes and manages their lifecycle
  • LocationCoordinator: Singleton that manages FusedLocationProviderClient to prevent conflicts
  • WorkHourLocationUploader: Handles background uploads to server with offline queue support

Configuration

Default Settings

const defaultConfig = {
  // Task tracking defaults
  interval: 3000,           // 3 seconds
  minDistance: 10,          // 10 meters
  highAccuracy: true,       // Use GPS
  
  // Work hour tracking defaults
  uploadInterval: 300000,   // 5 minutes
  enableOfflineQueue: true, // Enable offline support
  maxQueueSize: 1000,      // Maximum queued locations
  
  // Database settings
  maxStoredLocations: 10000, // Auto-cleanup threshold
  locationValidityTime: 30000, // 30 seconds
};

Environment Configuration

For different deployment environments:

const configs = {
  development: {
    uploadInterval: 10000,    // 10 seconds for testing
    serverUrl: 'http://localhost:3000/api/locations',
    enableDebugLogging: true
  },
  
  staging: {
    uploadInterval: 60000,    // 1 minute for staging
    serverUrl: 'https://staging-api.company.com/locations',
    enableDebugLogging: true
  },
  
  production: {
    uploadInterval: 300000,   // 5 minutes for production
    serverUrl: 'https://api.company.com/locations',
    enableDebugLogging: false
  }
};

Security Considerations

Data Protection

  • Local Storage: All location data stored in app-private SQLite database
  • Network Security: Use HTTPS for all server communications
  • Authentication: Support for token-based authentication
  • Data Minimization: Only collect necessary location data
  • Retention Policies: Implement data cleanup for old locations

Privacy Best Practices

  1. User Consent: Always obtain explicit user consent before tracking
  2. Transparency: Clearly explain what data is collected and why
  3. User Control: Provide easy start/stop controls
  4. Data Access: Allow users to view and delete their data
  5. Compliance: Follow GDPR, CCPA, and other applicable regulations

Permissions Security

// Always check permissions before sensitive operations
async function secureLocationAccess() {
  const permissions = await BackgroundLocation.checkPermissions();
  
  if (permissions.location !== 'granted') {
    // Explain why permissions are needed
    showPermissionExplanation();
    const result = await BackgroundLocation.requestPermissions();
    
    if (result.location !== 'granted') {
      throw new Error('Location access denied by user');
    }
  }
}

Contributing

We welcome contributions! Please see our Contributing Guide and Developer Guide for detailed information about:

  • Setting up development environment
  • Code style and conventions
  • Testing requirements
  • Pull request guidelines
  • Architecture documentation

Quick Development Setup

# Clone the repository
git clone https://github.com/elsapiens/backgroundlocation.git
cd backgroundlocation

# Install dependencies
npm install

# Build the plugin
npm run build

# Run tests
npm test

License

This project is licensed under the MIT License.

Support



## API

<docgen-index>

* [`checkPermissions()`](#checkpermissions)
* [`requestPermissions(...)`](#requestpermissions)
* [`isLocationServiceEnabled()`](#islocationserviceenabled)
* [`openLocationSettings()`](#openlocationsettings)
* [`openDeviceLocationSettings()`](#opendevicelocationsettings)
* [`startTracking(...)`](#starttracking)
* [`stopTracking()`](#stoptracking)
* [`getTrackingStatus()`](#gettrackingstatus)
* [`getCurrentLocation(...)`](#getcurrentlocation)
* [`cancelCurrentLocationRequest()`](#cancelcurrentlocationrequest)
* [`getStoredLocations(...)`](#getstoredlocations)
* [`clearStoredLocations()`](#clearstoredlocations)
* [`getLastLocation(...)`](#getlastlocation)
* [`startLocationStatusTracking()`](#startlocationstatustracking)
* [`stopLocationStatusTracking()`](#stoplocationstatustracking)
* [`startWorkHourTracking(...)`](#startworkhourtracking)
* [`stopWorkHourTracking()`](#stopworkhourtracking)
* [`isWorkHourTrackingActive()`](#isworkhourtrackingactive)
* [`getQueuedWorkHourLocations()`](#getqueuedworkhourlocations)
* [`clearQueuedWorkHourLocations()`](#clearqueuedworkhourlocations)
* [`addListener('locationUpdate', ...)`](#addlistenerlocationupdate-)
* [`addListener('locationStatus', ...)`](#addlistenerlocationstatus-)
* [`addListener('currentLocation', ...)`](#addlistenercurrentlocation-)
* [`addListener('error', ...)`](#addlistenererror-)
* [`addListener('workHourLocationUpdate', ...)`](#addlistenerworkhourlocationupdate-)
* [`addListener('workHourLocationUploaded', ...)`](#addlistenerworkhourlocationuploaded-)
* [`removeAllListeners()`](#removealllisteners)
* [Interfaces](#interfaces)
* [Type Aliases](#type-aliases)

</docgen-index>

<docgen-api>
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->

Background location tracking for Capacitor.

## Permission flow (Android)

```typescript
const status = await BackgroundLocation.checkPermissions();
if (status.location !== 'granted') {
  const after = await BackgroundLocation.requestPermissions({ permissions: ['location'] });
  if (after.location !== 'granted') {
    // Denied — explain, then send the user to settings:
    await BackgroundLocation.openLocationSettings();
    return;
  }
}
const start = await BackgroundLocation.startTracking({ reference: 'task_1' });
if (!start.backgroundLocationGranted) {
  // Tracking runs, but ask for "Allow all the time" so it survives backgrounding:
  await BackgroundLocation.requestPermissions({ permissions: ['backgroundLocation'] });
}

Every rejection carries an {@link ErrorCode} in error.code; asynchronous failures arrive through the error event. Nothing in this plugin crashes the app on a permission problem.

checkPermissions()

checkPermissions() => Promise<PermissionStatus>

Current state of all location permission tiers, including the accuracy tier (Android 12+ users may grant approximate location only).

Returns: Promise<PermissionStatus>


requestPermissions(...)

requestPermissions(options?: RequestPermissionsOptions | undefined) => Promise<PermissionStatus>

Request location permissions. Foreground and background are requested in sequence as Android requires; see {@link RequestPermissionsOptions} for requesting a single tier (recommended UX).

Once a tier reports denied, Android will not show the dialog again — use openLocationSettings() and let the user grant it manually.

| Param | Type | | ------------- | ------------------------------------------------------------------------------- | | options | RequestPermissionsOptions |

Returns: Promise<PermissionStatus>


isLocationServiceEnabled()

isLocationServiceEnabled() => Promise<{ enabled: boolean; }>

Whether device location services (the GPS toggle) are enabled.

Returns: Promise<{ enabled: boolean; }>


openLocationSettings()

openLocationSettings() => Promise<void>

Open this app's system settings page — where the user grants a previously denied permission or upgrades to "Allow all the time".


openDeviceLocationSettings()

openDeviceLocationSettings() => Promise<void>

Open the device location-services settings — for the GPS-off case.


startTracking(...)

startTracking(options: StartTrackingOptions) => Promise<StartTrackingResult>

Start recording a route under reference.

Requires foreground location permission and enabled location services (rejects with PERMISSION_DENIED / LOCATION_SERVICES_DISABLED otherwise). Missing background permission does NOT reject: tracking starts and the result's backgroundLocationGranted: false (plus a non-fatal error event) tells you to ask the user for "Allow all the time".

The session is persisted natively and survives app and device restarts until stopTracking() is called.

| Param | Type | | ------------- | --------------------------------------------------------------------- | | options | StartTrackingOptions |

Returns: Promise<StartTrackingResult>


stopTracking()

stopTracking() => Promise<void>

Stop the active tracking session. Idempotent — never rejects when inactive.


getTrackingStatus()

getTrackingStatus() => Promise<TrackingStatus>

Current native tracking state — useful to resync UI after an app restart.

Returns: Promise<TrackingStatus>


getCurrentLocation(...)

getCurrentLocation(options?: CurrentLocationOptions | undefined) => Promise<CurrentLocation>

Get the device's current position.

With targetAccuracy set, fixes stream as currentLocation events until one meets the target (see {@link CurrentLocationOptions}) — use this to show a live "improving accuracy" indicator while waiting for a precise fix.

| Param | Type | | ------------- | ------------------------------------------------------------------------- | | options | CurrentLocationOptions |

Returns: Promise<CurrentLocation>


cancelCurrentLocationRequest()

cancelCurrentLocationRequest() => Promise<void>

Cancel an in-flight progressive getCurrentLocation() request; its promise rejects with CANCELLED.


getStoredLocations(...)

getStoredLocations(options: { reference: string; }) => Promise<{ locations: LocationData[]; }>

All recorded fixes for a reference, oldest first.

| Param | Type | | ------------- | ----------------------------------- | | options | { reference: string; } |

Returns: Promise<{ locations: LocationData[]; }>


clearStoredLocations()

clearStoredLocations() => Promise<void>

Delete all stored fixes.


getLastLocation(...)

getLastLocation(options: { reference: string; }) => Promise<LocationData>

Latest stored fix for a reference. Also re-emits it as a locationUpdate event. Rejects with NOT_FOUND when nothing is stored yet.

| Param | Type | | ------------- | ----------------------------------- | | options | { reference: string; } |

Returns: Promise<LocationData>


startLocationStatusTracking()

startLocationStatusTracking() => Promise<void>

Start emitting locationStatus events when the user toggles device location services. Needs no permission — safe to call on app start.


stopLocationStatusTracking()

stopLocationStatusTracking() => Promise<void>

Stop emitting locationStatus events.


startWorkHourTracking(...)

startWorkHourTracking(options: WorkHourTrackingOptions) => Promise<StartTrackingResult>

Start periodic location uploads to serverUrl (persists across app kills as a foreground service). Same permission model as startTracking().

| Param | Type | | ------------- | --------------------------------------------------------------------------- | | options | WorkHourTrackingOptions |

Returns: Promise<StartTrackingResult>


stopWorkHourTracking()

stopWorkHourTracking() => Promise<void>

Stop work-hour tracking. Idempotent.


isWorkHourTrackingActive()

isWorkHourTrackingActive() => Promise<{ active: boolean; }>

Returns: Promise<{ active: boolean; }>


getQueuedWorkHourLocations()

getQueuedWorkHourLocations() => Promise<{ locations: WorkHourLocationData[]; }>

Fixes queued for upload, as visible to this app process.

Returns: Promise<{ locations: WorkHourLocationData[]; }>


clearQueuedWorkHourLocations()

clearQueuedWorkHourLocations() => Promise<void>

addListener('locationUpdate', ...)

addListener(eventName: 'locationUpdate', listenerFunc: (data: LocationData) => void) => Promise<PluginListenerHandle>

A fix was recorded for the active tracking session.

| Param | Type | | ------------------ | ------------------------------------------------------------------------ | | eventName | 'locationUpdate' | | listenerFunc | (data: LocationData) => void |

Returns: Promise<PluginListenerHandle>


addListener('locationStatus', ...)

addListener(eventName: 'locationStatus', listenerFunc: (status: { enabled: boolean; }) => void) => Promise<PluginListenerHandle>

Device location services were toggled on/off.

| Param | Type | | ------------------ | ------------------------------------------------------- | | eventName | 'locationStatus' | | listenerFunc | (status: { enabled: boolean; }) => void |

Returns: Promise<PluginListenerHandle>


addListener('currentLocation', ...)

addListener(eventName: 'currentLocation', listenerFunc: (data: CurrentLocation) => void) => Promise<PluginListenerHandle>

Live fix stream from a progressive getCurrentLocation() request.

| Param | Type | | ------------------ | ------------------------------------------------------------------------------ | | eventName | 'currentLocation' | | listenerFunc | (data: CurrentLocation) => void |

Returns: Promise<PluginListenerHandle>


addListener('error', ...)

addListener(eventName: 'error', listenerFunc: (error: PluginError) => void) => Promise<PluginListenerHandle>

Asynchronous failure or warning — see {@link PluginError}. Listen for BACKGROUND_PERMISSION_DENIED here to know when to ask the user for "Allow all the time".

| Param | Type | | ------------------ | ----------------------------------------------------------------------- | | eventName | 'error' | | listenerFunc | (error: PluginError) => void |

Returns: Promise<PluginListenerHandle>


addListener('workHourLocationUpdate', ...)

addListener(eventName: 'workHourLocationUpdate', listenerFunc: (data: WorkHourLocationData) => void) => Promise<PluginListenerHandle>

A fix entered the work-hour upload queue.

| Param | Type | | ------------------ | ---------------------------------------------------------------------------------------- | | eventName | 'workHourLocationUpdate' | | listenerFunc | (data: WorkHourLocationData) => void |

Returns: Promise<PluginListenerHandle>


addListener('workHourLocationUploaded', ...)

addListener(eventName: 'workHourLocationUploaded', listenerFunc: (data: WorkHourUploadResult) => void) => Promise<PluginListenerHandle>

A work-hour upload batch succeeded or failed.

| Param | Type | | ------------------ | ---------------------------------------------------------------------------------------- | | eventName | 'workHourLocationUploaded' | | listenerFunc | (data: WorkHourUploadResult) => void |

Returns: Promise<PluginListenerHandle>


removeAllListeners()

removeAllListeners() => Promise<void>

Remove all listeners registered by this plugin.


Interfaces

PermissionStatus

| Prop | Type | Description | | ------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | location | PermissionState | Foreground ("while in use") location permission. Granted when fine OR coarse is granted. | | backgroundLocation | PermissionState | Background ("allow all the time") location permission. | | accuracy | LocationAccuracyLevel | Accuracy tier the user granted. coarse means approximate location only. | | foregroundService | 'granted' | 'denied' | Install-time FOREGROUND_SERVICE_LOCATION permission (Android 14+). denied means the host app's AndroidManifest.xml is missing the declaration — an integration error, not something the user can fix. |

RequestPermissionsOptions

| Prop | Type | Description | | ----------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | permissions | ('location' | 'backgroundLocation')[] | Which permission tiers to request. Defaults to both, requested in the required order: foreground first (system dialog), then background (Android opens the app's location settings where the user picks "Allow all the time"). Best practice: request ['location'] when tracking starts, and request ['backgroundLocation'] separately after explaining why you need it. |

StartTrackingResult

| Prop | Type | Description | | ------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | backgroundLocationGranted | boolean | false when tracking started with foreground permission only. Tracking works while the service lives, but cannot recover from a background restart — ask the user for "Allow all the time" (see BACKGROUND_PERMISSION_DENIED). | | accuracy | LocationAccuracyLevel | Accuracy tier tracking runs at. Warn the user when it is coarse. |

StartTrackingOptions

| Prop | Type | Description | | ----------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | reference | string | Identifier the recorded route is stored under (e.g. a task id). Required. | | interval | number | Requested update interval in milliseconds. Default 3000. | | minDistance | number | Minimum movement in meters between recorded fixes. Default 10. | | highAccuracy | boolean | true (default) = GPS-grade accuracy; false = balanced power. | | maxAccuracy | number | Worst acceptable horizontal accuracy in meters; less accurate fixes are discarded instead of polluting the recorded route. Default 30. | | notificationTitle | string | Title of the persistent tracking notification. Defaults to an English string. | | notificationText | string | Body text of the persistent tracking notification. |

TrackingStatus

| Prop | Type | Description | | ------------------------ | --------------------------- | -------------------------------------------------------------------- | | isTracking | boolean | true when a task tracking session is active (survives app restarts). | | isWorkHourTracking | boolean | true when work-hour tracking is active. | | reference | string | null | Reference of the active task session, or null. |

CurrentLocation

| Prop | Type | Description | | -------------------- | -------------------- | ------------------------------------------------------------------------- | | latitude | number | | | longitude | number | | | accuracy | number | Horizontal accuracy of this fix in meters. | | altitude | number | | | speed | number | | | heading | number | | | timestamp | number | | | isFinal | boolean | true when this fix ended the request (target met or timeout). | | timedOut | boolean | true when the request timed out before reaching the target accuracy. | | targetAccuracy | number | Echo of the requested target accuracy (only on currentLocation events). |

CurrentLocationOptions

| Prop | Type | Description | | -------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | targetAccuracy | number | Desired horizontal accuracy in meters. When set, the plugin keeps requesting fixes and emits each one as a currentLocation event (live, so the UI can show the position refining) until a fix meets this accuracy — that fix resolves the promise with isFinal: true. When omitted, the first fresh fix resolves. | | timeout | number | Give-up time in milliseconds (default 30000). On timeout the most accurate fix seen so far resolves with timedOut: true; if nothing usable arrived the call rejects with LOCATION_UNAVAILABLE. |

LocationData

| Prop | Type | Description | | ---------------------- | ------------------- | -------------------------------------------------------- | | reference | string | | | index | number | | | latitude | number | | | longitude | number | | | altitude | number | | | speed | number | | | heading | number | | | accuracy | number | | | altitudeAccuracy | number | | | totalDistance | number | Total distance travelled in this session, in kilometers. | | timestamp | number | |

WorkHourTrackingOptions

| Prop | Type | Description | | ------------------------ | -------------------- | ----------------------------------------------------------------------------------- | | engineerId | string | Identifier sent with every upload. Required. | | uploadInterval | number | Upload (and sampling) interval in milliseconds. Default 300000 (5 minutes). | | serverUrl | string | Endpoint that receives {engineerId, timestamp, locations: [...]} POSTs. Required. | | authToken | string | Sent as a Bearer token in the Authorization header. | | enableOfflineQueue | boolean | Keep fixes queued across failed uploads (default true). |

WorkHourLocationData

| Prop | Type | | ---------------- | ------------------- | | latitude | number | | longitude | number | | accuracy | number | | timestamp | number | | engineerId | string |

PluginListenerHandle

| Prop | Type | | ------------ | ----------------------------------------- | | remove | () => Promise<void> |

PluginError

Payload of the error event.

Asynchronous failures — the tracking service losing its permission, the user switching GPS off mid-session, a failed service restart — cannot reject a promise, so they surface here. Fatal errors mean tracking stopped; non-fatal ones are warnings (e.g. background permission missing while tracking continues in the foreground).

| Prop | Type | Description | | ------------- | --------------------------------------------------- | ------------------------------------------------- | | code | ErrorCode | | | message | string | | | source | ErrorSource | | | fatal | boolean | true when tracking stopped because of this error. |

WorkHourUploadResult

| Prop | Type | Description | | ------------- | -------------------- | --------------------------------------------------- | | success | boolean | | | count | number | Number of fixes in the uploaded (or dropped) batch. | | error | string | |

Type Aliases

PermissionState

'prompt' | 'prompt-with-rationale' | 'granted' | 'denied'

LocationAccuracyLevel

Location accuracy tier the user granted (Android 12+ lets users pick "approximate").

'fine' | 'coarse' | 'none'

ErrorCode

Machine-readable error codes.

Every rejected call carries one of these in the code property of the error, and every error event carries one in its code field — branch on the code, never on the human-readable message.

| Code | Meaning | Recommended app reaction | |------|---------|--------------------------| | PERMISSION_DENIED | Foreground location permission missing | Call requestPermissions(); if state is denied, call openLocationSettings() | | BACKGROUND_PERMISSION_DENIED | "Allow all the time" missing | Explain why, then requestPermissions({permissions: ['backgroundLocation']}) or openLocationSettings() | | LOCATION_SERVICES_DISABLED | Device GPS toggle is off | Prompt user; openDeviceLocationSettings() | | MISSING_PARAMETER | A required option was not provided | Fix the call site | | SERVICE_START_FAILED | Android refused to start the foreground service | Retry from the foreground; check battery restrictions | | LOCATION_UNAVAILABLE | No usable fix within the timeout | Retry, or ask the user to move to open sky | | NOT_FOUND | No stored data for the given reference | Treat as empty | | CANCELLED | Superseded/cancelled request | Usually ignorable | | INTERNAL_ERROR | Unexpected native failure | Log and report |

'PERMISSION_DENIED' | 'BACKGROUND_PERMISSION_DENIED' | 'LOCATION_SERVICES_DISABLED' | 'MISSING_PARAMETER' | 'SERVICE_START_FAILED' | 'LOCATION_UNAVAILABLE' | 'NOT_FOUND' | 'CANCELLED' | 'INTERNAL_ERROR'

ErrorSource

Which subsystem raised an asynchronous error event.

'taskTracking' | 'workHourTracking' | 'currentLocation' | 'permissions'