-
Notifications
You must be signed in to change notification settings - Fork 276
Android Headless Mode
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.
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');
});
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
.
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));
}
}
}
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 savedBackgroundGeolocationHeadlessTask.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.
In your application code, configure BackgroundGeolocation.ready
with enableHeadless: true
:
BackgroundGeolocation.ready({
enableHeadless: true,
stopOnTerminate: false,
.
.
.
});
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
- 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));
}
}
}
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.
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}