Skip to content

Android Headless Mode

Simon Neutert edited this page Apr 20, 2021 · 15 revisions

BackgroundGeolocation Android "Headless" mode is the state where your app has been terminated with the plugin configured for stopOnTerminate: false. In this state, your Cordova Javascript app no longer exists. Any Javascript event-handlers you've registered with the plugin will no longer fire. Only the plugin's native Android service continues to run, tracking and posting locations to your server through your configured #url.

So what if you need to handle some business logic in this state?

If you're willing to get your feet wet with a bit of Android Java programming, BackgroundGeolocation allows you to provide a custom Java class of your own so you can receive events from the plugin while in the headless state. You can interact with the plugin's Java API in much the same manner you would its Javascript API, in addition to the entire Android API.



Cordova / Ionic Setup

Step 1: enableHeadless: true.

Where you execute BackgroundGeolocation#ready, add the following options:

  onDeviceReady() {    
    BackgroundGeolocation.ready({
      enableHeadless: true,    // <-- enable Headless mode
      stopOnTerminate: false,  // <-- required for Headless JS
      .
      .
      .
    }, (state) => {
      console.log('- Configure success');
    });

Step 2: BackgroundGeolocationHeadlessTask.java

Paste the following code into a file named BackgroundGeolocationHeadlessTask.java. Note: Choose one of the following based upon the version of background-geolocation since the Android Headless event changed in version 2.13.2.

⚠️ You can save the file anywhere in your app but the file must be named BackgroundGeolocationHeadlessTask.java:

πŸ“‚ src/android/BackgroundGeolocationHeadlessTask.java

package com.transistorsoft.cordova.bggeo;

import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import org.json.JSONObject;

import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation;
import com.transistorsoft.locationmanager.event.ActivityChangeEvent;
import com.transistorsoft.locationmanager.event.ConnectivityChangeEvent;
import com.transistorsoft.locationmanager.event.GeofenceEvent;
import com.transistorsoft.locationmanager.event.HeadlessEvent;
import com.transistorsoft.locationmanager.event.HeartbeatEvent;
import com.transistorsoft.locationmanager.event.MotionChangeEvent;
import com.transistorsoft.locationmanager.event.LocationProviderChangeEvent;
import com.transistorsoft.locationmanager.http.HttpResponse;
import com.transistorsoft.locationmanager.location.TSLocation;
import com.transistorsoft.locationmanager.logger.TSLog;


/**
 * BackgroundGeolocationHeadlessTask
 * This component allows you to receive events from the BackgroundGeolocation plugin in the native Android environment while your app has been *terminated*,
 * where the plugin is configured for stopOnTerminate: false.  In this context, only the plugin's service is running.  This component will receive all the same
 * events you'd listen to in the Javascript API.
 *
 * You might use this component to:
 * - fetch / post information to your server (eg: request new API key)
 * - execute BackgroundGeolocation API methods (eg: #getCurrentPosition, #setConfig, #addGeofence, #stop, etc -- you can execute ANY method of the Javascript API)
 */

public class BackgroundGeolocationHeadlessTask  {

    @Subscribe(threadMode=ThreadMode.MAIN)
    public void onHeadlessTask(HeadlessEvent event) {
        String name = event.getName();
        TSLog.logger.debug("\uD83D\uDC80  event: " + event.getName());
        TSLog.logger.debug("- event: " + event.getEvent());

        if (name.equals(BackgroundGeolocation.EVENT_TERMINATE)) {
            JSONObject state = event.getTerminateEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_LOCATION)) {
            TSLocation location = event.getLocationEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_MOTIONCHANGE)) {
            MotionChangeEvent motionChangeEvent = event.getMotionChangeEvent();
            TSLocation location = motionChangeEvent.getLocation();
        } else if (name.equals(BackgroundGeolocation.EVENT_HTTP)) {
            HttpResponse response = event.getHttpEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_PROVIDERCHANGE)) {
            LocationProviderChangeEvent providerChange = event.getProviderChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_PROVIDERCHANGE)) {
            LocationProviderChangeEvent providerChange = event.getProviderChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_ACTIVITYCHANGE)) {
            ActivityChangeEvent activityChange = event.getActivityChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_SCHEDULE)) {
            JSONObject state = event.getScheduleEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_BOOT)) {
            JSONObject state = event.getBootEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_GEOFENCE)) {
            GeofenceEvent geofenceEvent = event.getGeofenceEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_HEARTBEAT)) {
            HeartbeatEvent heartbeatEvent = event.getHeartbeatEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_ENABLEDCHANGE)) {
            boolean enabled = event.getEnabledChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_CONNECTIVITYCHANGE)) {
            ConnectivityChangeEvent connectivityChangeEvent = event.getConnectivityChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_POWERSAVECHANGE)) {
            boolean powerSaveEnabled = event.getPowerSaveChangeEvent().isPowerSaveMode();
        } else {
            TSLog.logger.warn(TSLog.warn("Unknown Headless Event: " + name));
        }
    }
}

Step 3: Copying BackgroundGeolocationHeadlessTask.java

In your config.xml, add the following <resource-file /> element within the <platform name="android"> element:

<platform name="android">
     <!-- Copy BackgroundGeolocationHeadlessTask.java -->
     <resource-file 
         src="src/android/BackgroundGeolocationHeadlessTask.java" 
         target="app/src/main/java/com/transistorsoft/cordova/bggeo/BackgroundGeolocationHeadlessTask.java" />
    .
    .
    .
</platform>
  • src: The path to where you saved BackgroundGeolocationHeadlessTask.java
  • target: ⚠️ Must be exactly as above.

Every time you cordova build android, this file will be re-copied into the Cordova Android application's src directory. In order to develop this file, it's best to operate upon it from within Android studio, and run your Cordova app from there, rather than using cordova build android.

When you're satisfied with result of your code, don't forget to copy the contents back to where you created your file or it will be over-written the next time you run cordova build android.

If you've configured a heartbeatInterval, you'll start seeing those events coming through. The default BackgroundGeolocationHeadlessTask is posting local-notification so you'll see & hear those.

Capacitor Setup

Step 1 enableHeadless: true

In your application code, configure BackgroundGeolocation.ready with enableHeadless: true:

BackgroundGeolocation.ready({
  enableHeadless: true,
  stopOnTerminate: false,
  .
  .
  .
});

Step 2

In Android Studio, right-click on your application's top-level namespace in app/java/com.yournamespace, the same folder containing MainActivity:

  • Select New -> Java file.
  • Name the file BackgroundGeolocationHeadlessTask

Step 3

  • Paste the following code.
  • Rename the namespace at the top of the file to match your application's top-level namespace.

package com.foo;  // <-- REQUIRED:  Rename according to YOUR package name

import org.greenrobot.eventbus.Subscribe;
import org.json.JSONObject;

import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation;
import com.transistorsoft.locationmanager.event.ActivityChangeEvent;
import com.transistorsoft.locationmanager.event.GeofenceEvent;
import com.transistorsoft.locationmanager.event.GeofencesChangeEvent;
import com.transistorsoft.locationmanager.event.ConnectivityChangeEvent;
import com.transistorsoft.locationmanager.event.HeadlessEvent;
import com.transistorsoft.locationmanager.event.HeartbeatEvent;
import com.transistorsoft.locationmanager.event.MotionChangeEvent;
import com.transistorsoft.locationmanager.event.LocationProviderChangeEvent;
import com.transistorsoft.locationmanager.http.HttpResponse;
import com.transistorsoft.locationmanager.location.TSLocation;
import com.transistorsoft.locationmanager.logger.TSLog;


/**
 * BackgroundGeolocationHeadlessTask
 * This component allows you to receive events from the BackgroundGeolocation plugin in the native Android environment while your app has been *terminated*,
 * where the plugin is configured for stopOnTerminate: false.  In this context, only the plugin's service is running.  This component will receive all the same
 * events you'd listen to in the Javascript API.
 *
 * You might use this component to:
 * - fetch / post information to your server (eg: request new API key)
 * - execute BackgroundGeolocation API methods (eg: #getCurrentPosition, #setConfig, #addGeofence, #stop, etc -- you can execute ANY method of the Javascript API)
 */

public class BackgroundGeolocationHeadlessTask  {

    @Subscribe
    public void onHeadlessTask(HeadlessEvent event) {
        String name = event.getName();
        TSLog.logger.debug("\uD83D\uDC80  event (CUSTOM IMPLEMENTATION): " + event.getName());
        TSLog.logger.debug("- event: " + event.getEvent());

        if (name.equals(BackgroundGeolocation.EVENT_TERMINATE)) {
            JSONObject state = event.getTerminateEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_LOCATION)) {
            TSLocation location = event.getLocationEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_MOTIONCHANGE)) {
            MotionChangeEvent motionChangeEvent = event.getMotionChangeEvent();
            TSLocation location = motionChangeEvent.getLocation();
        } else if (name.equals(BackgroundGeolocation.EVENT_HTTP)) {
            HttpResponse response = event.getHttpEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_PROVIDERCHANGE)) {
            LocationProviderChangeEvent providerChange = event.getProviderChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_PROVIDERCHANGE)) {
            LocationProviderChangeEvent providerChange = event.getProviderChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_ACTIVITYCHANGE)) {
            ActivityChangeEvent activityChange = event.getActivityChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_SCHEDULE)) {
            JSONObject state = event.getScheduleEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_BOOT)) {
            JSONObject state = event.getBootEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_GEOFENCE)) {
            GeofenceEvent geofenceEvent = event.getGeofenceEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_GEOFENCESCHANGE)) {
            GeofencesChangeEvent geofencesChangeEvent = event.getGeofencesChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_HEARTBEAT)) {
            HeartbeatEvent heartbeatEvent = event.getHeartbeatEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_NOTIFICATIONACTION)) {
            String buttonId = event.getNotificationEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_CONNECTIVITYCHANGE)) {
            ConnectivityChangeEvent connectivityChangeEvent = event.getConnectivityChangeEvent();
        } else if (name.equals(BackgroundGeolocation.EVENT_ENABLEDCHANGE)) {
            boolean enabled = event.getEnabledChangeEvent();
        } else {
            TSLog.logger.warn(TSLog.warn("Unknown Headless Event: " + name));
        }
    }
}

Interacting with BackgroundGeolocation native API.

The first step to interacting with the plugin's native Android API is to get a reference to it:

BackgroundGeolocation bgGeo = BackgroundGeolocation.getInstance(context);

From here, you can execute any of the documented Javascript methods. To execute the #getCurrentPosition method, you can first consult the cordova plugin CDVBackgroundGeolocation.java.

Ignoring all the permissions stuff (which is unnecessary in Headless Mode, since permission will have already been granted:

// Get reference to plugin singleton
BackgroundGeolocation bgGeo = BackgroundGeolocation.getInstance(context);

// Build config object:
JSONObject config = new JSONObject();
try {
  config.put("persist", false);
  config.put("samples", 1);
} catch (JSONException e) {
  // This really won't run
}

// Build a Callback
TSLocationCallback callback = new TSLocationCallback() {
    public void onLocation(TSLocation location) {
        Log.d("TSLocationManager", "- getCurrentPosition SUCCESS: " + location.toJson());
    }
    public void onError(Integer error) {
        Log.d("TSLocationManager", "- getCurrentPosition FAILURE" + error);
    }
};
// Run it
bgGeo.getCurrentPosition(config, callback);

Yes, it's Java. The syntax is more chatty but it's really very similar to the Javascript API.

Testing

In $ adb logcat, you'll see HeadlessTask events prefixed with the "πŸ’€" icon (as in dead / terminated). These "πŸ’€" events are logged just before being sent to your HeadlessTask:

$ adb logcat -s TSLocationManager

TSLocationManager: [c.t.l.LocationService onHeartbeat] ❀️
TSLocationManager: [c.t.l.a.BackgroundGeolocation isMainActivityActive] NO
TSLocationManager: [c.t.c.bggeo.HeadlessJobService onStartJob] πŸ’€  event: heartbeat
TSLocationManager: [c.t.c.b.BackgroundGeolocationHeadlessTask onReceive]
TSLocationManager: ╔═════════════════════════════════════════════
TSLocationManager: β•‘ BackgroundGeolocationHeadlessTask: heartbeat
TSLocationManager: ╠═════════════════════════════════════════════
{"location":{"event":"heartbeat","is_moving":false,"uuid":"6c320f5f-a59f-4e68-854e-2edd4158cbae","timestamp":"2018-01-27T04:11:09.742Z","odometer":13133.3,"coords":{"latitude":45.5193022,"longitude":-73.6169397,"accuracy":13.9,"speed":-1,"heading":-1,"altitude":44.9},"activity":{"type":"still","confidence":100},"battery":{"is_charging":true,"level":1},"extras":{}}}
TSLocationManager: [c.t.c.b.BackgroundGeolocationHeadlessTask onReceive] 
TSLocationManager: [c.t.l.a.BackgroundGeolocation isMainActivityActive] NO
TSLocationManager: [c.t.c.bggeo.HeadlessJobService onStartJob] πŸ’€  event: activitychange
TSLocationManager: [c.t.l.BackgroundGeolocationService onActivityRecognitionResult] still (62%)
TSLocationManager: [c.t.c.b.BackgroundGeolocationHeadlessTask onReceive]
TSLocationManager: ╔═════════════════════════════════════════════
TSLocationManager: β•‘ BackgroundGeolocationHeadlessTask: activitychange
TSLocationManager: ╠═════════════════════════════════════════════
TSLocationManager: [c.t.l.a.BackgroundGeolocation isMainActivityActive] NO
TSLocationManager: [c.t.c.b.BackgroundGeolocationHeadlessTask onReceive] {"activity":"still","confidence":62}