← Back to list

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…

Leejaywaggoner · 2024-03-08 04:09 · 0 claps · 3.1 min read
#android-app-development #androiddev #android-location #location-based-app #devlog
Open on Medium ↗
Wiki topics: 🎙️ · Creator Economy 🏃 · Running & Endurance

Where Am I?

Photo by Tradd Harter on Unsplash

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.


메타데이터
post_id
5415c4a988dc
slug
where-am-i-5415c4a988dc
url
https://medium.com/@leejaywaggoner/where-am-i-5415c4a988dc
canonical_url
https://medium.com/@leejaywaggoner/where-am-i-5415c4a988dc
author_url
https://medium.com/@leejaywaggoner
status
ok
fetched_at
2026-09-09 00:00:03