Where Am I?
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.