Health Connect exercise routes: ExerciseRouteResult and the per-session consent flow
Last verified October 4, 2026 · 7 min read
Covered here:Health Connect
GPS routes are the one part of a Health Connect exercise session that the ordinary read permission does not unlock. Read a session written by another app and its route may come back as "ask the user first" rather than as data. This page covers ExerciseRoute, the three ExerciseRouteResult cases, and the per-session consent flow, according to Google's exercise-routes guide and the Jetpack reference.
The data: ExerciseRoute and its locations#
ExerciseRoute, added in 1.1.0, "Captures a route associated with an exercise session a user does." The reference adds that it "Contains a sequence of location points, with timestamps, which do not have to be in order." Its one property is route: List<ExerciseRoute.Location>.
Location field | Type | Reference |
|---|---|---|
time | Instant | "The point in time when the location was recorded; Required field." |
latitude | Double | "Required field; Valid range -90; 90" |
longitude | Double | "Required field; Valid range -180; 180" |
horizontalAccuracy | Length? | Optional; "Valid range: non-negative numbers." |
verticalAccuracy | Length? | Optional; "Valid range: non-negative numbers." |
altitude | Length? | Optional. |
A route has no record of its own. It lives on ExerciseSessionRecord: you write it through the constructor's exerciseRoute parameter and read it back through the session's exerciseRouteResult property. The reference on that property: "Location data points of ExerciseRoute should be within the parent session, and should be before the end time of the session." The session record itself is described on the ExerciseSessionRecord page.
The three results#
exerciseRouteResult is an ExerciseRouteResult, "Result of the route associated with an exercise session a user does." It is always one of three subclasses, all added in 1.1.0:
| Subclass | Reference | What to do |
|---|---|---|
ExerciseRouteResult.Data | "Class containing data for an ExerciseRoute." | Read exerciseRoute.route. |
ExerciseRouteResult.ConsentRequired | "Class indicating that a permission hasn't been granted and a value couldn't be returned." | Ask the user, with the consent contract below. |
ExerciseRouteResult.NoData | "Class indicating that there's no data to request permissions for." | Nothing; the session has no route. |
ConsentRequired means a route exists. Do not show "no route recorded" for it.
The two permissions, and why one cannot be requested#
| Permission | Jetpack constant | Purpose |
|---|---|---|
android.permission.health.READ_EXERCISE_ROUTES | HealthPermission.PERMISSION_READ_EXERCISE_ROUTES | Read routes written by other apps without asking each time. |
android.permission.health.WRITE_EXERCISE_ROUTE | HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE | Write a route as part of a session. |
Google's guide flags the spelling: "For this permission type, READ_EXERCISE_ROUTES is plural, while WRITE_EXERCISE_ROUTE is singular." Both sit on top of the exercise permissions, because "each route is associated with an exercise session (one session = one workout)."
The read permission is the unusual one. The Jetpack reference: "This permission can't be granted via the standard permission request mechanism, and can only be granted by a user in Settings, or via the dialog launched by androidx.health.connect.client.contracts.ExerciseRouteRequestContract." The framework reference, where it was added in API level 35 and U Extensions 12, is blunter: "Attempts to request the permission by applications will be ignored." Putting it in your createRequestPermissionResultContract set does nothing.
Google lists the access paths like this: "For the session owner, data is accessed using a session read." and "From a third-party app, through a dialog that allows the user to grant a one-time read of a route." So the app that wrote a session gets its route back from a normal session read. For another app's route, the reference says that with the read permission granted "the app can read exercise routes without user interaction", and that when it is revoked "exercise routes can only be obtained by launching the androidx.health.connect.client.contracts.ExerciseRouteRequestContract intent".
The per-session consent flow#
ExerciseRouteRequestContract is "An ActivityResultContract to request a route associated with an androidx.health.connect.client.records.ExerciseSessionRecord. It receives the session id as input and returns an ExerciseRoute as output, if available." The input is the session's metadata.id; createIntent throws IllegalArgumentException "if the input is an empty string"; and parseResult returns null "if the user didn't grant access to the exercise route or if there's no exercise route for the session id passed on createIntent."
This follows Google's reference sample:
val requestRoute = registerForActivityResult(ExerciseRouteRequestContract()) { route: ExerciseRoute? ->
if (route != null) {
showRoute(route) // your code
} else {
// consent denied, or no route for this session
}
}
suspend fun openSessionMap(client: HealthConnectClient, sessionId: String) {
val session = client.readRecord(ExerciseSessionRecord::class, sessionId).record
when (val result = session.exerciseRouteResult) {
is ExerciseRouteResult.Data -> showRoute(result.exerciseRoute)
is ExerciseRouteResult.ConsentRequired -> requestRoute.launch(sessionId)
is ExerciseRouteResult.NoData -> Unit
else -> Unit
}
}
Google's samples keep an else branch: the reference declares ExerciseRouteResult as an abstract class, so Kotlin does not treat the three subclasses as exhaustive. readRecord takes the session's metadata.id and, like the readRecords family generally, needs the exercise read permission.
Background reads do not include other apps' routes#
Google's guide: "Your app can't read exercise route data created by other apps when it runs in the background. When your app runs in the background and tries to read an exercise route created by another app, Health Connect returns an ExerciseRouteResult.ConsentRequired response, even if your app has Always allow access to exercise route data." Its recommendation: "we strongly recommend that you request routes upon deliberate user interaction with your app, when the user is actively engaged with your app's UI."
The Jetpack reference for the read permission says reading apps "must be in the foreground unless android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND is also granted", which sounds more permissive. The guide's sentence is specific to routes created by other apps, so plan for those to need the foreground whatever else is granted. Background reads covers the rest.
Google's UI guidance gives the matching advice for when to ask: "Ask for exercise routes permission after users finish a workout in your app." More on that is on the permission UI guidelines page.
Writing a route#
Pass the route in the session constructor, and only when the write permission is granted. The reference: "An attempt to insert/update a session with a set route without the permission granted will result in a failed call and the session insertion/update will be rejected."
val granted = client.permissionController.getGrantedPermissions()
val route = if (HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE in granted) {
ExerciseRoute(points.map { p -> // points: your recorded fixes
ExerciseRoute.Location(
time = p.time,
latitude = p.lat,
longitude = p.lon,
horizontalAccuracy = Length.meters(p.accuracyMeters)
)
})
} else null
client.insertRecords(listOf(
ExerciseSessionRecord(
startTime = start, startZoneOffset = ZoneOffset.UTC,
endTime = end, endZoneOffset = ZoneOffset.UTC,
exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
exerciseRoute = route,
metadata = Metadata.activelyRecorded(device = Device(type = Device.TYPE_PHONE))
)
))
Updating a session later has a trap. Google's guide: "If your app has a route write permission and tries to update a session by passing in a session object without a route, the existing route is deleted." The reference covers the other case: "If the permission is not granted the previously written route will not be deleted if the session gets updated with no route set."
Pitfalls#
- Requesting
READ_EXERCISE_ROUTESin the normal permission set. It is ignored. UseExerciseRouteRequestContractper session, or send the user to Health Connect settings. - Treating
ConsentRequiredas "no route". It means a route exists and you lack access. OnlyNoDatameans there is none. - Reading routes from a worker. Other apps' routes come back as
ConsentRequiredin the background. Fetch them when the user opens the workout. - Updates that silently drop the route. With the write permission held, an update without
exerciseRoutedeletes the stored route. Carry the route through when you update a session. - A spelling mismatch in Google's own tables. Google's data-types table lists
android.permission.health.READ_EXERCISE_ROUTE; the framework constant's value and the routes guide useREAD_EXERCISE_ROUTES. Use the plural. - Points outside the session. The reference says points should fall within the session and before its end time. Trim stray GPS fixes before writing.
- Looking for an aggregate. Google lists only
EXERCISE_DURATION_TOTALas an aggregation forExerciseSessionRecord, and its own session sample readsDistanceRecordover the session's time range when it wants distance. The HealthKit counterpart, which returns points in batches, is HKWorkoutRouteQuery.
Frequently asked questions
- Why does Health Connect return ExerciseRouteResult.ConsentRequired for a session that has a route?
- Because your app lacks route access for that session. Google's reference says ConsentRequired indicates a permission has not been granted and a value could not be returned. It appears for routes written by other apps when the user has not granted route access, and, per Google's guide, whenever your app tries to read another app's route from the background, even with Always allow access.
- Can I request READ_EXERCISE_ROUTES with createRequestPermissionResultContract?
- No. Google's Jetpack reference says the permission cannot be granted through the standard permission request mechanism and can only be granted by the user in Settings or through the dialog launched by ExerciseRouteRequestContract. The Android framework reference adds that attempts by applications to request it are ignored.
- How does ExerciseRouteRequestContract work?
- It is an ActivityResultContract whose input is the exercise session's ID from its metadata and whose output is an ExerciseRoute or null. Launching it shows a dialog that lets the user grant a one-time read of that session's route. Google's reference says the result is null if the user did not grant access or if the session has no route, and that an empty session ID throws IllegalArgumentException.
- Why did updating a Health Connect exercise session delete its route?
- Because your app holds the route write permission and sent the session without a route. Google's exercise-routes guide says that if an app with route write permission updates a session by passing a session object without a route, the existing route is deleted. Without that permission, the reference says a previously written route is kept when the session is updated with no route set.
Keep reading
Elsewhere on the site
Pages that share this one’s concepts and sources, from other sections.
Next steps
Was this page useful?
Independent comparison, last reviewed October 4, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.
← All health connect api · by AIFitnessAPI