Previously, I have written two articles on monitoring location changes: one for iOS, and one for Android. This article and the related demo brings everything up-to-date and merges support for both operating systems.
STTP (Straight To The Point – I’m coining an acronym to replace TLDR)
If you’re just after the demo, you may find it here. As with most demos in the articles in this blog, it is dependent on the Kastri Free library. Please be aware that for iOS you will need to apply the System.iOS.Sensors patch as detailed below.
As per the previous articles, the aim is to be able to monitor location changes reliably, including when the application is not running. Due to restrictions introduced in later versions of Android, and some shortcomings in the iOS implementation in Delphi, there some “hoops to jump through” to make this all happen.
The demo has 2 project groups: one containing the application and service (CPLAndroidGroup), and one that contains just the application (CPLAppOnlyGroup). CPLAppOnlyGroup is useful for compiling for iOS, because if you compile just the application in the CPLAndroidGroup for iOS, the IDE will still compile the service, regardless of the fact that you are not compiling the application for Android. This issue has been reported, here.
Grijjy Cloud Logger
You may notice in the code that use is made of the excellent Grijjy Cloud Logger framework. This has been useful for debugging so I have left the code in, and if you want to use it, you will need to add defines (CLOUDLOGGING) in the Project Options for the supported platform targets.
Unfortunately at present there is no support for Grijjy Cloud Logging on Android 64-bit. Hopefully this will be rectified soon.
If you choose to use Grijjy Cloud Logging, you will need to either define user override environment variables in the IDE options that correspond to the library locations:
..or modify the project search paths to point to the required libraries.
You will also need to modify the value for cCloudLoggingHost in the CPL.Consts unit in the Common folder, to point to your cloud logging broker instance.
The application has been refactored to integrate the iOS implementation to make things work smoother, and hopefully be easier to follow. Notably, it now includes the DW.ServiceCommander.Android unit, which contains:
- Handles the BecameActive and EnteredBackground application events, sending a local broadcast to the service to inform it of the change so that the service can enter or exit foreground mode.
- Has a StartService command that starts the service if it is not already running.
- Has a SendCommand method to send any other commands that the service needs to receive.
This unit and the units it uses contain the classes necessary for integrating iOS support, and for Android for receiving location messages from the service. The iOS implementation uses TLocationSensor which comes with Delphi, however there are some customizations that are necessary, which are detailed later in this article.
On Android, the application needs a service, and since it needs to be able to track location when the app is not running, it needs to start when the device starts. As per the previous Android demo, instead of using TLocationSensor, I chose to write custom code; mostly for more control, and also so that it can be easily “swapped out” for another implementation such as one that uses FusedLocationProviderClient.
The ServiceModule has been refactored to decouple some of the functionality, namely:
The OnScreenLockChange event is fired when the screen is locked.
When the device enters “doze” mode, an alarm is set so that the service will be invoked when the alarm fires, and the OnDozeChange event fires so that any additional action may be taken.
This class is responsible for processing the location update. It may send the location details to a server, and/or store the location in a local database.
This class existed in the prior version of the demo, however it has been refactored somewhat to improve reliability.
Starting the service at boot time
The service starts when the device starts by using a broadcast receiver (A Java class called DWMultiBroadcastReceiver, in dw-kastri-base.jar) which is configured in AndroidManifest.template.xml
The android.intent.action.BOOT_COMPLETED and android.intent.action.QUICKBOOT_POWERON intent filters ensure that the broadcast receiver receives these intents on cold boot and restart. In the Java code for DWMultiBroadcastReceiver, the metadata is checked for an entry named DWMultiBroadcastReceiver.KEY_START_SERVICE_ON_BOOT, and the metadata value is used as the name of the service to start:
Note that DWMultiBroadcastReceiver also exists in dw-multireceiver.jar, however I am in the process of merging Java code used in Kastri Free in an effort to reduce the number of jar files.
Considerations for later versions of Android
In Android 10, using location services via a service requires requesting a runtime permission of android.permission.ACCESS_BACKGROUND_LOCATION, so this permission is added to AndroidManifest.template.xml (the permission cannot be configured via the IDE), and the permission is requested in code.
From Android 8, a service is required to start in foreground mode if the application goes to the background or is not running. The previous demo went some way to solving this, however it was not 100% reliable; this demo should work as it should. Since the demo uses a regular service, when starting in the foreground the service needs to post a notification which is visible in the status bar, and a banner is visible in the lock screen. Although this is a tad intrusive, presently it’s the only option using a plain Service constructed in Delphi, as using an Intent Service is currently broken in Delphi.
Delphi implementation issues
As per the previous article about location monitoring on iOS, the main issue is that using TLocationSensor “out of the box”, location updates do not occur when the application is in the background or the application is not running. The easiest way to solve this was to patch the System.iOS.Sensors unit, which uses code from the DW.iOS.Sensors unit to help manage the usage (e.g. When In Use and Always), background status (normal and significant changes) and activity (such as Auto and Fitness).
Applying the System.iOS.Sensors unit patch
The easiest way to apply the patch is with Codex, using the Source Patcher function:
If you have not already installed the Patch executable, there is a download link in the Source Patcher dialog:
After downloading/installing, you will need to change the name of the executable to something other than patch.exe (such as p.exe), as Windows thinks it is some kind of installer and will not run without elevated privileges.
Select the patch executable, source file, patch file, and destination folder, e.g:
The System.iOS.Sensors.10.3.3.patch file is supplied with the demo in the Patches folder
The patched code in the resulting file is indicated by comments beginning with // DW, so they’re easy to find.
Testing the demo
I would like to have done more testing on this demo than I have (so far), however it can be rather time consuming, since some of the “live” testing requires physically moving from place to place. I’d like to once again thank Atek Sudianto from Jakarta, for running this demo through its paces.
Normally a device will not go into “doze” mode until a couple of hours of inactivity, when not connected to any charging source. This can make it difficult to debug since debugging normally requires the device to be plugged in to the computer doing the debugging, however I stumbled across a technique for “faking” doze mode. Long story short: it’s via an adb command, and I have now integrated this function into Device Lens:
As pictured, click the “minus” button to fake doze mode, and the “plus” button to restore it to normal.
To test the service starting at boot, either turn the device off, then back on again, or use the device’s reset facility. Shortly after the device starts, you should see the notification icon for the app (default is the Firemonkey icon) in the status bar. This is because the service has started in foreground mode, which requires a notification being displayed.
If you then start the application, you should see the notification icon disappear, because the service has exited foreground mode.
Location updates on iOS when the app is in the background or not running have been exceedingly frustrating (regardless of the issues in Delphi) ever since I’ve started using them. When first testing this new demo, I found that it did not appear to be generating updates, despite the device having moved significantly (more than 500 metres, apparently), then after a while (a day or so), location updates started happening (there were no code changes), without the device moving significantly.
I’d be interested to hear from others about their experiences with background location updates on iOS (using their own code or the demo from this article), so please feel free to leave a comment.
Looking to the future
I have been working on an implementation of location updates that uses FusedLocationProviderClient, however it has been left out for now due to time constraints.
I am also looking into solving the intrusive notification that shows when the service is in the foreground on Android. Indications are that a JobIntentService should be used, however as described earlier this is currently broken in Delphi, though I am looking into solving that issue, as well as investigation other methods.
Finally, hopefully the location services issues for iOS in Delphi will be solved in Delphi 10.4, removing the need to patch the Delphi source.