[go: nahoru, domu]

Merge "Add request exercise route contract and expose exercise route." into androidx-main
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 7985cd9..7403050 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -107,6 +107,22 @@
 
 }
 
+package androidx.health.connect.client.contracts {
+
+  public final class ExerciseRouteRequestContract extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,androidx.health.connect.client.records.ExerciseRoute.Data> {
+    ctor public ExerciseRouteRequestContract();
+    method public android.content.Intent createIntent(android.content.Context context, String input);
+    method public androidx.health.connect.client.records.ExerciseRoute.Data? parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  public final class HealthPermissionsRequestContract extends androidx.activity.result.contract.ActivityResultContract<java.util.Set<? extends java.lang.String>,java.util.Set<? extends java.lang.String>> {
+    ctor public HealthPermissionsRequestContract(optional String providerPackageName);
+    method public android.content.Intent createIntent(android.content.Context context, java.util.Set<java.lang.String> input);
+    method public java.util.Set<java.lang.String> parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+}
+
 package androidx.health.connect.client.permission {
 
   public final class HealthPermission {
@@ -425,6 +441,43 @@
     property public final java.time.Instant startTime;
   }
 
+  public abstract class ExerciseRoute {
+  }
+
+  public static final class ExerciseRoute.ConsentRequired extends androidx.health.connect.client.records.ExerciseRoute {
+    ctor public ExerciseRoute.ConsentRequired();
+  }
+
+  public static final class ExerciseRoute.Data extends androidx.health.connect.client.records.ExerciseRoute {
+    ctor public ExerciseRoute.Data(java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> route);
+    method public java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> getRoute();
+    property public final java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> route;
+  }
+
+  public static final class ExerciseRoute.Location {
+    ctor public ExerciseRoute.Location(java.time.Instant time, double latitude, double longitude, optional androidx.health.connect.client.units.Length? horizontalAccuracy, optional androidx.health.connect.client.units.Length? verticalAccuracy, optional androidx.health.connect.client.units.Length? altitude);
+    method public androidx.health.connect.client.units.Length? getAltitude();
+    method public androidx.health.connect.client.units.Length? getHorizontalAccuracy();
+    method public double getLatitude();
+    method public double getLongitude();
+    method public java.time.Instant getTime();
+    method public androidx.health.connect.client.units.Length? getVerticalAccuracy();
+    property public final androidx.health.connect.client.units.Length? altitude;
+    property public final androidx.health.connect.client.units.Length? horizontalAccuracy;
+    property public final double latitude;
+    property public final double longitude;
+    property public final java.time.Instant time;
+    property public final androidx.health.connect.client.units.Length? verticalAccuracy;
+    field public static final androidx.health.connect.client.records.ExerciseRoute.Location.Companion Companion;
+  }
+
+  public static final class ExerciseRoute.Location.Companion {
+  }
+
+  public static final class ExerciseRoute.NoData extends androidx.health.connect.client.records.ExerciseRoute {
+    ctor public ExerciseRoute.NoData();
+  }
+
   public final class ExerciseSegment {
     ctor public ExerciseSegment(java.time.Instant startTime, java.time.Instant endTime, int segmentType, optional int repetitions);
     method public java.time.Instant getEndTime();
@@ -510,9 +563,16 @@
   }
 
   public final class ExerciseSessionRecord implements androidx.health.connect.client.records.Record {
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments);
     ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps, optional androidx.health.connect.client.records.ExerciseRoute.Data? exerciseRouteData);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
+    method public androidx.health.connect.client.records.ExerciseRoute getExerciseRoute();
     method public int getExerciseType();
     method public java.util.List<androidx.health.connect.client.records.ExerciseLap> getLaps();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
@@ -523,6 +583,7 @@
     method public String? getTitle();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
+    property public final androidx.health.connect.client.records.ExerciseRoute exerciseRoute;
     property public final int exerciseType;
     property public final java.util.List<androidx.health.connect.client.records.ExerciseLap> laps;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 4fc8f3b..7300554 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -107,6 +107,22 @@
 
 }
 
+package androidx.health.connect.client.contracts {
+
+  public final class ExerciseRouteRequestContract extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,androidx.health.connect.client.records.ExerciseRoute.Data> {
+    ctor public ExerciseRouteRequestContract();
+    method public android.content.Intent createIntent(android.content.Context context, String input);
+    method public androidx.health.connect.client.records.ExerciseRoute.Data? parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  public final class HealthPermissionsRequestContract extends androidx.activity.result.contract.ActivityResultContract<java.util.Set<? extends java.lang.String>,java.util.Set<? extends java.lang.String>> {
+    ctor public HealthPermissionsRequestContract(optional String providerPackageName);
+    method public android.content.Intent createIntent(android.content.Context context, java.util.Set<java.lang.String> input);
+    method public java.util.Set<java.lang.String> parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+}
+
 package androidx.health.connect.client.permission {
 
   public final class HealthPermission {
@@ -425,6 +441,43 @@
     property public final java.time.Instant startTime;
   }
 
+  public abstract class ExerciseRoute {
+  }
+
+  public static final class ExerciseRoute.ConsentRequired extends androidx.health.connect.client.records.ExerciseRoute {
+    ctor public ExerciseRoute.ConsentRequired();
+  }
+
+  public static final class ExerciseRoute.Data extends androidx.health.connect.client.records.ExerciseRoute {
+    ctor public ExerciseRoute.Data(java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> route);
+    method public java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> getRoute();
+    property public final java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> route;
+  }
+
+  public static final class ExerciseRoute.Location {
+    ctor public ExerciseRoute.Location(java.time.Instant time, double latitude, double longitude, optional androidx.health.connect.client.units.Length? horizontalAccuracy, optional androidx.health.connect.client.units.Length? verticalAccuracy, optional androidx.health.connect.client.units.Length? altitude);
+    method public androidx.health.connect.client.units.Length? getAltitude();
+    method public androidx.health.connect.client.units.Length? getHorizontalAccuracy();
+    method public double getLatitude();
+    method public double getLongitude();
+    method public java.time.Instant getTime();
+    method public androidx.health.connect.client.units.Length? getVerticalAccuracy();
+    property public final androidx.health.connect.client.units.Length? altitude;
+    property public final androidx.health.connect.client.units.Length? horizontalAccuracy;
+    property public final double latitude;
+    property public final double longitude;
+    property public final java.time.Instant time;
+    property public final androidx.health.connect.client.units.Length? verticalAccuracy;
+    field public static final androidx.health.connect.client.records.ExerciseRoute.Location.Companion Companion;
+  }
+
+  public static final class ExerciseRoute.Location.Companion {
+  }
+
+  public static final class ExerciseRoute.NoData extends androidx.health.connect.client.records.ExerciseRoute {
+    ctor public ExerciseRoute.NoData();
+  }
+
   public final class ExerciseSegment {
     ctor public ExerciseSegment(java.time.Instant startTime, java.time.Instant endTime, int segmentType, optional int repetitions);
     method public java.time.Instant getEndTime();
@@ -510,9 +563,16 @@
   }
 
   public final class ExerciseSessionRecord implements androidx.health.connect.client.records.IntervalRecord {
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments);
     ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps);
+    ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps, optional androidx.health.connect.client.records.ExerciseRoute.Data? exerciseRouteData);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
+    method public androidx.health.connect.client.records.ExerciseRoute getExerciseRoute();
     method public int getExerciseType();
     method public java.util.List<androidx.health.connect.client.records.ExerciseLap> getLaps();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
@@ -523,6 +583,7 @@
     method public String? getTitle();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
+    property public final androidx.health.connect.client.records.ExerciseRoute exerciseRoute;
     property public final int exerciseType;
     property public final java.util.List<androidx.health.connect.client.records.ExerciseLap> laps;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
index 3f35587..8f5cca7 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
@@ -18,8 +18,11 @@
 
 package androidx.health.connect.client.samples
 
+import androidx.activity.result.ActivityResultCaller
 import androidx.annotation.Sampled
 import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.contracts.ExerciseRouteRequestContract
+import androidx.health.connect.client.records.ExerciseRoute
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.HeartRateRecord
 import androidx.health.connect.client.records.SleepSessionRecord
@@ -80,6 +83,37 @@
 }
 
 @Sampled
+suspend fun ReadExerciseRoute(
+    activityResultCaller: ActivityResultCaller,
+    healthConnectClient: HealthConnectClient,
+    displayExerciseRoute: (ExerciseRoute.Data) -> Unit,
+    recordId: String
+) {
+    // See https://developer.android.com/training/basics/intents/result#launch for appropriately
+    // handling ActivityResultContract.
+    val requestExerciseRoute =
+        activityResultCaller.registerForActivityResult(ExerciseRouteRequestContract()) {
+            exerciseRoute: ExerciseRoute.Data? ->
+            if (exerciseRoute != null) {
+                displayExerciseRoute(exerciseRoute)
+            } else {
+                // Consent was denied
+            }
+        }
+
+    // Show exercise route, based on user action
+    val exerciseSessionRecord =
+        healthConnectClient.readRecord(ExerciseSessionRecord::class, recordId).record
+
+    when (val exerciseRoute = exerciseSessionRecord.exerciseRoute) {
+        is ExerciseRoute.Data -> displayExerciseRoute(exerciseRoute)
+        is ExerciseRoute.ConsentRequired -> requestExerciseRoute.launch(recordId)
+        is ExerciseRoute.NoData -> Unit // No exercise route to show
+        else -> Unit
+    }
+}
+
+@Sampled
 suspend fun ReadSleepSessions(
     healthConnectClient: HealthConnectClient,
     startTime: Instant,
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContractTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContractTest.kt
new file mode 100644
index 0000000..293d577
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContractTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.contracts
+
+import android.content.Context
+import android.health.connect.HealthConnectManager
+import android.os.Build
+import androidx.health.platform.client.service.HealthDataServiceConstants
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@MediumTest
+class ExerciseRouteRequestContractTest {
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun requestExerciseRoute_createIntent_hasPlatformIntentAction() {
+        val intent = ExerciseRouteRequestContract().createIntent(context, "sessionId")
+        assertThat(intent.action).isEqualTo(HealthConnectManager.ACTION_REQUEST_EXERCISE_ROUTE)
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    fun requestExerciseRoute_createIntent_hasApkIntentAction() {
+        val intent = ExerciseRouteRequestContract().createIntent(context, "sessionId")
+        assertThat(intent.action).isEqualTo(HealthDataServiceConstants.ACTION_REQUEST_ROUTE)
+    }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContractTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContractTest.kt
new file mode 100644
index 0000000..0c7cdb5d
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContractTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.contracts
+
+import android.content.Context
+import android.os.Build
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.platform.client.service.HealthDataServiceConstants
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@MediumTest
+class HealthPermissionsRequestContractTest {
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun requestHealthPermissions_createIntent_hasPlatformIntentAction() {
+        val intent =
+            HealthPermissionsRequestContract()
+                .createIntent(context, setOf(HealthPermission.READ_STEPS))
+        assertThat(intent.action)
+            .isEqualTo(
+                ActivityResultContracts.RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS
+            )
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    fun requestHealthPermissions_createIntent_hasApkIntentAction() {
+        val intent =
+            HealthPermissionsRequestContract()
+                .createIntent(context, setOf(HealthPermission.READ_STEPS))
+        assertThat(intent.action).isEqualTo(HealthDataServiceConstants.ACTION_REQUEST_PERMISSIONS)
+        assertThat(intent.`package`).isEqualTo(HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME)
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    fun requestHealthPermissions_createIntent_hasApkIntentActionAndProvider() {
+        val intent =
+            HealthPermissionsRequestContract("some.provider")
+                .createIntent(context, setOf(HealthPermission.READ_STEPS))
+        assertThat(intent.action).isEqualTo(HealthDataServiceConstants.ACTION_REQUEST_PERMISSIONS)
+        assertThat(intent.`package`).isEqualTo("some.provider")
+    }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/RequestExerciseRouteUpsideDownCakeTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/RequestExerciseRouteUpsideDownCakeTest.kt
index ea4548d..23160ad 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/RequestExerciseRouteUpsideDownCakeTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/RequestExerciseRouteUpsideDownCakeTest.kt
@@ -80,7 +80,7 @@
         val intent = Intent()
         intent.putExtra(HealthConnectManager.EXTRA_EXERCISE_ROUTE, PlatformExerciseRoute(listOf()))
         val result = requestRouteContract.parseResult(0, intent)
-        assertThat(result).isEqualTo(ExerciseRoute(listOf()))
+        assertThat(result).isEqualTo(ExerciseRoute.Data(listOf()))
     }
 
     @Test
@@ -103,7 +103,7 @@
         val result = requestRouteContract.parseResult(0, intent)
         assertThat(result)
             .isEqualTo(
-                ExerciseRoute(
+                ExerciseRoute.Data(
                     listOf(
                         ExerciseRoute.Location(
                             time = Instant.ofEpochMilli(1234L),
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordConvertersTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordConvertersTest.kt
index 892ed70..d0cf773 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordConvertersTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordConvertersTest.kt
@@ -407,8 +407,8 @@
                                 10
                             )
                         ),
-                    route =
-                        ExerciseRoute(
+                    exerciseRoute =
+                        ExerciseRoute.Data(
                             listOf(
                                 ExerciseRoute.Location(
                                     START_TIME,
@@ -1179,7 +1179,7 @@
 
     @Test
     fun exerciseSessionRecord_convertToSdk() {
-        val sdkExerciseSession =
+        val platformExerciseSessionBuilder =
             PlatformExerciseSessionRecordBuilder(
                     PLATFORM_METADATA,
                     START_TIME,
@@ -1230,8 +1230,9 @@
                         )
                     )
                 )
-                .build()
-                .toSdkRecord() as ExerciseSessionRecord
+
+        var sdkExerciseSession =
+            platformExerciseSessionBuilder.build().toSdkRecord() as ExerciseSessionRecord
 
         assertSdkRecord(sdkExerciseSession) {
             assertThat(title).isEqualTo("Training")
@@ -1260,9 +1261,9 @@
                         10
                     )
                 )
-            assertThat(route)
+            assertThat(exerciseRoute as ExerciseRoute.Data)
                 .isEqualTo(
-                    ExerciseRoute(
+                    ExerciseRoute.Data(
                         listOf(
                             ExerciseRoute.Location(
                                 time = START_TIME,
@@ -1276,6 +1277,14 @@
                     )
                 )
         }
+
+        sdkExerciseSession =
+            platformExerciseSessionBuilder.setRoute(null).build().toSdkRecord()
+                as ExerciseSessionRecord
+
+        assertSdkRecord(sdkExerciseSession) {
+            assertThat(exerciseRoute).isEqualTo(ExerciseRoute.NoData())
+        }
     }
 
     @Test
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
index 18ebf35..1a840d2 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
@@ -15,13 +15,12 @@
  */
 package androidx.health.connect.client
 
-import android.os.Build
 import androidx.activity.result.contract.ActivityResultContract
 import androidx.annotation.RestrictTo
 import androidx.health.connect.client.HealthConnectClient.Companion.DEFAULT_PROVIDER_PACKAGE_NAME
+import androidx.health.connect.client.contracts.HealthPermissionsRequestContract
 import androidx.health.connect.client.permission.HealthDataRequestPermissionsInternal
 import androidx.health.connect.client.permission.HealthPermission
-import androidx.health.connect.client.permission.platform.HealthDataRequestPermissionsUpsideDownCake
 
 @JvmDefaultWithCompatibility
 /** Interface for operations related to permissions. */
@@ -71,10 +70,7 @@
         fun createRequestPermissionResultContract(
             providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME
         ): ActivityResultContract<Set<String>, Set<String>> {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
-                return HealthDataRequestPermissionsUpsideDownCake()
-            }
-            return HealthDataRequestPermissionsInternal(providerPackageName = providerPackageName)
+            return HealthPermissionsRequestContract(providerPackageName)
         }
     }
 }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt
new file mode 100644
index 0000000..6e8e74b
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/ExerciseRouteRequestContract.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.contracts
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.health.connect.client.permission.RequestExerciseRouteInternal
+import androidx.health.connect.client.permission.platform.RequestExerciseRouteUpsideDownCake
+import androidx.health.connect.client.records.ExerciseRoute
+
+/**
+ * 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.
+ *
+ * @sample androidx.health.connect.client.samples.ReadExerciseRoute
+ */
+class ExerciseRouteRequestContract : ActivityResultContract<String, ExerciseRoute.Data?>() {
+
+    private val delegate: ActivityResultContract<String, ExerciseRoute.Data?> =
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            RequestExerciseRouteUpsideDownCake()
+        } else {
+            RequestExerciseRouteInternal()
+        }
+
+    /**
+     * Creates an intent to request an [ExerciseRoute.Data]. It receives the exercise session id as
+     * [input].
+     *
+     * @param context the context
+     * @param input the exercise session id obtained via
+     *   [androidx.health.connect.client.records.ExerciseSessionRecord.metadata]
+     * @throws IllegalArgumentException if the [input] is an empty string.
+     * @see ActivityResultContract.createIntent
+     */
+    override fun createIntent(context: Context, input: String): Intent {
+        return delegate.createIntent(context, input)
+    }
+
+    /**
+     * Converts the activity result into [ExerciseRoute.Data], to return as output.
+     *
+     * @return 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].
+     * @see ActivityResultContract.parseResult
+     */
+    override fun parseResult(resultCode: Int, intent: Intent?): ExerciseRoute.Data? {
+        return delegate.parseResult(resultCode, intent)
+    }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt
new file mode 100644
index 0000000..1251304
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.contracts
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.permission.HealthDataRequestPermissionsInternal
+import androidx.health.connect.client.permission.platform.HealthDataRequestPermissionsUpsideDownCake
+
+/**
+ * An [ActivityResultContract] to request Health permissions.
+ *
+ * It receives a set of permissions as input and returns a set with the granted permissions as
+ * output.
+ */
+class HealthPermissionsRequestContract(
+    providerPackageName: String = HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME
+) : ActivityResultContract<Set<String>, Set<String>>() {
+
+    private val delegate: ActivityResultContract<Set<String>, Set<String>> =
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            HealthDataRequestPermissionsUpsideDownCake()
+        } else {
+            HealthDataRequestPermissionsInternal(providerPackageName)
+        }
+
+    /**
+     * Creates an intent to request HealthConnect permissions. It receives as [input] a [Set] of
+     * HealthConnect permissions.
+     *
+     * @param context the context
+     * @param input the health permission strings to request permissions for
+     * @see ActivityResultContract.createIntent
+     */
+    override fun createIntent(context: Context, input: Set<String>): Intent {
+        return delegate.createIntent(context, input)
+    }
+
+    /**
+     * Converts the activity result into a [Set] of granted permissions. This will be a subset of
+     * [Set] passed in [createIntent].
+     *
+     * @see ActivityResultContract.parseResult
+     */
+    override fun parseResult(resultCode: Int, intent: Intent?): Set<String> {
+        return delegate.parseResult(resultCode, intent)
+    }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
index a410c3f..dc60de8 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -404,7 +404,7 @@
                     endZoneOffset = endZoneOffset,
                     metadata = metadata
                 )
-            "ActivitySession" ->
+            "ActivitySession" -> {
                 ExerciseSessionRecord(
                     exerciseType =
                         mapEnum(
@@ -421,12 +421,15 @@
                     metadata = metadata,
                     segments = subTypeDataListsMap["segments"]?.toSegmentList() ?: emptyList(),
                     laps = subTypeDataListsMap["laps"]?.toLapList() ?: emptyList(),
-                    route =
+                    exerciseRoute =
                         subTypeDataListsMap["route"]?.let {
-                            ExerciseRoute(route = it.toLocationList())
-                        },
-                    hasRoute = valuesMap["hasRoute"]?.booleanVal ?: false,
+                            ExerciseRoute.Data(route = it.toLocationList())
+                        }
+                            ?: if (valuesMap["hasRoute"]?.booleanVal == true)
+                                ExerciseRoute.ConsentRequired()
+                            else ExerciseRoute.NoData(),
                 )
+            }
             "Distance" ->
                 DistanceRecord(
                     distance = getDouble("distance").meters,
@@ -582,10 +585,10 @@
         }
     }
 
-fun toExerciseRoute(
+fun toExerciseRouteData(
     protoWrapper: androidx.health.platform.client.exerciseroute.ExerciseRoute
-): ExerciseRoute {
-    return ExerciseRoute(
+): ExerciseRoute.Data {
+    return ExerciseRoute.Data(
         protoWrapper.proto.valuesList.map { value ->
             ExerciseRoute.Location(
                 time = Instant.ofEpochMilli(value.startTimeMillis),
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
index f986b53..e9e29d8 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
@@ -34,6 +34,7 @@
 import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
 import androidx.health.connect.client.records.DistanceRecord
 import androidx.health.connect.client.records.ElevationGainedRecord
+import androidx.health.connect.client.records.ExerciseRoute
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.FloorsClimbedRecord
 import androidx.health.connect.client.records.HeartRateRecord
@@ -75,41 +76,38 @@
                 .apply {
                     putValues("temperature", doubleVal(temperature.inCelsius))
                     enumValFromInt(
-                        measurementLocation,
-                        BodyTemperatureMeasurementLocation
-                            .MEASUREMENT_LOCATION_INT_TO_STRING_MAP,
-                    )
+                            measurementLocation,
+                            BodyTemperatureMeasurementLocation
+                                .MEASUREMENT_LOCATION_INT_TO_STRING_MAP,
+                        )
                         ?.let { putValues("measurementLocation", it) }
                 }
                 .build()
-
         is BasalMetabolicRateRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BasalMetabolicRate"))
                 .apply { putValues("bmr", doubleVal(basalMetabolicRate.inKilocaloriesPerDay)) }
                 .build()
-
         is BloodGlucoseRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BloodGlucose"))
                 .apply {
                     putValues("level", doubleVal(level.inMillimolesPerLiter))
                     enumValFromInt(
-                        specimenSource,
-                        BloodGlucoseRecord.SPECIMEN_SOURCE_INT_TO_STRING_MAP
-                    )
+                            specimenSource,
+                            BloodGlucoseRecord.SPECIMEN_SOURCE_INT_TO_STRING_MAP
+                        )
                         ?.let { putValues("specimenSource", it) }
                     enumValFromInt(mealType, MealType.MEAL_TYPE_INT_TO_STRING_MAP)?.let {
                         putValues("mealType", it)
                     }
                     enumValFromInt(
-                        relationToMeal,
-                        BloodGlucoseRecord.RELATION_TO_MEAL_INT_TO_STRING_MAP,
-                    )
+                            relationToMeal,
+                            BloodGlucoseRecord.RELATION_TO_MEAL_INT_TO_STRING_MAP,
+                        )
                         ?.let { putValues("relationToMeal", it) }
                 }
                 .build()
-
         is BloodPressureRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BloodPressure"))
@@ -117,50 +115,45 @@
                     putValues("systolic", doubleVal(systolic.inMillimetersOfMercury))
                     putValues("diastolic", doubleVal(diastolic.inMillimetersOfMercury))
                     enumValFromInt(
-                        bodyPosition,
-                        BloodPressureRecord.BODY_POSITION_INT_TO_STRING_MAP
-                    )
+                            bodyPosition,
+                            BloodPressureRecord.BODY_POSITION_INT_TO_STRING_MAP
+                        )
                         ?.let { putValues("bodyPosition", it) }
                     enumValFromInt(
-                        measurementLocation,
-                        BloodPressureRecord.MEASUREMENT_LOCATION_INT_TO_STRING_MAP
-                    )
+                            measurementLocation,
+                            BloodPressureRecord.MEASUREMENT_LOCATION_INT_TO_STRING_MAP
+                        )
                         ?.let { putValues("measurementLocation", it) }
                 }
                 .build()
-
         is BodyFatRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BodyFat"))
                 .apply { putValues("percentage", doubleVal(percentage.value)) }
                 .build()
-
         is BodyTemperatureRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BodyTemperature"))
                 .apply {
                     putValues("temperature", doubleVal(temperature.inCelsius))
                     enumValFromInt(
-                        measurementLocation,
-                        BodyTemperatureMeasurementLocation
-                            .MEASUREMENT_LOCATION_INT_TO_STRING_MAP,
-                    )
+                            measurementLocation,
+                            BodyTemperatureMeasurementLocation
+                                .MEASUREMENT_LOCATION_INT_TO_STRING_MAP,
+                        )
                         ?.let { putValues("measurementLocation", it) }
                 }
                 .build()
-
         is BodyWaterMassRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BodyWaterMass"))
                 .apply { putValues("mass", doubleVal(mass.inKilograms)) }
                 .build()
-
         is BoneMassRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BoneMass"))
                 .apply { putValues("mass", doubleVal(mass.inKilograms)) }
                 .build()
-
         is CervicalMucusRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("CervicalMucus"))
@@ -173,7 +166,6 @@
                     }
                 }
                 .build()
-
         is CyclingPedalingCadenceRecord ->
             toProto(dataTypeName = "CyclingPedalingCadenceSeries") { sample ->
                 DataProto.SeriesValue.newBuilder()
@@ -181,7 +173,6 @@
                     .setInstantTimeMillis(sample.time.toEpochMilli())
                     .build()
             }
-
         is HeartRateRecord ->
             toProto(dataTypeName = "HeartRateSeries") { sample ->
                 DataProto.SeriesValue.newBuilder()
@@ -189,28 +180,23 @@
                     .setInstantTimeMillis(sample.time.toEpochMilli())
                     .build()
             }
-
         is HeightRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("Height"))
                 .apply { putValues("height", doubleVal(height.inMeters)) }
                 .build()
-
         is HeartRateVariabilityRmssdRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("HeartRateVariabilityRmssd"))
                 .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                 .build()
-
         is IntermenstrualBleedingRecord ->
             instantaneousProto().setDataType(protoDataType("IntermenstrualBleeding")).build()
-
         is LeanBodyMassRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("LeanBodyMass"))
                 .apply { putValues("mass", doubleVal(mass.inKilograms)) }
                 .build()
-
         is MenstruationFlowRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("Menstruation"))
@@ -220,10 +206,8 @@
                     }
                 }
                 .build()
-
         is MenstruationPeriodRecord ->
             intervalProto().setDataType(protoDataType("MenstruationPeriod")).build()
-
         is OvulationTestRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("OvulationTest"))
@@ -233,13 +217,11 @@
                     }
                 }
                 .build()
-
         is OxygenSaturationRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("OxygenSaturation"))
                 .apply { putValues("percentage", doubleVal(percentage.value)) }
                 .build()
-
         is PowerRecord ->
             toProto(dataTypeName = "PowerSeries") { sample ->
                 DataProto.SeriesValue.newBuilder()
@@ -247,31 +229,27 @@
                     .setInstantTimeMillis(sample.time.toEpochMilli())
                     .build()
             }
-
         is RespiratoryRateRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("RespiratoryRate"))
                 .apply { putValues("rate", doubleVal(rate)) }
                 .build()
-
         is RestingHeartRateRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("RestingHeartRate"))
                 .apply { putValues("bpm", longVal(beatsPerMinute)) }
                 .build()
-
         is SexualActivityRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("SexualActivity"))
                 .apply {
                     enumValFromInt(
-                        protectionUsed,
-                        SexualActivityRecord.PROTECTION_USED_INT_TO_STRING_MAP
-                    )
+                            protectionUsed,
+                            SexualActivityRecord.PROTECTION_USED_INT_TO_STRING_MAP
+                        )
                         ?.let { putValues("protectionUsed", it) }
                 }
                 .build()
-
         is SpeedRecord ->
             toProto(dataTypeName = "SpeedSeries") { sample ->
                 DataProto.SeriesValue.newBuilder()
@@ -279,7 +257,6 @@
                     .setInstantTimeMillis(sample.time.toEpochMilli())
                     .build()
             }
-
         is StepsCadenceRecord ->
             toProto(dataTypeName = "StepsCadenceSeries") { sample ->
                 DataProto.SeriesValue.newBuilder()
@@ -287,36 +264,32 @@
                     .setInstantTimeMillis(sample.time.toEpochMilli())
                     .build()
             }
-
         is Vo2MaxRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("Vo2Max"))
                 .apply {
                     putValues("vo2", doubleVal(vo2MillilitersPerMinuteKilogram))
                     enumValFromInt(
-                        measurementMethod,
-                        Vo2MaxRecord.MEASUREMENT_METHOD_INT_TO_STRING_MAP
-                    )
+                            measurementMethod,
+                            Vo2MaxRecord.MEASUREMENT_METHOD_INT_TO_STRING_MAP
+                        )
                         ?.let { putValues("measurementMethod", it) }
                 }
                 .build()
-
         is WeightRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("Weight"))
                 .apply { putValues("weight", doubleVal(weight.inKilograms)) }
                 .build()
-
         is ActiveCaloriesBurnedRecord ->
             intervalProto()
                 .setDataType(protoDataType("ActiveCaloriesBurned"))
                 .apply { putValues("energy", doubleVal(energy.inKilocalories)) }
                 .build()
-
         is ExerciseSessionRecord ->
             intervalProto()
                 .setDataType(protoDataType("ActivitySession"))
-                .putValues("hasRoute", boolVal(hasRoute))
+                .putValues("hasRoute", boolVal(exerciseRoute !is ExerciseRoute.NoData))
                 .apply {
                     val exerciseType =
                         enumValFromInt(
@@ -331,50 +304,48 @@
                         putSubTypeDataLists(
                             "segments",
                             DataProto.DataPoint.SubTypeDataList.newBuilder()
-                                .addAllValues(segments.map { it.toProto() }).build()
+                                .addAllValues(segments.map { it.toProto() })
+                                .build()
                         )
                     }
                     if (laps.isNotEmpty()) {
                         putSubTypeDataLists(
                             "laps",
                             DataProto.DataPoint.SubTypeDataList.newBuilder()
-                                .addAllValues(laps.map { it.toProto() }).build()
+                                .addAllValues(laps.map { it.toProto() })
+                                .build()
                         )
                     }
-                    route?.let { exerciseRoute ->
+                    if (exerciseRoute is ExerciseRoute.Data) {
                         putSubTypeDataLists(
                             "route",
                             DataProto.DataPoint.SubTypeDataList.newBuilder()
-                                .addAllValues(exerciseRoute.route.map { it.toProto() }).build()
+                                .addAllValues(exerciseRoute.route.map { it.toProto() })
+                                .build()
                         )
                     }
                 }
                 .build()
-
         is DistanceRecord ->
             intervalProto()
                 .setDataType(protoDataType("Distance"))
                 .apply { putValues("distance", doubleVal(distance.inMeters)) }
                 .build()
-
         is ElevationGainedRecord ->
             intervalProto()
                 .setDataType(protoDataType("ElevationGained"))
                 .apply { putValues("elevation", doubleVal(elevation.inMeters)) }
                 .build()
-
         is FloorsClimbedRecord ->
             intervalProto()
                 .setDataType(protoDataType("FloorsClimbed"))
                 .apply { putValues("floors", doubleVal(floors)) }
                 .build()
-
         is HydrationRecord ->
             intervalProto()
                 .setDataType(protoDataType("Hydration"))
                 .apply { putValues("volume", doubleVal(volume.inLiters)) }
                 .build()
-
         is NutritionRecord ->
             intervalProto()
                 .setDataType(protoDataType("Nutrition"))
@@ -511,7 +482,6 @@
                     name?.let { putValues("name", stringVal(it)) }
                 }
                 .build()
-
         is SleepSessionRecord ->
             intervalProto()
                 .setDataType(protoDataType("SleepSession"))
@@ -520,14 +490,14 @@
                         putSubTypeDataLists(
                             "stages",
                             DataProto.DataPoint.SubTypeDataList.newBuilder()
-                                .addAllValues(stages.map { it.toProto() }).build()
+                                .addAllValues(stages.map { it.toProto() })
+                                .build()
                         )
                     }
                     title?.let { putValues("title", stringVal(it)) }
                     notes?.let { putValues("notes", stringVal(it)) }
                 }
                 .build()
-
         is SleepStageRecord ->
             intervalProto()
                 .setDataType(protoDataType("SleepStage"))
@@ -537,25 +507,21 @@
                     }
                 }
                 .build()
-
         is StepsRecord ->
             intervalProto()
                 .setDataType(protoDataType("Steps"))
                 .apply { putValues("count", longVal(count)) }
                 .build()
-
         is TotalCaloriesBurnedRecord ->
             intervalProto()
                 .setDataType(protoDataType("TotalCaloriesBurned"))
                 .apply { putValues("energy", doubleVal(energy.inKilocalories)) }
                 .build()
-
         is WheelchairPushesRecord ->
             intervalProto()
                 .setDataType(protoDataType("WheelchairPushes"))
                 .apply { putValues("count", longVal(count)) }
                 .build()
-
         else -> throw RuntimeException("Unsupported yet!")
     }
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordConverters.kt
index e82005f..4da86be 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordConverters.kt
@@ -290,11 +290,11 @@
         exerciseType = exerciseType.toSdkExerciseSessionType(),
         title = title?.toString(),
         notes = notes?.toString(),
-        route = route?.toSdkExerciseRoute(),
         laps = laps.map { it.toSdkExerciseLap() }.sortedBy { it.startTime },
         segments = segments.map { it.toSdkExerciseSegment() }.sortedBy { it.startTime },
-        hasRoute = hasRoute(),
         metadata = metadata.toSdkMetadata(),
+        exerciseRoute = route?.toSdkExerciseRouteData()
+                ?: if (hasRoute()) ExerciseRoute.ConsentRequired() else ExerciseRoute.NoData(),
     )
 
 private fun PlatformFloorsClimbedRecord.toSdkFloorsClimbedRecord() =
@@ -706,9 +706,11 @@
             endZoneOffset?.let { setEndZoneOffset(it) }
             notes?.let { setNotes(it) }
             title?.let { setTitle(it) }
-            route?.let { setRoute(it.toPlatformExerciseRoute()) }
             setLaps(laps.map { it.toPlatformExerciseLap() })
             setSegments(segments.map { it.toPlatformExerciseSegment() })
+            if (exerciseRoute is ExerciseRoute.Data) {
+                setRoute(exerciseRoute.toPlatformExerciseRoute())
+            }
         }
         .build()
 
@@ -717,7 +719,7 @@
         .apply { length?.let { setLength(it.toPlatformLength()) } }
         .build()
 
-private fun ExerciseRoute.toPlatformExerciseRoute() =
+private fun ExerciseRoute.Data.toPlatformExerciseRoute() =
     PlatformExerciseRoute(
         route.map { location ->
             PlatformExerciseRouteLocationBuilder(
@@ -1031,8 +1033,8 @@
 private fun PlatformSleepSessionStage.toSdkSleepSessionStage() =
     SleepSessionRecord.Stage(startTime, endTime, type.toSdkSleepStageType())
 
-internal fun PlatformExerciseRoute.toSdkExerciseRoute() =
-    ExerciseRoute(
+internal fun PlatformExerciseRoute.toSdkExerciseRouteData() =
+    ExerciseRoute.Data(
         routeLocations.map { value ->
             ExerciseRoute.Location(
                 time = value.time,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt
index 4417c02..1fdba9c 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/RequestExerciseRouteInternal.kt
@@ -21,7 +21,7 @@
 import androidx.activity.result.contract.ActivityResultContract
 import androidx.annotation.RestrictTo
 import androidx.health.connect.client.HealthConnectClient
-import androidx.health.connect.client.impl.converters.records.toExerciseRoute
+import androidx.health.connect.client.impl.converters.records.toExerciseRouteData
 import androidx.health.connect.client.records.ExerciseRoute
 import androidx.health.platform.client.impl.logger.Logger
 import androidx.health.platform.client.service.HealthDataServiceConstants
@@ -32,16 +32,17 @@
  * @see androidx.activity.ComponentActivity.registerForActivityResult
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class RequestExerciseRouteInternal : ActivityResultContract<String?, ExerciseRoute?>() {
-    override fun createIntent(context: Context, input: String?): Intent {
-        require(!input.isNullOrEmpty()) { "Session identifier is required" }
+internal class RequestExerciseRouteInternal :
+    ActivityResultContract<String, ExerciseRoute.Data?>() {
+    override fun createIntent(context: Context, input: String): Intent {
+        require(input.isNotEmpty()) { "Session identifier can't be empty" }
         return Intent(HealthDataServiceConstants.ACTION_REQUEST_ROUTE).apply {
             putExtra(HealthDataServiceConstants.EXTRA_SESSION_ID, input)
         }
     }
 
     @Suppress("DEPRECATION") // getParcelableExtra
-    override fun parseResult(resultCode: Int, intent: Intent?): ExerciseRoute? {
+    override fun parseResult(resultCode: Int, intent: Intent?): ExerciseRoute.Data? {
         val route =
             intent?.getParcelableExtra<androidx.health.platform.client.exerciseroute.ExerciseRoute>(
                 HealthDataServiceConstants.EXTRA_EXERCISE_ROUTE
@@ -51,6 +52,6 @@
             return null
         }
         Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "Returned a route.")
-        return toExerciseRoute(route)
+        return toExerciseRouteData(route)
     }
 }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt
index 8582568..f1a435a 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/RequestExerciseRouteUpsideDownCake.kt
@@ -24,7 +24,7 @@
 import androidx.annotation.RestrictTo
 import androidx.health.connect.client.HealthConnectClient
 import androidx.health.connect.client.impl.platform.records.PlatformExerciseRoute
-import androidx.health.connect.client.impl.platform.records.toSdkExerciseRoute
+import androidx.health.connect.client.impl.platform.records.toSdkExerciseRouteData
 import androidx.health.connect.client.records.ExerciseRoute
 import androidx.health.platform.client.impl.logger.Logger
 
@@ -36,15 +36,15 @@
 @RequiresApi(34)
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 internal class RequestExerciseRouteUpsideDownCake :
-    ActivityResultContract<String?, ExerciseRoute?>() {
-    override fun createIntent(context: Context, input: String?): Intent {
-        require(!input.isNullOrEmpty()) { "Session identifier is required" }
+    ActivityResultContract<String, ExerciseRoute.Data?>() {
+    override fun createIntent(context: Context, input: String): Intent {
+        require(input.isNotEmpty()) { "Session identifier can't be empty" }
         return Intent(HealthConnectManager.ACTION_REQUEST_EXERCISE_ROUTE).apply {
             putExtra(HealthConnectManager.EXTRA_SESSION_ID, input)
         }
     }
 
-    override fun parseResult(resultCode: Int, intent: Intent?): ExerciseRoute? {
+    override fun parseResult(resultCode: Int, intent: Intent?): ExerciseRoute.Data? {
         val route =
             intent?.getParcelableExtra(
                 HealthConnectManager.EXTRA_EXERCISE_ROUTE,
@@ -55,6 +55,6 @@
             return null
         }
         Logger.debug(HealthConnectClient.HEALTH_CONNECT_CLIENT_TAG, "Returned a route.")
-        return route.toSdkExerciseRoute()
+        return route.toSdkExerciseRouteData()
     }
 }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt
index 26f9961..6f36983 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRoute.kt
@@ -16,43 +16,66 @@
 
 package androidx.health.connect.client.records
 
-import androidx.annotation.RestrictTo
 import androidx.health.connect.client.units.Length
 import java.time.Instant
 
-/**
- * Captures a route associated with an exercise session a user does.
- *
- * Contains a sequence of location points, with timestamps, which do not have to be in order.
- *
- * Location points contain a timestamp, longitude, latitude, and optionally altitude, horizontal and
- * vertical accuracy.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class ExerciseRoute(public val route: List<Location>) {
+/** Captures a route associated with an exercise session a user does. */
+abstract class ExerciseRoute internal constructor() {
 
-    init {
-        val sortedRoute: List<Location> = route.sortedWith { a, b -> a.time.compareTo(b.time) }
-        for (i in 0 until sortedRoute.lastIndex) {
-            require(sortedRoute[i].time.isBefore(sortedRoute[i + 1].time))
+    /**
+     * Class containing data of an exercise route.
+     *
+     * Contains a sequence of location points, with timestamps, which do not have to be in order.
+     *
+     * Location points contain a timestamp, longitude, latitude, and optionally altitude, horizontal
+     * and vertical accuracy.
+     */
+    class Data constructor(val route: List<Location>) : ExerciseRoute() {
+        init {
+            val sortedRoute: List<Location> = route.sortedBy { it.time }
+            for (i in 0 until sortedRoute.lastIndex) {
+                require(sortedRoute[i].time.isBefore(sortedRoute[i + 1].time))
+            }
+        }
+
+        internal fun isWithin(startTime: Instant, endTime: Instant): Boolean {
+            val minTime = route.minBy { it.time }.time
+            val maxTime = route.maxBy { it.time }.time
+            return !minTime.isBefore(startTime) && maxTime.isBefore(endTime)
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Data) return false
+
+            return route == other.route
+        }
+
+        override fun hashCode(): Int {
+            return route.hashCode()
         }
     }
-    internal fun isWithin(startTime: Instant, endTime: Instant): Boolean {
-        // startTime is inclusive, endTime is exclusive
-        val sortedRoute: List<Location> = route.sortedWith { a, b -> a.time.compareTo(b.time) }
-        return !sortedRoute.first().time.isBefore(startTime) &&
-            sortedRoute.last().time.isBefore(endTime)
+
+    /** Class indicating that a permission hasn't been granted and a value couldn't be returned. */
+    class ConsentRequired : ExerciseRoute() {
+        override fun equals(other: Any?): Boolean {
+            return other is ConsentRequired
+        }
+
+        override fun hashCode(): Int {
+            return 0
+        }
     }
 
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is ExerciseRoute) return false
+    /** Class indicating that there's no data to request permissions for. */
+    class NoData : ExerciseRoute() {
+        override fun equals(other: Any?): Boolean {
+            return other is NoData
+        }
 
-        return route == other.route
-    }
-
-    override fun hashCode(): Int {
-        return route.hashCode()
+        override fun hashCode(): Int {
+            return 0
+        }
     }
 
     /**
@@ -67,7 +90,7 @@
      * @param verticalAccuracy in [Length] unit. Optional field. Valid range: non-negative numbers.
      * @see ExerciseRoute
      */
-    public class Location(
+    class Location(
         val time: Instant,
         val latitude: Double,
         val longitude: Double,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
index 1709165..1a3747c 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
@@ -34,42 +34,37 @@
  *
  * @sample androidx.health.connect.client.samples.ReadExerciseSessions
  */
-public class ExerciseSessionRecord
+class ExerciseSessionRecord
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-constructor(
+internal constructor(
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
     /** Type of exercise (e.g. walking, swimming). Required field. */
-    @property:ExerciseTypes public val exerciseType: Int,
+    @property:ExerciseTypes val exerciseType: Int,
     /** Title of the session. Optional field. */
-    public val title: String? = null,
+    val title: String? = null,
     /** Additional notes for the session. Optional field. */
-    public val notes: String? = null,
+    val notes: String? = null,
     override val metadata: Metadata = Metadata.EMPTY,
     /**
      * [ExerciseSegment]s of the session. Optional field. Time in segments should be within the
      * parent session, and should not overlap with each other.
      */
-    public val segments: List<ExerciseSegment> = emptyList(),
+    val segments: List<ExerciseSegment> = emptyList(),
     /**
      * [ExerciseLap]s of the session. Optional field. Time in laps should be within the parent
      * session, and should not overlap with each other.
      */
-    public val laps: List<ExerciseLap> = emptyList(),
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
-    /** [ExerciseRoute]s of the session. Optional field. */
-    public val route: ExerciseRoute? = null,
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
-    /**
-     * Indicates whether or not the underlying [ExerciseSessionRecord] has a route, even it's not
-     * present because lack of permission.
-     */
-    public val hasRoute: Boolean = false,
+    val laps: List<ExerciseLap> = emptyList(),
+
+    /** [ExerciseRoute] [ExerciseRoute] of the session. */
+    val exerciseRoute: ExerciseRoute = ExerciseRoute.NoData(),
 ) : IntervalRecord {
 
-    public constructor(
+    @JvmOverloads
+    constructor(
         startTime: Instant,
         startZoneOffset: ZoneOffset?,
         endTime: Instant,
@@ -83,6 +78,7 @@
         metadata: Metadata = Metadata.EMPTY,
         segments: List<ExerciseSegment> = emptyList(),
         laps: List<ExerciseLap> = emptyList(),
+        exerciseRouteData: ExerciseRoute.Data? = null,
     ) : this(
         startTime,
         startZoneOffset,
@@ -94,38 +90,7 @@
         metadata,
         segments,
         laps,
-        null
-    )
-
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    public constructor(
-        startTime: Instant,
-        startZoneOffset: ZoneOffset?,
-        endTime: Instant,
-        endZoneOffset: ZoneOffset?,
-        /** Type of exercise (e.g. walking, swimming). Required field. */
-        exerciseType: Int,
-        /** Title of the session. Optional field. */
-        title: String? = null,
-        /** Additional notes for the session. Optional field. */
-        notes: String? = null,
-        metadata: Metadata = Metadata.EMPTY,
-        segments: List<ExerciseSegment> = emptyList(),
-        laps: List<ExerciseLap> = emptyList(),
-        route: ExerciseRoute? = null,
-    ) : this(
-        startTime,
-        startZoneOffset,
-        endTime,
-        endZoneOffset,
-        exerciseType,
-        title,
-        notes,
-        metadata,
-        segments,
-        laps,
-        route,
-        route != null
+        exerciseRouteData ?: ExerciseRoute.NoData()
     )
 
     init {
@@ -165,9 +130,8 @@
                 "laps can not be out of parent time range."
             }
         }
-        require(route == null || hasRoute) { "hasRoute must be true if the route is not null" }
-        if (route != null) {
-            require(route.isWithin(startTime, endTime)) {
+        if (exerciseRoute is ExerciseRoute.Data) {
+            require(exerciseRoute.isWithin(startTime, endTime)) {
                 "route can not be out of parent time range."
             }
         }
@@ -187,21 +151,20 @@
         if (metadata != other.metadata) return false
         if (segments != other.segments) return false
         if (laps != other.laps) return false
-        if (route != other.route) return false
-        if (hasRoute != other.hasRoute) return false
+        if (exerciseRoute != other.exerciseRoute) return false
 
         return true
     }
 
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + exerciseType.hashCode()
+        var result = exerciseType.hashCode()
         result = 31 * result + title.hashCode()
         result = 31 * result + notes.hashCode()
         result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
         result = 31 * result + endTime.hashCode()
         result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
         result = 31 * result + metadata.hashCode()
+        result = 31 * result + exerciseRoute.hashCode()
         return result
     }
 
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index 21b3465..ac86a98 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -125,7 +125,7 @@
             BasalBodyTemperatureRecord(
                 temperature = 1.celsius,
                 measurementLocation =
-                BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_ARMPIT,
+                    BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_ARMPIT,
                 time = START_TIME,
                 zoneOffset = END_ZONE_OFFSET,
                 metadata = TEST_METADATA
@@ -259,16 +259,16 @@
                 endTime = END_TIME,
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
-                listOf(
-                    CyclingPedalingCadenceRecord.Sample(
-                        time = START_TIME,
-                        revolutionsPerMinute = 1.0,
+                    listOf(
+                        CyclingPedalingCadenceRecord.Sample(
+                            time = START_TIME,
+                            revolutionsPerMinute = 1.0,
+                        ),
+                        CyclingPedalingCadenceRecord.Sample(
+                            time = START_TIME,
+                            revolutionsPerMinute = 2.0,
+                        ),
                     ),
-                    CyclingPedalingCadenceRecord.Sample(
-                        time = START_TIME,
-                        revolutionsPerMinute = 2.0,
-                    ),
-                ),
                 metadata = TEST_METADATA,
             )
 
@@ -285,20 +285,20 @@
                 endTime = END_TIME,
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
-                listOf(
-                    HeartRateRecord.Sample(
-                        time = START_TIME,
-                        beatsPerMinute = 100L,
+                    listOf(
+                        HeartRateRecord.Sample(
+                            time = START_TIME,
+                            beatsPerMinute = 100L,
+                        ),
+                        HeartRateRecord.Sample(
+                            time = START_TIME,
+                            beatsPerMinute = 110L,
+                        ),
+                        HeartRateRecord.Sample(
+                            time = START_TIME,
+                            beatsPerMinute = 120L,
+                        ),
                     ),
-                    HeartRateRecord.Sample(
-                        time = START_TIME,
-                        beatsPerMinute = 110L,
-                    ),
-                    HeartRateRecord.Sample(
-                        time = START_TIME,
-                        beatsPerMinute = 120L,
-                    ),
-                ),
                 metadata = TEST_METADATA,
             )
 
@@ -448,16 +448,16 @@
                 endTime = END_TIME,
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
-                listOf(
-                    PowerRecord.Sample(
-                        time = START_TIME,
-                        power = 1.watts,
+                    listOf(
+                        PowerRecord.Sample(
+                            time = START_TIME,
+                            power = 1.watts,
+                        ),
+                        PowerRecord.Sample(
+                            time = START_TIME,
+                            power = 2.watts,
+                        ),
                     ),
-                    PowerRecord.Sample(
-                        time = START_TIME,
-                        power = 2.watts,
-                    ),
-                ),
                 metadata = TEST_METADATA,
             )
 
@@ -516,16 +516,16 @@
                 endTime = END_TIME,
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
-                listOf(
-                    SpeedRecord.Sample(
-                        time = START_TIME,
-                        speed = 1.metersPerSecond,
+                    listOf(
+                        SpeedRecord.Sample(
+                            time = START_TIME,
+                            speed = 1.metersPerSecond,
+                        ),
+                        SpeedRecord.Sample(
+                            time = START_TIME,
+                            speed = 2.metersPerSecond,
+                        ),
                     ),
-                    SpeedRecord.Sample(
-                        time = START_TIME,
-                        speed = 2.metersPerSecond,
-                    ),
-                ),
                 metadata = TEST_METADATA,
             )
 
@@ -542,16 +542,16 @@
                 endTime = END_TIME,
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
-                listOf(
-                    StepsCadenceRecord.Sample(
-                        time = START_TIME,
-                        rate = 1.0,
+                    listOf(
+                        StepsCadenceRecord.Sample(
+                            time = START_TIME,
+                            rate = 1.0,
+                        ),
+                        StepsCadenceRecord.Sample(
+                            time = START_TIME,
+                            rate = 2.0,
+                        ),
                     ),
-                    StepsCadenceRecord.Sample(
-                        time = START_TIME,
-                        rate = 2.0,
-                    ),
-                ),
                 metadata = TEST_METADATA,
             )
 
@@ -616,48 +616,51 @@
                 endTime = END_TIME,
                 endZoneOffset = END_ZONE_OFFSET,
                 metadata = TEST_METADATA,
-                segments = listOf(
-                    ExerciseSegment(
-                        startTime = Instant.ofEpochMilli(1234L),
-                        endTime = Instant.ofEpochMilli(1235L),
-                        segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_CRUNCH,
-                        repetitions = 10
-                    ),
-                    ExerciseSegment(
-                        startTime = Instant.ofEpochMilli(1235L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                        segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_CRUNCH,
-                    )
-                ),
-                laps = listOf(
-                    ExerciseLap(
-                        startTime = Instant.ofEpochMilli(1234L),
-                        endTime = Instant.ofEpochMilli(1235L),
-                        length = 1.meters
-                    ),
-                    ExerciseLap(
-                        startTime = Instant.ofEpochMilli(1235L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                    )
-                ),
-                route = ExerciseRoute(
-                    route = listOf(
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(1234L),
-                            latitude = 34.5,
-                            longitude = -34.5,
-                            horizontalAccuracy = Length.meters(0.4),
-                            verticalAccuracy = Length.meters(1.3),
-                            altitude = Length.meters(23.4)
+                segments =
+                    listOf(
+                        ExerciseSegment(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1235L),
+                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_CRUNCH,
+                            repetitions = 10
                         ),
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(1235L),
-                            latitude = 34.5,
-                            longitude = -34.5,
+                        ExerciseSegment(
+                            startTime = Instant.ofEpochMilli(1235L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_CRUNCH,
+                        )
+                    ),
+                laps =
+                    listOf(
+                        ExerciseLap(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1235L),
+                            length = 1.meters
                         ),
-                    )
-                ),
-                hasRoute = true,
+                        ExerciseLap(
+                            startTime = Instant.ofEpochMilli(1235L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                        )
+                    ),
+                exerciseRoute =
+                    ExerciseRoute.Data(
+                        route =
+                            listOf(
+                                ExerciseRoute.Location(
+                                    time = Instant.ofEpochMilli(1234L),
+                                    latitude = 34.5,
+                                    longitude = -34.5,
+                                    horizontalAccuracy = Length.meters(0.4),
+                                    verticalAccuracy = Length.meters(1.3),
+                                    altitude = Length.meters(23.4)
+                                ),
+                                ExerciseRoute.Location(
+                                    time = Instant.ofEpochMilli(1235L),
+                                    latitude = 34.5,
+                                    longitude = -34.5,
+                                ),
+                            )
+                    ),
             )
 
         checkProtoAndRecordTypeNameMatch(data)
@@ -673,6 +676,7 @@
                 startZoneOffset = null,
                 endTime = END_TIME,
                 endZoneOffset = null,
+                exerciseRouteData = null,
             )
 
         checkProtoAndRecordTypeNameMatch(data)
@@ -813,13 +817,14 @@
                 endTime = END_TIME,
                 endZoneOffset = END_ZONE_OFFSET,
                 metadata = TEST_METADATA,
-                stages = listOf(
-                    SleepSessionRecord.Stage(
-                        startTime = Instant.ofEpochMilli(1234L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                        stage = SleepSessionRecord.STAGE_TYPE_DEEP,
+                stages =
+                    listOf(
+                        SleepSessionRecord.Stage(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                            stage = SleepSessionRecord.STAGE_TYPE_DEEP,
+                        )
                     )
-                )
             )
 
         checkProtoAndRecordTypeNameMatch(data)
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/RequestExerciseRouteInternalTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/RequestExerciseRouteInternalTest.kt
index b650485..ffa47f9 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/RequestExerciseRouteInternalTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/RequestExerciseRouteInternalTest.kt
@@ -73,7 +73,7 @@
             )
         )
         val result = requestRouteContract.parseResult(0, intent)
-        assertThat(result).isEqualTo(ExerciseRoute(listOf()))
+        assertThat(result).isEqualTo(ExerciseRoute.Data(listOf()))
     }
 
     @Test
@@ -114,7 +114,7 @@
         val result = requestRouteContract.parseResult(0, intent)
         assertThat(result)
             .isEqualTo(
-                ExerciseRoute(
+                ExerciseRoute.Data(
                     listOf(
                         ExerciseRoute.Location(
                             time = Instant.ofEpochMilli(1234L),
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseRouteTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseRouteTest.kt
index d31308f..46afdf5 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseRouteTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseRouteTest.kt
@@ -27,15 +27,15 @@
     @Test
     fun validLocation_equals() {
         assertThat(
-            ExerciseRoute.Location(
-                time = Instant.ofEpochMilli(1234L),
-                latitude = 34.5,
-                longitude = -34.5,
-                horizontalAccuracy = Length.meters(0.4),
-                verticalAccuracy = Length.meters(1.3),
-                altitude = Length.meters(23.4)
+                ExerciseRoute.Location(
+                    time = Instant.ofEpochMilli(1234L),
+                    latitude = 34.5,
+                    longitude = -34.5,
+                    horizontalAccuracy = Length.meters(0.4),
+                    verticalAccuracy = Length.meters(1.3),
+                    altitude = Length.meters(23.4)
+                )
             )
-        )
             .isEqualTo(
                 ExerciseRoute.Location(
                     time = Instant.ofEpochMilli(1234L),
@@ -74,7 +74,7 @@
 
     @Test
     fun emptyRoute() {
-        assertThat(ExerciseRoute(listOf())).isEqualTo(ExerciseRoute(listOf()))
+        assertThat(ExerciseRoute.Data(listOf())).isEqualTo(ExerciseRoute.Data(listOf()))
     }
 
     @Test
@@ -94,23 +94,26 @@
                 latitude = 34.8,
                 longitude = -34.8,
             )
-        assertThat(ExerciseRoute(listOf(location1, location2)))
-            .isEqualTo(ExerciseRoute(listOf(location1, location2)))
+        assertThat(ExerciseRoute.Data(listOf(location1, location2)))
+            .isEqualTo(ExerciseRoute.Data(listOf(location1, location2)))
     }
 
     @Test
     fun locationTimeOverlap_throws() {
-        val location1 = ExerciseRoute.Location(
-            time = Instant.ofEpochMilli(1234L),
-            latitude = 34.5,
-            longitude = -34.5,
-        )
+        val location1 =
+            ExerciseRoute.Location(
+                time = Instant.ofEpochMilli(1234L),
+                latitude = 34.5,
+                longitude = -34.5,
+            )
         val location2 =
             ExerciseRoute.Location(
                 time = Instant.ofEpochMilli(1234L),
                 latitude = 34.8,
                 longitude = -34.8,
             )
-        assertFailsWith<IllegalArgumentException> { ExerciseRoute(listOf(location1, location2)) }
+        assertFailsWith<IllegalArgumentException> {
+            ExerciseRoute.Data(listOf(location1, location2))
+        }
     }
 }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
index 57efb01..6adb8bb 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
@@ -37,43 +37,46 @@
     @Test
     fun validRecord_equals() {
         assertThat(
-            ExerciseSessionRecord(
-                startTime = Instant.ofEpochMilli(1234L),
-                startZoneOffset = null,
-                endTime = Instant.ofEpochMilli(1236L),
-                endZoneOffset = null,
-                exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_BIKING,
-                title = "title",
-                notes = "notes",
-                segments = listOf(
-                    ExerciseSegment(
-                        startTime = Instant.ofEpochMilli(1234L),
-                        endTime = Instant.ofEpochMilli(1235L),
-                        segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
-                    )
-                ),
-                laps = listOf(
-                    ExerciseLap(
-                        startTime = Instant.ofEpochMilli(1235L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                        length = 10.meters,
-                    )
-                ),
-                route = ExerciseRoute(
-                    route = listOf(
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(1234L),
-                            latitude = 34.5,
-                            longitude = -34.5,
-                            horizontalAccuracy = 0.4.meters,
-                            verticalAccuracy = 1.3.meters,
-                            altitude = 23.4.meters
-                        )
-                    )
-                ),
-                hasRoute = true,
+                ExerciseSessionRecord(
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = null,
+                    endTime = Instant.ofEpochMilli(1236L),
+                    endZoneOffset = null,
+                    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_BIKING,
+                    title = "title",
+                    notes = "notes",
+                    segments =
+                        listOf(
+                            ExerciseSegment(
+                                startTime = Instant.ofEpochMilli(1234L),
+                                endTime = Instant.ofEpochMilli(1235L),
+                                segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                            )
+                        ),
+                    laps =
+                        listOf(
+                            ExerciseLap(
+                                startTime = Instant.ofEpochMilli(1235L),
+                                endTime = Instant.ofEpochMilli(1236L),
+                                length = 10.meters,
+                            )
+                        ),
+                    exerciseRoute =
+                        ExerciseRoute.Data(
+                            route =
+                                listOf(
+                                    ExerciseRoute.Location(
+                                        time = Instant.ofEpochMilli(1234L),
+                                        latitude = 34.5,
+                                        longitude = -34.5,
+                                        horizontalAccuracy = 0.4.meters,
+                                        verticalAccuracy = 1.3.meters,
+                                        altitude = 23.4.meters
+                                    )
+                                )
+                        ),
+                )
             )
-        )
             .isEqualTo(
                 ExerciseSessionRecord(
                     startTime = Instant.ofEpochMilli(1234L),
@@ -83,33 +86,36 @@
                     exerciseType = EXERCISE_TYPE_BIKING,
                     title = "title",
                     notes = "notes",
-                    segments = listOf(
-                        ExerciseSegment(
-                            startTime = Instant.ofEpochMilli(1234L),
-                            endTime = Instant.ofEpochMilli(1235L),
-                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
-                        )
-                    ),
-                    laps = listOf(
-                        ExerciseLap(
-                            startTime = Instant.ofEpochMilli(1235L),
-                            endTime = Instant.ofEpochMilli(1236L),
-                            length = 10.meters,
-                        )
-                    ),
-                    route = ExerciseRoute(
-                        route = listOf(
-                            ExerciseRoute.Location(
-                                time = Instant.ofEpochMilli(1234L),
-                                latitude = 34.5,
-                                longitude = -34.5,
-                                horizontalAccuracy = 0.4.meters,
-                                verticalAccuracy = 1.3.meters,
-                                altitude = 23.4.meters
+                    segments =
+                        listOf(
+                            ExerciseSegment(
+                                startTime = Instant.ofEpochMilli(1234L),
+                                endTime = Instant.ofEpochMilli(1235L),
+                                segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
                             )
-                        )
-                    ),
-                    hasRoute = true,
+                        ),
+                    laps =
+                        listOf(
+                            ExerciseLap(
+                                startTime = Instant.ofEpochMilli(1235L),
+                                endTime = Instant.ofEpochMilli(1236L),
+                                length = 10.meters,
+                            )
+                        ),
+                    exerciseRoute =
+                        ExerciseRoute.Data(
+                            route =
+                                listOf(
+                                    ExerciseRoute.Location(
+                                        time = Instant.ofEpochMilli(1234L),
+                                        latitude = 34.5,
+                                        longitude = -34.5,
+                                        horizontalAccuracy = 0.4.meters,
+                                        verticalAccuracy = 1.3.meters,
+                                        altitude = 23.4.meters
+                                    )
+                                )
+                        ),
                 )
             )
     }
@@ -125,6 +131,7 @@
                 exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
                 title = "title",
                 notes = "notes",
+                exerciseRouteData = null,
             )
         }
     }
@@ -166,13 +173,15 @@
                 endTime = Instant.ofEpochMilli(1235L),
                 endZoneOffset = null,
                 exerciseType = EXERCISE_TYPE_BIKING,
-                segments = listOf(
-                    ExerciseSegment(
-                        startTime = Instant.ofEpochMilli(1233L),
-                        endTime = Instant.ofEpochMilli(1235L),
-                        segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
-                    )
-                )
+                segments =
+                    listOf(
+                        ExerciseSegment(
+                            startTime = Instant.ofEpochMilli(1233L),
+                            endTime = Instant.ofEpochMilli(1235L),
+                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                        )
+                    ),
+                exerciseRouteData = null,
             )
         }
 
@@ -183,13 +192,15 @@
                 endTime = Instant.ofEpochMilli(1235L),
                 endZoneOffset = null,
                 exerciseType = EXERCISE_TYPE_BIKING,
-                segments = listOf(
-                    ExerciseSegment(
-                        startTime = Instant.ofEpochMilli(1234L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                        segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
-                    )
-                )
+                segments =
+                    listOf(
+                        ExerciseSegment(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                        )
+                    ),
+                exerciseRouteData = null,
             )
         }
     }
@@ -203,12 +214,14 @@
                 endTime = Instant.ofEpochMilli(1235L),
                 endZoneOffset = null,
                 exerciseType = EXERCISE_TYPE_BIKING,
-                laps = listOf(
-                    ExerciseLap(
-                        startTime = Instant.ofEpochMilli(1233L),
-                        endTime = Instant.ofEpochMilli(1235L),
-                    )
-                )
+                laps =
+                    listOf(
+                        ExerciseLap(
+                            startTime = Instant.ofEpochMilli(1233L),
+                            endTime = Instant.ofEpochMilli(1235L),
+                        )
+                    ),
+                exerciseRouteData = null,
             )
         }
 
@@ -219,12 +232,14 @@
                 endTime = Instant.ofEpochMilli(1235L),
                 endZoneOffset = null,
                 exerciseType = EXERCISE_TYPE_BIKING,
-                laps = listOf(
-                    ExerciseLap(
-                        startTime = Instant.ofEpochMilli(1234L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                    )
-                )
+                laps =
+                    listOf(
+                        ExerciseLap(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                        )
+                    ),
+                exerciseRouteData = null,
             )
         }
     }
@@ -238,15 +253,17 @@
                 endTime = Instant.ofEpochMilli(1235L),
                 endZoneOffset = null,
                 exerciseType = EXERCISE_TYPE_BIKING,
-                route = ExerciseRoute(
-                    route = listOf(
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(1233L),
-                            latitude = 34.5,
-                            longitude = -34.5
-                        )
+                exerciseRoute =
+                    ExerciseRoute.Data(
+                        route =
+                            listOf(
+                                ExerciseRoute.Location(
+                                    time = Instant.ofEpochMilli(1233L),
+                                    latitude = 34.5,
+                                    longitude = -34.5
+                                )
+                            )
                     )
-                )
             )
         }
 
@@ -257,15 +274,17 @@
                 endTime = Instant.ofEpochMilli(1235L),
                 endZoneOffset = null,
                 exerciseType = EXERCISE_TYPE_BIKING,
-                route = ExerciseRoute(
-                    route = listOf(
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(1235L),
-                            latitude = 34.5,
-                            longitude = -34.5
-                        )
+                exerciseRoute =
+                    ExerciseRoute.Data(
+                        route =
+                            listOf(
+                                ExerciseRoute.Location(
+                                    time = Instant.ofEpochMilli(1235L),
+                                    latitude = 34.5,
+                                    longitude = -34.5
+                                )
+                            )
                     )
-                )
             )
         }
     }
@@ -279,18 +298,20 @@
                 endTime = Instant.ofEpochMilli(1236L),
                 endZoneOffset = null,
                 exerciseType = EXERCISE_TYPE_BIKING,
-                segments = listOf(
-                    ExerciseSegment(
-                        startTime = Instant.ofEpochMilli(1234L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                        segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                segments =
+                    listOf(
+                        ExerciseSegment(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                        ),
+                        ExerciseSegment(
+                            startTime = Instant.ofEpochMilli(1235L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
+                        ),
                     ),
-                    ExerciseSegment(
-                        startTime = Instant.ofEpochMilli(1235L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                        segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING
-                    ),
-                )
+                exerciseRouteData = null,
             )
         }
     }
@@ -304,16 +325,18 @@
                 endTime = Instant.ofEpochMilli(1236L),
                 endZoneOffset = null,
                 exerciseType = EXERCISE_TYPE_BIKING,
-                laps = listOf(
-                    ExerciseLap(
-                        startTime = Instant.ofEpochMilli(1234L),
-                        endTime = Instant.ofEpochMilli(1236L),
+                laps =
+                    listOf(
+                        ExerciseLap(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                        ),
+                        ExerciseLap(
+                            startTime = Instant.ofEpochMilli(1235L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                        ),
                     ),
-                    ExerciseLap(
-                        startTime = Instant.ofEpochMilli(1235L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                    ),
-                )
+                exerciseRouteData = null,
             )
         }
     }
@@ -327,36 +350,15 @@
                 endTime = Instant.ofEpochMilli(1236L),
                 endZoneOffset = null,
                 exerciseType = EXERCISE_TYPE_BIKING,
-                segments = listOf(
-                    ExerciseSegment(
-                        startTime = Instant.ofEpochMilli(1234L),
-                        endTime = Instant.ofEpochMilli(1236L),
-                        segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_PLANK
+                segments =
+                    listOf(
+                        ExerciseSegment(
+                            startTime = Instant.ofEpochMilli(1234L),
+                            endTime = Instant.ofEpochMilli(1236L),
+                            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_PLANK
+                        ),
                     ),
-                )
-            )
-        }
-    }
-
-    @Test
-    fun exerciseRouteSet_hasRouteSetToFalse_throws() {
-        assertFailsWith<IllegalArgumentException> {
-            ExerciseSessionRecord(
-                startTime = Instant.ofEpochMilli(1234L),
-                startZoneOffset = null,
-                endTime = Instant.ofEpochMilli(1236L),
-                endZoneOffset = null,
-                exerciseType = EXERCISE_TYPE_BIKING,
-                route = ExerciseRoute(
-                    route = listOf(
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(1235L),
-                            latitude = 34.5,
-                            longitude = -34.5
-                        )
-                    )
-                ),
-                hasRoute = false,
+                exerciseRouteData = null,
             )
         }
     }
@@ -364,31 +366,49 @@
     @Test
     fun secondConstructor_hasRouteSetCorrectly() {
         assertThat(
-            ExerciseSessionRecord(
-                startTime = Instant.ofEpochMilli(1234L),
-                startZoneOffset = null,
-                endTime = Instant.ofEpochMilli(1236L),
-                endZoneOffset = null,
-                exerciseType = EXERCISE_TYPE_BIKING,
-                route = ExerciseRoute(
-                    route = listOf(
-                        ExerciseRoute.Location(
-                            time = Instant.ofEpochMilli(1235L),
-                            latitude = 34.5,
-                            longitude = -34.5
-                        )
+                ExerciseSessionRecord(
+                        startTime = Instant.ofEpochMilli(1234L),
+                        startZoneOffset = null,
+                        endTime = Instant.ofEpochMilli(1236L),
+                        endZoneOffset = null,
+                        exerciseType = EXERCISE_TYPE_BIKING,
+                        exerciseRoute =
+                            ExerciseRoute.Data(
+                                route =
+                                    listOf(
+                                        ExerciseRoute.Location(
+                                            time = Instant.ofEpochMilli(1235L),
+                                            latitude = 34.5,
+                                            longitude = -34.5
+                                        )
+                                    )
+                            ),
                     )
-                ),
-            ).hasRoute
-        ).isTrue()
+                    .exerciseRoute
+            )
+            .isEqualTo(
+                ExerciseRoute.Data(
+                    route =
+                        listOf(
+                            ExerciseRoute.Location(
+                                time = Instant.ofEpochMilli(1235L),
+                                latitude = 34.5,
+                                longitude = -34.5
+                            )
+                        )
+                )
+            )
         assertThat(
-            ExerciseSessionRecord(
-                startTime = Instant.ofEpochMilli(1234L),
-                startZoneOffset = null,
-                endTime = Instant.ofEpochMilli(1236L),
-                endZoneOffset = null,
-                exerciseType = EXERCISE_TYPE_BIKING,
-            ).hasRoute
-        ).isFalse()
+                ExerciseSessionRecord(
+                        startTime = Instant.ofEpochMilli(1234L),
+                        startZoneOffset = null,
+                        endTime = Instant.ofEpochMilli(1236L),
+                        endZoneOffset = null,
+                        exerciseType = EXERCISE_TYPE_BIKING,
+                        exerciseRouteData = null
+                    )
+                    .exerciseRoute
+            )
+            .isEqualTo(ExerciseRoute.NoData())
     }
 }