Where Am I?

Leejaywaggoner
4 min readMar 8, 2024

--

Photo by Tradd Harter on Unsplash

To recap, I now have an app that tracks the user’s activities: walking, running, riding a bike, driving, and standing still. The app tracks the transition to and from these activities and starts a service in the foreground/background when it detects the user has started driving, whether the app is running or not. It also automatically re-starts tracking if the user reboots the device. Now I need to actually get the user’s current location as they drive.

I mentioned at the start of this series that I don’t want to mess around with the Google Maps SDK so instead, I’ll use the FusedLocationProviderClient to get a series of location data in order to calculate the distance the user has driven.

To start off this phase, I added the location permissions to the manifest:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

Then I ask the user for those permissions at runtime with a compose element I wrote based on the Accompanist library. At some point, I need to revisit it to allow the user to decline the permissions — but, really, if they’re going to decline permission for the app to track their location, why even download it? 😄

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CheckPermissions(
permissions: List<String>,
permissionId: Int,
rationaleId: Int,
content: @Composable () -> Unit
) {
if (permissions.isEmpty()) {
content()
return
}

val permissionState = rememberMultiplePermissionsState(
permissions = permissions
)

if (permissionState.allPermissionsGranted) {
content()
} else {
Surface(
modifier = Modifier
.fillMaxSize(),
) {
MileageAppAlert(
title = stringResource(id = R.string.grant_permissions_title),
message = if (permissionState.shouldShowRationale) {
stringResource(id = rationaleId)
} else {
stringResource(id = permissionId)
},
onDismissRequest = { },
onConfirmAlert = {
permissionState.launchMultiplePermissionRequest()
},
onDismissAlert = null,
)
}
}
}

The MileageAppAlert is just a wrapper around the compose AlertDialog element. Also, note that the ACCESS_BACKGROUND_LOCATION permission needs to be asked for after the ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION permissions have been granted in order to get the ‘Allow all the time’ option for background location access. So, it’s a two-stage process:

val startupPermissions = mutableListOf<String>().apply {
add(android.Manifest.permission.ACCESS_FINE_LOCATION)
add(android.Manifest.permission.ACCESS_COARSE_LOCATION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
add(android.Manifest.permission.ACTIVITY_RECOGNITION)
}
}.toList()

CheckPermissions(
permissions = startupPermissions,
permissionId = R.string.tracking_permission,
rationaleId = R.string.tracking_rationale,
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
CheckPermissions(
permissions = listOf(
android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
),
permissionId = R.string.background_permission,
rationaleId = R.string.background_rationale,
) {
MainScreen()
}
} else {
MainScreen()
}
}

So, I’ve asked the user for permission to track their location, now I need to periodically get the user’s location during a drive. I’m injecting the FusedLocationProviderClient through my Hilt module

@Provides
fun provideFusedLocationProviderClient(@ApplicationContext context: Context) =
LocationServices.getFusedLocationProviderClient(context)

into myLocationTracker class:

class LocationTracker @Inject constructor(
private val fusedLocationProviderClient: FusedLocationProviderClient
) {
@SuppressWarnings("MissingPermission")
fun startTrackingMiles(locationCallback: LocationCallback) {
fusedLocationProviderClient.requestLocationUpdates(
LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
TimeUnit.SECONDS.toMillis(3)
).apply {
setMinUpdateDistanceMeters(1f)
setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL)
setWaitForAccurateLocation(true)
}.build(),
locationCallback,
Looper.getMainLooper(),
)
}

@SuppressWarnings("MissingPermission")
fun getCurrentLocation(onSuccess: (Location?) -> Unit) {
fusedLocationProviderClient.getCurrentLocation(
Priority.PRIORITY_HIGH_ACCURACY,
CancellationTokenSource().token
).addOnSuccessListener { location: Location? ->
onSuccess(location)
}
}

fun stopTrackingMiles(locationCallback: LocationCallback) {
fusedLocationProviderClient.removeLocationUpdates(locationCallback)
}
}

You’ll notice I’m suppressing the MissingPermission warning, which is getting triggered because the calls are not closely wrapped by a permissions check. I’m doing the check for the location permissions in my foreground service:

@AndroidEntryPoint
class MileageService : LifecycleService() {
@Inject
lateinit var locationTracker: LocationTracker
private val locationCallback: LocationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
for (location in locationResult.locations) {
//for now, display the location updates in logcat
Log.d(
"${MileageService::class.simpleName}",
"Location: $location"
)
}
}
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
intent?.let { mileageIntent ->
mileageIntent.extras?.let { extras ->
if (extras.containsKey(START_TRACKING_MILES)) {
val startTrackingMiles =
extras.getBoolean(START_TRACKING_MILES)
if (startTrackingMiles) {
//the user started driving, so start the foreground service...
startForeground()
//and start tracking the location
startTrackingMiles()
} else {
//the user stopped driving, so stop tracking the location...
stopTrackingMiles()
//and stop the foreground service
stopForeground()
}
}
}
}
return START_STICKY
}

private fun startTrackingMiles() {
//make sure the user has granted permission to track the location
if (hasLocationPermission()) {
//get the initial location where we detected a drive starting
var location: Location? = null
locationTracker.getCurrentLocation { loc ->
location = loc
Log.d(
"${MileageService::class.simpleName}",
"Initial location: $location"
)
}
//start periodically getting location updates
locationTracker.startTrackingMiles(locationCallback)
} else {
Log.e(
"${MileageService::class.simpleName}",
"Location permission not granted"
)
}
}

private fun stopTrackingMiles() {
locationTracker.stopTrackingMiles(locationCallback)
}

private fun hasLocationPermission(): Boolean =
(ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) &&
(ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED)

...
}

That works! Getting the location data points is pretty straightforward. Really, the only hiccup was realizing I still had an Indonesian SIM card in my phone and swapping it with a US SIM card. Next up, saving and displaying drive data.

--

--

Leejaywaggoner

A seasoned software developer with decades of experience, sharing deep insights and industry wisdom.