Know Your Hardware: Mastering GPS for Live Location Tracking and Beyond

Our Android devices are packed with powerful hardware, constantly feeding us with valuable information. Learn about GPS (Global Positioning System), which can pinpoint your live location, track your direction and speed, and even measure your altitude.

Know Your Hardware: Mastering GPS for Live Location Tracking and Beyond

(Getting a deep dive into GPS hardware fascinated me and made me want to explore various hardware in mobile devices, so to motivate me, I'm starting this "Know Your Hardware" series where I'll be posting about all the hardware in Android and documenting it in an open source app (https://github.com/MadFlasheroo7/Know-Your-Hardware) and blog about it here. The repo is open for contributions! 😄

Our Android devices are packed with powerful hardware, constantly feeding us with valuable information. It's time that we tap into their full potential and that's what this series will be focused on. Starting with GPS (Global Positioning System). It can pinpoint your live location, track your direction and speed, and even measure your altitude. So, let's start from the basics.

What is Location Manager?

Location Manager is a platform API that provides access to the system location services (but the recommended way is to use the fused provider provided by Google Play services). This lets us know the GNSS hardware capabilities and provides info about the available location providers also helps us fetch the device's live location. But what is this GNSS?

GNSS and NMEA

Both GNSS and NMEA are used to listen to location updates. NMEA(National Marine Electronics Association) existed way before GNSS(Global Navigation Satellite System) and thus can be used to fetch location on older Android versions, GNSS on the other hand helps us receive signals from satellites like GPS (US), GLONASS (Russia), Galileo (Europe), BeiDou (China) and more, this helps us determine accurate location.

Now that we have enough theory – let's start implementing it and we will look at the GNSS and location capabilities as we code along.

Implementation

To effectively monitor GPS status changes throughout your app's lifecycle, we'll create a broadcast receiver that listens for location mode updates. While Jetpack Compose offers options like DisposableEffect(), we'll prioritize a solution that remains active as long as the app runs. This approach is particularly beneficial when considering potential code sharing across multiple screens in a multi-module project. Although there might be more optimal solutions, here's a functional (though not the most efficient) method to achieve this:

class GpsManager {
  ...
  private val gpsReceiver = object : BroadcastReceiver() {
      override fun onReceive(context: Context?, intent: Intent?) {
        val action = intent?.action
        if (action == LocationManager.PROVIDERS_CHANGED_ACTION) {
          ...
        }
      }
  }

 fun register(context: Context) {
        val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)
        context.registerReceiver(gpsReceiver, filter)
        ....
 } 

 fun unregister(context: Context) {
        context.unregisterReceiver(gpsReceiver)
  }
}

Here we create a GpsManager() class that is responsible for registering and unregistering the broadcast receiver. Here, first, we create a broadcast receiver object function that registers or unregisters the receiver which we call it in onCreate() and onDestroy() respectively, but here a problem arises that our GPS manager does not know the initial GPS state until the mode is changed. So to solve that, we will create a new private class that provides us the initial GPS status.

private class InitialGpsStatus(context: Context) {
    private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    ...
}

Now that we have created our classes we have to register it in the onCreate() method.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        gpsManager.register(this)
        ...
}

Now we can simply pass our instance of gpsManager to the nav host which can be shared to all the screens that need gps state. Now let's see what LocationManager provides to us.

Location Manager

First, we create an instance of LocationManager.

 val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager

Now to help us fetch location android devices have 4 location providers Fused, GPS, Network, and Passive.

  • Fused - Fused location provider combines inputs from all the providers to provide optimal accuracy and battery consumption.
  • GPS - GPS (Global Positioning System) uses the dedicated hardware in the device to communicate with the satellite to provide the location. Works well in open sky and battery consumption is high.
  • Network - Uses WIFI or Cell towers to find out estimated location battery consumption is pretty low but at the cost of accuracy.
  • Passive - Passive provider don't actively request location updates but rather rely on other providers to give the location updates. Consumes little to no battery since it doesn't actively request location updates

LocationManager() and Location() provides a lot of useful methods that we cannot cover in a single blog, but here, treat your eyes with this waterfall of information!

showcase of GNSS capabilities
GitHub - MadFlasheroo7/Know-Your-Hardware: Aims to teach availability and use of multiple hardware and sensors in android device
Aims to teach availability and use of multiple hardware and sensors in android device - MadFlasheroo7/Know-Your-Hardware

Fused Client VS Location Manager

Before learning how to fetch location it's important to decide which client to use to fetch location. So, what is fused client and location manager client anyway?


Location manager is a platform API provided by the underlying OS and Fused on the other hand is provided by Google Play services now both have their pros and cons. Since location manager is provided by the platform we can ensure that irrespective of whether the device supports Google Play services or not it will work as intended, but let's say your device supports both fused and location manager then which one should you use? Fused client is faster in providing you the response whereas the location manager is more accurate most of the time. So depending on your use case, you can decide which one to use but if you want the most accurate location fix its best to have both fused and location manager then pick whichever is more accurate.

Note: either one of them is best when you set minium of 3 seconds interval to fetch live location for more consistant accuracy.

Track Live Location

Now finally to the juicy stuff how do we track live location? most importantly how do we do it efficiently? When working with location data in Android, you have several options at your disposal. Each method, such as getCurrentLocation(), getLastKnownLocation(), and requestLocationUpdates(), serves a unique purpose depending on your app's needs.

  • getLastKnownLocation() - Returns the last cached location by the specified provider, useful in a use case where getting an accurate and latest location is not a priority.
  • getCurrentLocation() - Returns the latest location fix at the time this getting called, usefull when you want a single best location estimate.
  • requestLocationUpdates() - Returns the latest location on either a specified Looper or Executor, useful when you want to track live location of the device.

So, for our use case, we will use requestLocationUpdates(). The requestLocationUpdates() method offers flexibility in how you receive location updates. You can choose a callback mechanism either the legacy LocationListener or the more modern LocationCallback. Additionally, you can specify how and where these callbacks are executed using either a Looper or an Executor.

  • LocationListener - location listener is a legacy way to fetch location, it returns a location fix every time its requested.
  • LocationCallback - location callback is more modern way to fetch location, it rather keeps providing location updates untill stoped.
  • Looper - a looper runs the task that a thread will run and execute them in a queue one after the other, its tied to a specfied thread.
  • Executor - a executor runs on the specfied thread and it manages the scheduling and manages the thread pool internally. Executors are more flexible.

To keep this example simple we will use Looper.

@Composable
fun FusedLocationUpdatesListener(
    locationClient: FusedLocationProviderClient,
    locationRequest: LocationRequest,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onUpdate: (result: LocationResult) -> Unit,
) {
    val context = LocalContext.current
    val currentOnUpdate by rememberUpdatedState(newValue = onUpdate)

    // Whenever on of these parameters changes, dispose and restart the effect.
    DisposableEffect(locationRequest, lifecycleOwner) {
        val locationCallback: LocationCallback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                currentOnUpdate(result)
            }
        }
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                locationClient.requestLocationUpdates(
                    locationRequest, locationCallback, Looper.getMainLooper(),
                )
            } else if (event == Lifecycle.Event.ON_STOP) {
                locationClient.removeLocationUpdates(locationCallback)
            }
        }

        // Add the observer to the lifecycle
        lifecycleOwner.lifecycle.addObserver(observer)

        // When the effect leaves the Composition, remove the observer
        onDispose {
            locationClient.removeLocationUpdates(locationCallback)
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

This sample creates a disposable effect that is executed every time the parameters change and disposes and restarts the effect. Here we create a lifecycle observer that requests location updates on onStart() and removes location updates on onStop(), and the disposable effect is used to remove both the observer and location client from memory when the effect leaves the composition.

0:00
/1:11

Conclusion

GPS and location in Android are way too complex to cover their workings in this blog so you can check out the repo here, the app not only lists the details of the GNSS hardware but also lets you switch the providers between LocationManager and FusedClient on the fly and look at their capabilities. This app and repo will keep getting updated as I learn more not just about the GPS but also more hardware available in Android devices. If you find any work that could be improved check the repo and contribute and that's it for now, do keep an eye on the repo for more hardware coverage until then


Happy Composing! :)

Bonus Links

Repo link:

GitHub - MadFlasheroo7/Know-Your-Hardware: Aims to teach availability and use of multiple hardware and sensors in android device
Aims to teach availability and use of multiple hardware and sensors in android device - MadFlasheroo7/Know-Your-Hardware

Fix nested layouts in compose:

Constraint Layout - Jetpack Compose
Learn more about how you’d go about using a constraint layout when building applications with Jetpack Compose to create layouts with more control and concise code.

Android lifecycle from the view of compose:

Android Lifecycle - from the view of Compose
Learn more about not only the core stages of the activity lifecycle and how it impacts or changes with Jetpack Compose, but also how an activity reacts to hardware and software state changes!

Publish your library to Maven Central:

Build and Publish multi-module Android library to Maven Central
Learn to build and publish your single or multi-module Android library to Maven Central!