Tips on Building Android Applications with Long-running Location Service using Google Play Location API
Introduction Google Play Location API is a powerful API that allows your Android app to utilize your phone's location functionalities with a plethora of customizations. This article presents a skeleton of a typical architecture of the location Service class, and points out how the performance of the app can be tweaked in order to find the right balance between accuracy, power usage, privacy, and more. In the end, a quick summary of the essentials to make the app work and the tips and their takeaways are presented. Architecture Skeleton Permissions To compile an app which uses Google Play Location API, we need to add com.google.android.gms:play-services-location as the dependency to build.gradle.kts. dependencies { ... implementation 'com.google.android.gms:play-services-location:21.0.1' } For the app to function properly in runtime, we need to add the following permissions to AndroidManifest.xml. ... ... We leverage FOREGROUND_SERVICE, FOREGROUND_SERVICE_LOCATION, and ACCESS_BACKGROUND_LOCATION to enable a foreground service, to enable a foreground service that uses location, and to access location while the app is in the background respectively. ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION allows the user to control the granularity of the location precision, either an approximate or precise location, in order to strike a balance between the user's preference on privacy, power usage, and location-tracking performance. These two permissions are also required for some older Android APIs. See TIP A below for details. TIP A: On Android's Restrictions on starting a foreground service from the background page, although it states that "if the service declares a foreground service type of location and is started by an app that has the ACCESS_BACKGROUND_LOCATION permission, this service can access location information all the time, even when the app runs in the background", it is found that this behavior is inconsistent, and sometimes does not work as intended. However, this permission should always be included. Note that ACCESS_BACKGROUND_LOCATION was added back in API 29, so the consistent behavior may have to do with some degrees of incompatibility between permissions Note that Android requires the following steps for Android 10 (API level 29) or lower: Include the ACCESS_BACKGROUND_LOCATION permission, as well as either the ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission, in your app's manifest. Request the ACCESS_BACKGROUND_LOCATION permission, as well as one of the other location permissions: ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION. You can request these permissions in a single operation." TIP B: Some Android versions and devices may pause this location service periodically when it runs in the background, especially when battery saver or other power-saving mechanism is on. To override this, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS is used to allow the service to persist in the background without hibernating. Note that this should be brought up to the user, such as via a dialog, to notify them that their power usage might increase if they enable this bypass option. Setting up permissions is usually the hardest part to get right, especially when the app are designed to be backward compatible with older Android versions. Consult the Android documentation meticulously and use conditional statements to refine the scope of (runtime) permissions needed for each API versions. Location Service Class Variables In our Service class, we want to define these variables for the Google Play Location API to work: class LocationService: Service() { // Obsolete! private var serviceRunningInForeground = false // Recommended for "background running" service private lateinit var notificationManager: NotificationManager // Google Play services location APIs. // fusedLocationProviderClient requires a LocationRequest and LocationCallback instance. private lateinit var fusedLocationProviderClient: FusedLocationProviderClient private lateinit var locationRequest: LocationRequest private lateinit var locationCallback: LocationCallback } In the past, it was typical for developers to create a Location Service class which could switch between being in the foreground and background. In Android 11 or lower, this could be achieved by adding the flag (such as serviceRunningInForeground) and using it to call functions such as startForeground when applicable to control the service. However, this is no longer allowed as a security measure, and this requirement is now governed by Android's internal system policies: since location permission is a type of while-in-use permissions, an app can never create a location service while being in the background, even if the app falls into some of the exemptions from background start restrictions. Fr

Introduction
Google Play Location API is a powerful API that allows your Android app to utilize your phone's location functionalities with a plethora of customizations.
This article presents a skeleton of a typical architecture of the location Service
class, and points out how the performance of the app can be tweaked in order to find the right balance between accuracy, power usage, privacy, and more. In the end, a quick summary of the essentials to make the app work and the tips and their takeaways are presented.
Architecture Skeleton
Permissions
To compile an app which uses Google Play Location API, we need to add com.google.android.gms:play-services-location
as the dependency to build.gradle.kts
.
dependencies {
...
implementation 'com.google.android.gms:play-services-location:21.0.1'
}
For the app to function properly in runtime, we need to add the following permissions to AndroidManifest.xml
.
...
...
We leverage FOREGROUND_SERVICE
, FOREGROUND_SERVICE_LOCATION
, and ACCESS_BACKGROUND_LOCATION
to enable a foreground service, to enable a foreground service that uses location, and to access location while the app is in the background respectively.
ACCESS_COARSE_LOCATION
and ACCESS_FINE_LOCATION
allows the user to control the granularity of the location precision, either an approximate or precise location, in order to strike a balance between the user's preference on privacy, power usage, and location-tracking performance. These two permissions are also required for some older Android APIs. See TIP A below for details.
TIP A: On Android's Restrictions on starting a foreground service from the background page, although it states that
"if the service declares a foreground service type of location and is started by an app that has the
ACCESS_BACKGROUND_LOCATION
permission, this service can access location information all the time, even when the app runs in the background",
it is found that this behavior is inconsistent, and sometimes does not work as intended. However, this permission should always be included. Note that ACCESS_BACKGROUND_LOCATION
was added back in API 29, so the consistent behavior may have to do with some degrees of incompatibility between permissions
Note that Android requires the following steps for Android 10 (API level 29) or lower:
Include the
ACCESS_BACKGROUND_LOCATION
permission, as well as either the ACCESS_COARSE_LOCATION
orACCESS_FINE_LOCATION
permission, in your app's manifest.
Request theACCESS_BACKGROUND_LOCATION
permission, as well as one of the other location permissions:ACCESS_COARSE_LOCATION
or ACCESS_FINE_LOCATION. You can request these permissions in a single operation."
TIP B: Some Android versions and devices may pause this location service periodically when it runs in the background, especially when battery saver or other power-saving mechanism is on. To override this, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
is used to allow the service to persist in the background without hibernating. Note that this should be brought up to the user, such as via a dialog, to notify them that their power usage might increase if they enable this bypass option.
Setting up permissions is usually the hardest part to get right, especially when the app are designed to be backward compatible with older Android versions. Consult the Android documentation meticulously and use conditional statements to refine the scope of (runtime) permissions needed for each API versions.
Location Service
Class Variables
In our Service class, we want to define these variables for the Google Play Location API to work:
class LocationService: Service() {
// Obsolete!
private var serviceRunningInForeground = false
// Recommended for "background running" service
private lateinit var notificationManager: NotificationManager
// Google Play services location APIs.
// fusedLocationProviderClient requires a LocationRequest and LocationCallback instance.
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
private lateinit var locationRequest: LocationRequest
private lateinit var locationCallback: LocationCallback
}
In the past, it was typical for developers to create a Location Service
class which could switch between being in the foreground and background. In Android 11 or lower, this could be achieved by adding the flag (such as serviceRunningInForeground
) and using it to call functions such as startForeground
when applicable to control the service.
However, this is no longer allowed as a security measure, and this requirement is now governed by Android's internal system policies: since location permission is a type of while-in-use permissions, an app can never create a location service while being in the background, even if the app falls into some of the exemptions from background start restrictions.
From a developer's point of view, although a foreground service could still transition into a background service, it is impossible to bring it back from the background into a foreground service: for Android 12 (API level 13) or higher, foreground services cannot be started while app is running in the background. In fact, if there are no foreground activities running, the app will crash when it attempts to start the background service or bringing a service from the background to the foreground.
TIP: given this caveat, the only way to have location service running "in the background" is to always start the location service as a foreground service while the app has an activity visible to the user (i.e. a "visible activity"), and keeps the service in the foreground in some form (such as a notification). In fact, Android recommends using a notification via NotificationManager
to inform the user all the time that a service is accessing the location functionality of the device, as a measure for user privacy and to make the user aware of such tasks running.
onCreate
- When the Service is First Created
Initialization of these variables are usually done in the onCreate
method:
override fun onCreate() {
// Initialize the notification manager so we can launch the service as a foreground service.
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// The location provider
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
// The LocationRequest object for requesting location updates.
locationRequest = LocationRequest.Builder(10000)
.setPriority(...)
...
.set...().build()
// The LocationCallback object for handling location updates.
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
if (locationResult.lastLocation != null) {
...
}
val intent = Intent(ACTION_FOREGROUND_ONLY_LOCATION_BROADCAST)
intent
.putExtra(EXTRA_LOCATION,
locationResult.lastLocation)
LocalBroadcastManager
.getInstance(applicationContext)
.sendBroadcast(intent)
}
}
startForeground(NOTIFICATION_ID, generateNotification())
}
In the onCreate
class, this is where the LocationRequest
is initialized. The service also uses the this method to attach to a relevant NotificationManager
and start in the foreground via startForeground
.
TIP: LocationRequest.Builder
has a ton of methods that provides customizations to the location service running, including how often it is triggered, the duration for the whole service, the accuracy and quality of the ping, and the minimum update distance between location updates.
I recommend that the app provides an UI for the user to customize this themselves, instead of setting a pre-defined instance for the whole app. This is particularly important because this single line of code can affect the performance of your app significantly, in terms of your use cases and functionalities, and the performance of the user's device: more accurate and frequent pinging of locations could result in a substantial increase in the device's power usage and possibly incur overheat issues.
For example, simply increasing the ping interval from every 10 seconds to every minute could prolong the device battery by over 50% (from personal experience). Thus, depending on the end user's device and preference, the user should have the control of how they want your app to run.
onStartCommand
- Starting the Service
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
return START_STICKY
}
In the onStartCommand
, which is called by the system whenever a client calls the service explicitly, return START_STICKY
. This means that the system will try to re-create this service (essentially restarting it) whenever this service is killed, e.g. when system resource is scarce.
[Optional] onBind
, onRebind
, onUnbind
(and onTaskRemoved
) - (Un-/Re-)Binding the Service to a Client
override fun onBind(intent: Intent): IBinder {
stopForeground(STOP_FOREGROUND_REMOVE)
serviceRunningInForeground = false
// Your custom subclass for Binder
return localBinder
}
override fun onRebind(intent: Intent) {
stopForeground(STOP_FOREGROUND_REMOVE)
serviceRunningInForeground = false
super.onRebind(intent)
}
override fun onUnbind(intent: Intent): Boolean {
if (!configurationChange) {
startForeground(NOTIFICATION_ID, notification)
serviceRunningInForeground = true
}
return true // Ensures onRebind() is called if MainActivity (client) rebinds.
}
override fun onTaskRemoved(rootIntent: Intent?) {
if (!configurationChange) {
Log.d(TAG, "Start foreground service")
val notification = generateNotification(currentLocation)
startForeground(NOTIFICATION_ID, notification)
serviceRunningInForeground = true
}
}
Due to the Android API permission changes stated above, these code are no longer necessary for Android 12 or higher, since the location service should always be running as a foreground service. The code above is included as a reference to show how it was done before Android 12. In particular, it showcases the usage of the custom flag variable serviceRunningInForeground
as a helpful indicator to know where the service is currently running, and a configurationChange
flag to indicate whether the "change" is induced by a switch in service location, and not simply due to a configuration change such as rotating the screen from portrait to landscape.
Thus, overriding onRebind
, onUnbind
, and onTaskRemoved
is no longer necessary, allowing developers to simply rely on their default implementations.
TIP: Note that the onBind
method is still necessary. A custom Binder
subclass should be declare for the app to know that this service exists. This could be done easily with just three lines of code, as shown below.
/**
* Class used for the client Binder. Since this service runs in the same process as its
* clients, we don't need to deal with inter-process communication (IPC).
*/
inner class LocalBinder : Binder() {
internal val service: ForegroundOnlyLocationService
get() = this@ForegroundOnlyLocationService
}
Allowing clients to subscribe to the Location Service
fun subscribeToLocationUpdates() {
SharedPreferenceUtil.saveLocationTrackingPref(this, true)
startService(
Intent(
applicationContext,
ForegroundOnlyLocationService::class.java
)
)
}
fun unsubscribeToLocationUpdates() {
try {
val removeTask = fusedLocationProviderClient
.removeLocationUpdates(locationCallback)
removeTask.addOnCompleteListener { task ->
...
}
removeTask.addOnFailureListener { task ->
...
}
} catch (unlikely: SecurityException) {
...
}
}
Finally, the location Service
class would need to public methods for clients (e.g. Activity
) to use this itself. The code above shows the standard way of how to start and end the service.
Conclusion
Working with Android has always been enjoyable due to its detailed documentation and a large collection of working examples online. When it comes to Google Play Location API, developers could get started easily, but they must be follow the documentation closely in order to cater for a wide variety of supported devices.
Here are the main takeaways regarding optimization techniques and common pitfalls when working with Android and Google Play Location API:
- Start with a simple architecture for the location service class, and build up from there.
- Include permissions that you need, based on your target Android versions and API levels. For each version, check what combination of permissions are needed for your app to work.
- Allow the user to customize your location service. Running the location functionality on a device is a power-hungry process, so users should have control on how much resources they are willing to give up. Customization includes permissions, location updates frequency, and how long the service is allowed to run.
- Continuing from above, user privacy should also be considered. Giving the user options to choose between high and low location accuracy, and showing the user that a location service is running (in the "background") are recommended practice from Android.
- Last but not least, when the location functionality is not working as intended, first consult Android's own documentation to see if there has been any updates to the usage of Google Play Location API. In particular, pay extra attention to the permissions and lifecycle of the service, which are the two most important factors when it comes to whether the service can be run at all.
BONUS. If all else fails, explore other Android's capabilities such as PowerManager.WakeLock
and AlarmManager
as workarounds. These can also be used as safeguards to make sure the service does not dominate over the use of device resources. However, these should only be used as a last resort as existing location API should already cover your use cases.
Happy Coding!
References
https://developer.android.com/develop/sensors-and-location/location/permissions
https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start
https://developer.android.com/reference/android/location/LocationRequest.Builder
https://developer.android.com/develop/sensors-and-location/location/permissions
https://developer.android.com/reference/android/app/Service
Appendix - a Minimal Example of a Long-running Location Service
class LocationService: Service() {
private lateinit var notificationManager: NotificationManager
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
private lateinit var locationRequest: LocationRequest
private lateinit var locationCallback: LocationCallback
override fun onCreate() {
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
locationRequest = LocationRequest.Builder(10000)
.setPriority(...)
...
.set...().build()
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
if (locationResult.lastLocation != null) {
...
}
val intent = Intent(ACTION_FOREGROUND_ONLY_LOCATION_BROADCAST)
intent
.putExtra(EXTRA_LOCATION,
locationResult.lastLocation)
LocalBroadcastManager
.getInstance(applicationContext)
.sendBroadcast(intent)
}
}
startForeground(NOTIFICATION_ID, generateNotification())
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
return START_STICKY
}
inner class LocalBinder : Binder() {
internal val service: ForegroundOnlyLocationService
get() = this@ForegroundOnlyLocationService
}
}