Handling Location Data in Android

In this blog post, you’ll be learning about how to implement and leverage location data in Android.

Leveraging location data from user devices, you can find nearby shops, cafes, cinemas, hospitals, your time zone, provide location-based suggestions to users, share locations with other users or 3rd parties, reach your destination, and more!

Today, most major applications like Google Maps, Facebook, Uber, Swiggy, etc. are using this feature to provide some of the best services to the users as well.

All of this is achieved through GPS tracking information that gets beamed to various satellites orbiting the Earth. Apart from this, information from Wi-Fi and Bluetooth also affects the location you receive. Hence, the final location is a combination of data obtained through these tools. This is why the location is not always accurate—it’s more of an approximate location.

Types of Location Access

Here are the following 4 different options for location access:

  • Allow only while using the app — This option allows developers to retrieve a user’s location only while the app is actively being used, i.e. the app is in the foreground. This is the most suitable and recommended way to get the location in the applications. This option was recently added to Android 10 and versions above it (screenshot attached below).
  • One time only —Added in Android 11, this is the same as “Allow only while using the app”, but for a limited amount of time.
  • Deny — This means that developers don’t have any access to location information.
  • Allow all the time — Allows location access all the time, but requires extra permission for Android 10 and higher.

This blog will focus on the most common location permission option—Allow only while using the app. To receive location updates, your app must either have a visible activity or a service running in the foreground (with a notification).

So let’s start coding!

1. Adding Gradle dependencies

Your app can access the set of supported location services through classes in the com.google.android.gms.location package. So add it in your app’s build.gradle file:

2. Add the Permissions

Now, to get access to the location information of the app, we need the help of the location provider. In Android, the 3 frequently-used location providers are:

  1. GPS_PROVIDERThis provider determines location using satellites. Depending on conditions such as connectivity, climatic conditions, etc., this provider may take a while to return a fixed location. It requires the permission android.permission.ACCESS_FINE_LOCATION.
  2. NETWORK_PROVIDER This provider determines location based on the availability of cell tower and Wi-Fi access points. Results are retrieved by means of a network lookup. Requires either of the permissions android.permission.ACCESS_COARSE_LOCATION or android.permission.ACCESS_FINE_LOCATION.

So if we want to use any location provider, we must request the user’s permission by declaring the corresponding permissions in our Android manifest file:

  • ACCESS_COARSE_LOCATIONIf you specify ACCESS_COARSE_LOCATION, the API can use Wi-Fi, mobile data, or both to determine the device’s location. The API returns an approximate location.
  • ACCESS_FINE_LOCATIONIf you specify ACCESS_FINE_LOCATION, the API returns as much precise and accurate location info as possible from the available location providers, including the GPS as well as Wi-Fi and mobile cellular data.

The permission you choose determines the accuracy of the location returned by the FusedLocationProviderClient API.

3. Create location services client

In your activity’s onCreate() method, create an instance of the FusedLocationProviderClient, which is the main component that interacts with the fused location provider. This can be used to request location updates, get the last known location, and more, as shown below:

// FusedLocationProviderClient - MainActivity class for receiving location updates.
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient

// LocationRequest - Requirements for the location updates, i.e., how often you
// should receive updates, the priority, etc.
private lateinit var locationRequest: LocationRequest

// LocationCallback - Called when FusedLocationProviderClient has a new Location.
private lateinit var locationCallback: LocationCallback

override fun onCreate(savedInstanceState: Bundle?) {
    // ...

    fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
}

4. Get the last known location

After creating the location services client, you can get the last known location of a user’s device directly by calling the getLastLocation() function, provided by the fused location client object. The following code snippet illustrates the request and simple handling of the response:

fusedLocationProviderClient.lastLocation.apply {
            addOnFailureListener{
               //Handle the failure of the call. You can show an error dialogue or a toast stating the failure in receiving location.
            }
            addOnSuccessListener {
                //Got last known location. (Can be null sometimes)            
                
                //You ca extract the details from the client, like the latitude and longitude of the place and use it accordingly. 
                myLat = it.latitude
                myLong = it.longitude                
            }
        }

The getLastLocation() method returns a Task that you can use to get aLocation object with the latitude and longitude coordinates of a geographic location. The location object may be null when:

  • The location is turned off in the device settings. The result could be null even if the last location was previously retrieved, because disabling the location also clears the cache.
  • The device never recorded its location, which could be the case for a new device or a device that has been restored to factory settings, or in cases where there are network connectivity issues.

5. Initialize the LocationRequest

LocationRequest is a data object through which we can set location service requests parameters like intervals for updates, priorities, accuracy, etc. This is passed to the FusedLocationProviderClient when you request location updates.

For example, if your application wants a high-accuracy location, it should create a location request with setPriority(int) set to PRIORITY_HIGH_ACCURACY and setInterval(long) to 5 seconds; or if your app needs to receive updates at a specified interval, then you can also set it in one of the parameters contained by this object. This would be appropriate for mapping applications that are showing your location in real-time.

   // Sets the desired interval for active location updates. This interval is inexact. You
   // may not receive updates at all if no location sources are available, or you may
   // receive them less frequently than requested. You may also receive updates more
   // frequently than requested if other applications are requesting location at a more
   // frequent interval.
   
locationRequest = LocationRequest().apply {
   // Sets the desired interval for active location updates. 
   interval = TimeUnit.SECONDS.toMillis(60)

   // Sets the fastest rate for active location updates. 
   fastestInterval = TimeUnit.SECONDS.toMillis(30)

   // Sets the maximum time when batched location updates are delivered. Updates may be
   // delivered sooner than this interval.
   maxWaitTime = TimeUnit.MINUTES.toMillis(2)

   priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}

6. Initialize the LocationCallback

LocationCallback is used for receiving notifications when the device location has changed or can no longer be determined. This is one of the parameters passed to the fusedLocationProviderClient.requestLocationUpdates() function.

Also, the onLocationResult() method of this object makes it a lot easier to deal with receiving multiple locations simultaneously (when you have to tackle and batch a lot of requests) as compared to getting location updates through the LocationListener in Android.

Here, we first get the latest location (also shown above) using a LocationResult object. After that, we notify our Activity of the new location using a local broadcast (if it is active because we are using ‘Allow only while using the app to fetch user’s location’) or we update the Notification if this service is running in the foreground.

locationCallback = object : LocationCallback() {
   override fun onLocationResult(locationResult: LocationResult?) {
       super.onLocationResult(locationResult)

       if (locationResult?.lastLocation != null) {

           // Get the currentLocation from the locationResult object.
           currentLocation = locationResult.lastLocation

           // Notify our Activity that a new location was added. 
           val intent = Intent(ACTION_FOREGROUND_ONLY_LOCATION_BROADCAST)
           intent.putExtra(EXTRA_LOCATION, currentLocation)
           LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)

           // Updates notification content if this service is running as a foreground
           // service.
           if (serviceRunningInForeground) {
               notificationManager.notify(
                   NOTIFICATION_ID,
                   generateNotification(currentLocation))
           }
       } else {
           Log.d(TAG, "Location information isn't available.")
       }
   }
}

7. Subscribe to location changes

Now call the requestLocationUpdates() function on the FusedLocationProviderClient object as created above and pass the LocationRequest , LocationCallback,and a Looper object (specifies the thread for the callback, i.e on which thread the above function would be running) to it as the parameters.

...
    try {
            fusedLocationProviderClient.requestLocationUpdates(
                locationRequest, locationCallback, Looper.myLooper())
        } catch (se: SecurityException) {
            Log.e(TAG, "Lost location permissions. Couldn't remove updates. $se")
            //Create a function to request necessary permissions from the app.
            
            checkAndStartLocationUpdates()
        }
   ...

private fun checkAndStartLocationUpdates(){
        if(ActivityCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(
                this, //instance of current activity that is trying to subscribe to get location changes
                arrayOf(
                    Manifest.permission.ACCESS_COARSE_LOCATION,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.READ_EXTERNAL_STORAGE
                ),
                LOCATION_REQ  //A Constant showing the request to get access to location permissions
            )
        }else{
            //retry to subscribe to location changes
        }
    }

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if(requestCode == LOCATION_REQ){
            for(i in grantResults.indices){
                if (grantResults[i]!= PackageManager.PERMISSION_GRANTED){
                    Toast.makeText(this,"Please Give ${permissions[i]}",Toast.LENGTH_LONG).show()
                    return
                }
            }
            //retry to subscribe to location changes
        }
    }

8. Unsubscribe to location changes

When the app no longer needs access to location information, it’s important to unsubscribe from location updates to prevent unnecessary computations, to use the memory and the battery efficiently, and for security reasons as well.

Call the removeLocationUpdates() on the FusedLocationProviderClient method that sets up a task to let it know that we no longer want to receive location updates for our LocationCallback.

The addOnCompleteListener() gives the callback for completion and executes the Task.

val removeTask = fusedLocationProviderClient.removeLocationUpdates(locationCallback)
removeTask.addOnCompleteListener { task ->
   if (task.isSuccessful) {
       Log.d(TAG, "Location Callback removed.")
       stopSelf()
   } else {
       Log.d(TAG, "Failed to remove Location Callback.")
   }
}

The app would look something like this 📷 :-

Notes:

  • From Android 10, a new permission has been added namedACCESS_BACKGROUND_LOCATION—for more about this, refer here.
  • If your app targets Android 9 (API level 28) or lower, the ACCESS_BACKGROUND_LOCATION permission will be automatically added for you by the system if you request either ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION.
  • If your app targets Android 10 or later and you have a foreground service, declare a foregroundServiceType of “location” in the manifest, due to the reason explained above.
  • Never request location permission during app startup unless absolutely necessary. And don’t request background location permissions unless you have a valid use case, as described in this location policy article (some of the best practices in Android 😉).

Below are some screenshots of how the request to access locations would look like on Android 10 and above.

Thanks for reading! If you enjoyed this story, please click the 👏 button and share it to help others find it!

Have feedback? Let’s connect on Twitter.

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *

wix banner square