Skip to content

Android Headless Mode

Chris Scott edited this page Sep 16, 2021 · 5 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.

Android Headless Setup

Step 1 enableHeadless: true

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

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

Step 2

  • With your app open in Android Studio, browse the app folder to find the MainActivity class. Right-click the containing folder and click New > Java Class.

  • You MUST name the file BackgroundGeolocationHeadlessTask.

  • Add the following Java code to BackgroundGeolocationHeadlessTask, taking care to preserve the top line:

package com.your.package.name:

package com.your.package.name  // <-- DO NOT REPLACE THIS LINE!!!!!!!!!!!!

/// ---------------- Paste everything BELOW THIS LINE: -----------------------

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;
import android.util.Log;

/**
 * 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  {
    private static final String TAG = "TSLocationManager";

    @Subscribe(threadMode=ThreadMode.MAIN)
    public void onHeadlessTask(HeadlessEvent event) {
        // Get a reference to the BackgroundGeolocation Android API.
        BackgroundGeolocation bgGeo = BackgroundGeolocation.getInstance(event.getContext());

        String name = event.getName();
        Log.d(TAG, "\uD83D\uDC80  event: " + event.getName());
        Log.d(TAG, "- 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 {
            Log.d(TAG, "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(event.getContext());

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(event.getContext());

// 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}