Merge "Add aggregation fallback and nutrition transfat total aggregation." into androidx-main
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
index f25c14e..926ed7a 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -64,8 +64,7 @@
 @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 @MediumTest
 @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-// Comment the SDK suppress to run on emulators lower than U.
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
 class HealthConnectClientUpsideDownImplTest {
 
     private companion object {
@@ -88,7 +87,6 @@
             .filter { it.startsWith(PERMISSION_PREFIX) }
             .toTypedArray()
 
-    // Grant every permission as deletion by id checks for every permission
     @get:Rule
     val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(*allHealthPermissions)
 
@@ -167,10 +165,10 @@
         )
 
         assertThat(
-                healthConnectClient
-                    .readRecords(ReadRecordsRequest(StepsRecord::class, TimeRangeFilter.none()))
-                    .records
-            )
+            healthConnectClient
+                .readRecords(ReadRecordsRequest(StepsRecord::class, TimeRangeFilter.none()))
+                .records
+        )
             .containsExactly(initialRecords[0])
     }
 
@@ -208,10 +206,10 @@
         )
 
         assertThat(
-                healthConnectClient
-                    .readRecords(ReadRecordsRequest(StepsRecord::class, TimeRangeFilter.none()))
-                    .records
-            )
+            healthConnectClient
+                .readRecords(ReadRecordsRequest(StepsRecord::class, TimeRangeFilter.none()))
+                .records
+        )
             .containsExactly(initialRecords[1])
     }
 
@@ -336,10 +334,10 @@
                     endTime = START_TIME + 30.seconds,
                     endZoneOffset = ZoneOffset.UTC,
                     samples =
-                        listOf(
-                            HeartRateRecord.Sample(START_TIME, 57L),
-                            HeartRateRecord.Sample(START_TIME + 15.seconds, 120L)
-                        )
+                    listOf(
+                        HeartRateRecord.Sample(START_TIME, 57L),
+                        HeartRateRecord.Sample(START_TIME + 15.seconds, 120L)
+                    )
                 ),
                 HeartRateRecord(
                     startTime = START_TIME + 1.minutes,
@@ -347,10 +345,10 @@
                     endTime = START_TIME + 1.minutes + 30.seconds,
                     endZoneOffset = ZoneOffset.UTC,
                     samples =
-                        listOf(
-                            HeartRateRecord.Sample(START_TIME + 1.minutes, 47L),
-                            HeartRateRecord.Sample(START_TIME + 1.minutes + 15.seconds, 48L)
-                        )
+                    listOf(
+                        HeartRateRecord.Sample(START_TIME + 1.minutes, 47L),
+                        HeartRateRecord.Sample(START_TIME + 1.minutes + 15.seconds, 48L)
+                    )
                 ),
                 NutritionRecord(
                     startTime = START_TIME,
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
new file mode 100644
index 0000000..75435a1
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
@@ -0,0 +1,548 @@
+/*
+ * Copyright 2024 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.impl.platform.aggregate
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.impl.HealthConnectClientUpsideDownImpl
+import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.time.TimeRangeFilter
+import androidx.health.connect.client.units.Mass
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.GrantPermissionRule
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+import kotlinx.coroutines.flow.fold
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@MediumTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class HealthConnectClientAggregationExtensionsTest {
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val healthConnectClient: HealthConnectClient =
+        HealthConnectClientUpsideDownImpl(context)
+
+    private companion object {
+        private val START_TIME =
+            LocalDate.now().minusDays(5).atStartOfDay().toInstant(ZoneOffset.UTC)
+    }
+
+    @get:Rule
+    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        HealthPermission.getWritePermission(NutritionRecord::class),
+        HealthPermission.getReadPermission(NutritionRecord::class),
+        HealthPermission.getWritePermission(StepsRecord::class),
+        HealthPermission.getReadPermission(StepsRecord::class)
+    )
+
+    @After
+    fun tearDown() = runTest {
+        healthConnectClient.deleteRecords(NutritionRecord::class, TimeRangeFilter.none())
+        healthConnectClient.deleteRecords(StepsRecord::class, TimeRangeFilter.none())
+    }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_noFilters() = runTest {
+        healthConnectClient.insertRecords(
+            listOf(
+                NutritionRecord(
+                    startTime = START_TIME,
+                    endTime = START_TIME + 1.minutes,
+                    transFat = Mass.grams(0.3),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 2.minutes,
+                    endTime = START_TIME + 3.minutes,
+                    transFat = null,
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 4.minutes,
+                    endTime = START_TIME + 5.minutes,
+                    transFat = Mass.grams(0.4),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 6.minutes,
+                    endTime = START_TIME + 7.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 8.minutes,
+                    endTime = START_TIME + 9.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                )
+            )
+        )
+
+        val aggregationResult =
+            healthConnectClient.aggregateNutritionTransFatTotal(TimeRangeFilter.none(), emptySet())
+
+        assertThat(aggregationResult[NutritionRecord.TRANS_FAT_TOTAL]).isEqualTo(Mass.grams(1.7))
+        assertThat(aggregationResult.dataOrigins).containsExactly(DataOrigin(context.packageName))
+    }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_instantTimeRangeFilter() = runTest {
+        healthConnectClient.insertRecords(
+            listOf(
+                NutritionRecord(
+                    startTime = START_TIME,
+                    endTime = START_TIME + 1.minutes,
+                    transFat = Mass.grams(0.3),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 2.minutes,
+                    endTime = START_TIME + 3.minutes,
+                    transFat = null,
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 4.minutes,
+                    endTime = START_TIME + 5.minutes,
+                    transFat = Mass.grams(0.4),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 6.minutes,
+                    endTime = START_TIME + 7.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 8.minutes,
+                    endTime = START_TIME + 9.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                )
+            )
+        )
+
+        val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+            TimeRangeFilter.between(
+                START_TIME + 30.seconds,
+                START_TIME + 6.minutes + 45.seconds
+            ), emptySet()
+        )
+
+        assertThat(aggregationResult[NutritionRecord.TRANS_FAT_TOTAL])
+            .isEqualTo(Mass.grams(0.15 + 0.4 + 0.375))
+        assertThat(aggregationResult.dataOrigins).containsExactly(DataOrigin(context.packageName))
+    }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_instantTimeRangeFilter_filterStartTimeRecordEndTime() =
+        runTest {
+            healthConnectClient.insertRecords(
+                listOf(
+                    NutritionRecord(
+                        startTime = START_TIME,
+                        endTime = START_TIME + 1.minutes,
+                        transFat = Mass.grams(0.3),
+                        startZoneOffset = ZoneOffset.UTC,
+                        endZoneOffset = ZoneOffset.UTC
+                    ),
+                    NutritionRecord(
+                        startTime = START_TIME + 2.minutes,
+                        endTime = START_TIME + 3.minutes,
+                        transFat = Mass.grams(0.4),
+                        startZoneOffset = ZoneOffset.UTC,
+                        endZoneOffset = ZoneOffset.UTC
+                    )
+                )
+            )
+
+            val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+                TimeRangeFilter.between(
+                    START_TIME + 1.minutes,
+                    START_TIME + 2.minutes
+                ), emptySet()
+            )
+
+            assertThat(NutritionRecord.TRANS_FAT_TOTAL in aggregationResult).isFalse()
+            assertThat(aggregationResult.dataOrigins).isEmpty()
+        }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_instantTimeRangeFilter_filterStartTimeRecordStartTime() =
+        runTest {
+            healthConnectClient.insertRecords(
+                listOf(
+                    NutritionRecord(
+                        startTime = START_TIME,
+                        endTime = START_TIME + 1.minutes,
+                        transFat = Mass.grams(0.3),
+                        startZoneOffset = ZoneOffset.UTC,
+                        endZoneOffset = ZoneOffset.UTC
+                    ),
+                    NutritionRecord(
+                        startTime = START_TIME + 2.minutes,
+                        endTime = START_TIME + 3.minutes,
+                        transFat = Mass.grams(0.4),
+                        startZoneOffset = ZoneOffset.UTC,
+                        endZoneOffset = ZoneOffset.UTC
+                    )
+                )
+            )
+
+            val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+                TimeRangeFilter.between(
+                    START_TIME,
+                    START_TIME + 2.minutes
+                ), emptySet()
+            )
+
+            assertThat(aggregationResult[NutritionRecord.TRANS_FAT_TOTAL])
+                .isEqualTo(Mass.grams(0.3))
+            assertThat(aggregationResult.dataOrigins)
+                .containsExactly(DataOrigin(context.packageName))
+        }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_instantTimeRangeFilter_recordRangeLargerThanQuery() =
+        runTest {
+            healthConnectClient.insertRecords(
+                listOf(
+                    NutritionRecord(
+                        startTime = START_TIME,
+                        endTime = START_TIME + 1.minutes,
+                        transFat = Mass.grams(0.5),
+                        startZoneOffset = ZoneOffset.UTC,
+                        endZoneOffset = ZoneOffset.UTC
+                    ),
+                )
+            )
+
+            val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+                TimeRangeFilter.between(
+                    START_TIME + 15.seconds,
+                    START_TIME + 45.seconds
+                ), emptySet()
+            )
+
+            assertThat(aggregationResult[NutritionRecord.TRANS_FAT_TOTAL])
+                .isEqualTo(Mass.grams(0.25))
+            assertThat(aggregationResult.dataOrigins)
+                .containsExactly(DataOrigin(context.packageName))
+        }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_localTimeRangeFilter() = runTest {
+        healthConnectClient.insertRecords(
+            listOf(
+                NutritionRecord(
+                    startTime = START_TIME,
+                    endTime = START_TIME + 1.minutes,
+                    transFat = Mass.grams(0.3),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 2.minutes,
+                    endTime = START_TIME + 3.minutes,
+                    transFat = null,
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME - 2.hours + 4.minutes,
+                    endTime = START_TIME + 5.minutes,
+                    transFat = Mass.grams(0.4),
+                    startZoneOffset = ZoneOffset.ofHours(2),
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                NutritionRecord(
+                    startTime = START_TIME + 3.hours + 6.minutes,
+                    endTime = START_TIME + 3.hours + 7.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.ofHours(-3),
+                    endZoneOffset = ZoneOffset.ofHours(-3)
+                ),
+                NutritionRecord(
+                    startTime = START_TIME - 4.hours + 8.minutes,
+                    endTime = START_TIME - 4.hours + 9.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.ofHours(4),
+                    endZoneOffset = ZoneOffset.ofHours(4)
+                )
+            )
+        )
+
+        val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+            TimeRangeFilter.between(
+                LocalDateTime.ofInstant(START_TIME + 30.seconds, ZoneOffset.UTC),
+                LocalDateTime.ofInstant(START_TIME + 6.minutes + 45.seconds, ZoneOffset.UTC)
+            ), emptySet()
+        )
+
+        assertThat(aggregationResult[NutritionRecord.TRANS_FAT_TOTAL])
+            .isEqualTo(Mass.grams(0.15 + 0.4 + 0.375))
+        assertThat(aggregationResult.dataOrigins).containsExactly(DataOrigin(context.packageName))
+    }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_localTimeRangeFilter_recordRangeLargerThanQuery() =
+        runTest {
+            healthConnectClient.insertRecords(
+                listOf(
+                    NutritionRecord(
+                        startTime = START_TIME,
+                        endTime = START_TIME + 1.minutes,
+                        transFat = Mass.grams(0.5),
+                        startZoneOffset = ZoneOffset.UTC,
+                        endZoneOffset = ZoneOffset.UTC
+                    ),
+                )
+            )
+
+            val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+                TimeRangeFilter.between(
+                    LocalDateTime.ofInstant(
+                        START_TIME - 2.hours + 15.seconds,
+                        ZoneOffset.ofHours(2)
+                    ),
+                    LocalDateTime.ofInstant(
+                        START_TIME - 2.hours + 45.seconds,
+                        ZoneOffset.ofHours(2)
+                    )
+                ), emptySet()
+            )
+
+            assertThat(aggregationResult[NutritionRecord.TRANS_FAT_TOTAL])
+                .isEqualTo(Mass.grams(0.25))
+            assertThat(aggregationResult.dataOrigins)
+                .containsExactly(DataOrigin(context.packageName))
+        }
+
+    // TODO(b/337195270): Test with data origins from multiple apps
+    @Test
+    fun aggregateNutritionTransFatTotal_insertedDataOriginFilter() = runTest {
+        healthConnectClient.insertRecords(
+            listOf(
+                NutritionRecord(
+                    startTime = START_TIME,
+                    endTime = START_TIME + 1.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+            )
+        )
+
+        val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+            TimeRangeFilter.none(),
+            setOf(DataOrigin(context.packageName))
+        )
+
+        assertThat(aggregationResult[NutritionRecord.TRANS_FAT_TOTAL])
+            .isEqualTo(Mass.grams(0.5))
+        assertThat(aggregationResult.dataOrigins).containsExactly(DataOrigin(context.packageName))
+    }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_timeRangeFilterOutOfBounds() = runTest {
+        healthConnectClient.insertRecords(
+            listOf(
+                NutritionRecord(
+                    startTime = START_TIME,
+                    endTime = START_TIME + 1.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+            )
+        )
+
+        val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+            TimeRangeFilter.after(START_TIME + 2.minutes),
+            emptySet()
+        )
+
+        assertThat(NutritionRecord.TRANS_FAT_TOTAL in aggregationResult).isFalse()
+        assertThat(aggregationResult.dataOrigins).isEmpty()
+    }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_recordStartTimeWithNegativeZoneOffset() = runTest {
+        healthConnectClient.insertRecords(
+            listOf(
+                NutritionRecord(
+                    startTime = START_TIME,
+                    endTime = START_TIME + 60.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.ofHours(-2),
+                    endZoneOffset = ZoneOffset.UTC
+                )
+            )
+        )
+
+        val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+            TimeRangeFilter.between(
+                LocalDateTime.ofInstant(START_TIME, ZoneOffset.UTC),
+                LocalDateTime.ofInstant(START_TIME + 60.minutes, ZoneOffset.UTC)
+            ), emptySet()
+        )
+
+        assertThat(NutritionRecord.TRANS_FAT_TOTAL in aggregationResult).isFalse()
+        assertThat(aggregationResult.dataOrigins).isEmpty()
+    }
+
+    @Test
+    fun aggregateNutritionTransFatTotal_nonExistingDataOriginFilter() = runTest {
+        healthConnectClient.insertRecords(
+            listOf(
+                NutritionRecord(
+                    startTime = START_TIME,
+                    endTime = START_TIME + 1.minutes,
+                    transFat = Mass.grams(0.5),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+            )
+        )
+
+        val aggregationResult = healthConnectClient.aggregateNutritionTransFatTotal(
+            TimeRangeFilter.none(),
+            setOf(DataOrigin("some random package name"))
+        )
+
+        assertThat(NutritionRecord.TRANS_FAT_TOTAL in aggregationResult).isFalse()
+        assertThat(aggregationResult.dataOrigins).isEmpty()
+    }
+
+    @Test
+    fun readRecordsFlow_noFilters_readsAllInsertedRecords() = runTest {
+        insertManyStepsRecords()
+
+        val count = healthConnectClient.readRecordsFlow(
+            StepsRecord::class,
+            TimeRangeFilter.none(),
+            emptySet()
+        ).fold(0) { currentCount, records ->
+            currentCount + records.size
+        }
+
+        assertThat(count).isEqualTo(10_000L)
+    }
+
+    @Test
+    fun readRecordsFlow_timeRangeFilter_readsFilteredRecords() = runTest {
+        assumeTrue(SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10)
+        insertManyStepsRecords()
+
+        val count = healthConnectClient.readRecordsFlow(
+            StepsRecord::class,
+            TimeRangeFilter.between(START_TIME + 10_000.seconds, START_TIME + 90_000.seconds),
+            emptySet()
+        ).fold(0) { currentCount, records ->
+            currentCount + records.size
+        }
+
+        assertThat(count).isEqualTo(8_000L)
+    }
+
+    // TODO(b/337195270): Test with data origins from multiple apps
+    @Test
+    fun readRecordsFlow_insertedDataOriginFilter_readsAllInsertedRecords() = runTest {
+        insertManyStepsRecords()
+
+        val count = healthConnectClient.readRecordsFlow(
+            StepsRecord::class,
+            TimeRangeFilter.none(),
+            setOf(DataOrigin(context.packageName))
+        ).fold(0) { currentCount, records ->
+            currentCount + records.size
+        }
+
+        assertThat(count).isEqualTo(10_000L)
+    }
+
+    @Test
+    fun readRecordsFlow_nonExistingDataOriginFilter_doesNotReadAnyRecord() = runTest {
+        insertManyStepsRecords()
+
+        val count = healthConnectClient.readRecordsFlow(
+            StepsRecord::class,
+            TimeRangeFilter.none(),
+            setOf(DataOrigin("some random package name"))
+        ).fold(0) { currentCount, records ->
+            currentCount + records.size
+        }
+
+        assertThat(count).isEqualTo(0L)
+    }
+
+    private suspend fun insertManyStepsRecords() {
+        // Insert a large number of step records, bigger than the default page size
+        for (i in 0..9) {
+            healthConnectClient.insertRecords(List(1000) {
+                val startTime = START_TIME + (i * 10_000 + it * 10).seconds
+                StepsRecord(
+                    startTime = startTime,
+                    endTime = startTime + 5.seconds,
+                    count = 10L,
+                    startZoneOffset = ZoneOffset.UTC,
+                    endZoneOffset = ZoneOffset.UTC
+                )
+            })
+        }
+    }
+
+    private val Int.seconds: Duration
+        get() = Duration.ofSeconds(this.toLong())
+
+    private val Int.minutes: Duration
+        get() = Duration.ofMinutes(this.toLong())
+
+    private val Int.hours: Duration
+        get() = Duration.ofHours(this.toLong())
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt
index d2edf4b..859cbbc 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt
@@ -25,6 +25,9 @@
 import android.health.connect.datatypes.StepsRecord as PlatformStepsRecord
 import android.health.connect.datatypes.WheelchairPushesRecord as PlatformWheelchairPushesRecord
 import android.os.Build
+import androidx.health.connect.client.impl.platform.request.toAggregationType
+import androidx.health.connect.client.impl.platform.request.toPlatformRequest
+import androidx.health.connect.client.impl.platform.request.toPlatformTimeRangeFilter
 import androidx.health.connect.client.records.HeartRateRecord
 import androidx.health.connect.client.records.NutritionRecord
 import androidx.health.connect.client.records.StepsRecord
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/ResponseConvertersTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/ResponseConvertersTest.kt
index fffd43a..b7bbc3d 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/ResponseConvertersTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/ResponseConvertersTest.kt
@@ -23,8 +23,13 @@
 import android.health.connect.datatypes.units.Power as PlatformPower
 import android.health.connect.datatypes.units.Volume as PlatformVolume
 import android.os.Build
+import android.os.ext.SdkExtensions
 import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.impl.platform.response.buildAggregationResult
+import androidx.health.connect.client.impl.platform.response.getDoubleMetricValues
+import androidx.health.connect.client.impl.platform.response.getLongMetricValues
 import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.BloodPressureRecord
 import androidx.health.connect.client.records.DistanceRecord
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.FloorsClimbedRecord
@@ -32,6 +37,8 @@
 import androidx.health.connect.client.records.HydrationRecord
 import androidx.health.connect.client.records.NutritionRecord
 import androidx.health.connect.client.records.PowerRecord
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.WeightRecord
 import androidx.health.connect.client.records.metadata.DataOrigin
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
@@ -39,6 +46,7 @@
 import com.google.common.truth.Correspondence
 import com.google.common.truth.Truth.assertThat
 import java.time.Duration
+import org.junit.Assume.assumeTrue
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -56,8 +64,10 @@
     fun buildAggregationResult() {
         val aggregationResult =
             buildAggregationResult(
-                metrics =
-                    setOf(HeartRateRecord.BPM_MIN, ExerciseSessionRecord.EXERCISE_DURATION_TOTAL),
+                metrics = setOf(
+                    HeartRateRecord.BPM_MIN,
+                    ExerciseSessionRecord.EXERCISE_DURATION_TOTAL
+                ),
                 aggregationValueGetter = { aggregationType ->
                     when (aggregationType) {
                         PlatformHeartRateRecord.BPM_MIN -> 53L
@@ -72,8 +82,10 @@
                                 PlatformDataOriginBuilder().setPackageName("HR App1").build(),
                                 PlatformDataOriginBuilder().setPackageName("HR App2").build()
                             )
+
                         PlatformExerciseSessionRecord.EXERCISE_DURATION_TOTAL ->
                             setOf(PlatformDataOriginBuilder().setPackageName("Workout app").build())
+
                         else -> emptySet()
                     }
                 }
@@ -159,6 +171,17 @@
     }
 
     @Test
+    fun getDoubleMetricValue_convertsMassToKilograms() {
+        val metricValues = getDoubleMetricValues(
+            mapOf(
+                WeightRecord.WEIGHT_MAX as AggregateMetric<Any> to PlatformMass.fromGrams(100_000.0)
+            )
+        )
+
+        assertThat(metricValues).containsExactly(WeightRecord.WEIGHT_MAX.metricKey, 100.0)
+    }
+
+    @Test
     fun getDoubleMetricValues_convertsPowerToWatts() {
         val metricValues =
             getDoubleMetricValues(
@@ -170,6 +193,34 @@
     }
 
     @Test
+    fun getDoubleMetricValues_convertsPressureToMillimetersOfMercury() {
+        assumeTrue(SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10)
+        val metricValues = getDoubleMetricValues(
+            mapOf(
+                BloodPressureRecord.SYSTOLIC_MAX as AggregateMetric<Any> to
+                    PlatformPressure.fromMillimetersOfMercury(
+                        120.0
+                    )
+            )
+        )
+
+        assertThat(metricValues).containsExactly(BloodPressureRecord.SYSTOLIC_MAX.metricKey, 120.0)
+    }
+
+    @Test
+    fun getDoubleMetricValues_convertsVelocityToMetersPerSecond() {
+        assumeTrue(SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10)
+        val metricValues = getDoubleMetricValues(
+            mapOf(
+                SpeedRecord.SPEED_AVG as AggregateMetric<Any> to
+                    PlatformVelocity.fromMetersPerSecond(2.8)
+            )
+        )
+
+        assertThat(metricValues).containsExactly(SpeedRecord.SPEED_AVG.metricKey, 2.8)
+    }
+
+    @Test
     fun getDoubleMetricValues_convertsVolumeToLiters() {
         val metricValues =
             getDoubleMetricValues(
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
index eac2033..5b6d17e 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
@@ -38,14 +38,16 @@
 import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod
 import androidx.health.connect.client.changes.DeletionChange
 import androidx.health.connect.client.changes.UpsertionChange
-import androidx.health.connect.client.impl.platform.records.toPlatformLocalTimeRangeFilter
+import androidx.health.connect.client.impl.platform.aggregate.aggregateFallback
+import androidx.health.connect.client.impl.platform.aggregate.plus
 import androidx.health.connect.client.impl.platform.records.toPlatformRecord
 import androidx.health.connect.client.impl.platform.records.toPlatformRecordClass
-import androidx.health.connect.client.impl.platform.records.toPlatformRequest
-import androidx.health.connect.client.impl.platform.records.toPlatformTimeRangeFilter
 import androidx.health.connect.client.impl.platform.records.toSdkRecord
-import androidx.health.connect.client.impl.platform.records.toSdkResponse
+import androidx.health.connect.client.impl.platform.request.toPlatformLocalTimeRangeFilter
+import androidx.health.connect.client.impl.platform.request.toPlatformRequest
+import androidx.health.connect.client.impl.platform.request.toPlatformTimeRangeFilter
 import androidx.health.connect.client.impl.platform.response.toKtResponse
+import androidx.health.connect.client.impl.platform.response.toSdkResponse
 import androidx.health.connect.client.impl.platform.toKtException
 import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_PREFIX
 import androidx.health.connect.client.records.Record
@@ -201,31 +203,33 @@
     }
 
     override suspend fun aggregate(request: AggregateRequest): AggregationResult {
-        return wrapPlatformException {
-                suspendCancellableCoroutine { continuation ->
-                    healthConnectManager.aggregate(
-                        request.toPlatformRequest(),
-                        executor,
-                        continuation.asOutcomeReceiver()
-                    )
-                }
+        val platformResponse = wrapPlatformException {
+            suspendCancellableCoroutine { continuation ->
+                healthConnectManager.aggregate(
+                    request.toPlatformRequest(),
+                    executor,
+                    continuation.asOutcomeReceiver()
+                )
             }
+        }
             .toSdkResponse(request.metrics)
+        val fallbackResponse = aggregateFallback(request)
+        return platformResponse + fallbackResponse
     }
 
     override suspend fun aggregateGroupByDuration(
         request: AggregateGroupByDurationRequest
     ): List<AggregationResultGroupedByDuration> {
         return wrapPlatformException {
-                suspendCancellableCoroutine { continuation ->
-                    healthConnectManager.aggregateGroupByDuration(
-                        request.toPlatformRequest(),
-                        request.timeRangeSlicer,
-                        executor,
-                        continuation.asOutcomeReceiver()
-                    )
-                }
+            suspendCancellableCoroutine { continuation ->
+                healthConnectManager.aggregateGroupByDuration(
+                    request.toPlatformRequest(),
+                    request.timeRangeSlicer,
+                    executor,
+                    continuation.asOutcomeReceiver()
+                )
             }
+        }
             .map { it.toSdkResponse(request.metrics) }
     }
 
@@ -233,19 +237,19 @@
         request: AggregateGroupByPeriodRequest
     ): List<AggregationResultGroupedByPeriod> {
         return wrapPlatformException {
-                suspendCancellableCoroutine { continuation ->
-                    healthConnectManager.aggregateGroupByPeriod(
-                        request.toPlatformRequest(),
-                        request.timeRangeSlicer,
-                        executor,
-                        continuation.asOutcomeReceiver()
-                    )
-                }
+            suspendCancellableCoroutine { continuation ->
+                healthConnectManager.aggregateGroupByPeriod(
+                    request.toPlatformRequest(),
+                    request.timeRangeSlicer,
+                    executor,
+                    continuation.asOutcomeReceiver()
+                )
             }
+        }
             .mapIndexed { index, platformResponse ->
                 if (
                     SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10 ||
-                        (request.timeRangeSlicer.months == 0 && request.timeRangeSlicer.years == 0)
+                    (request.timeRangeSlicer.months == 0 && request.timeRangeSlicer.years == 0)
                 ) {
                     platformResponse.toSdkResponse(request.metrics)
                 } else {
@@ -261,11 +265,11 @@
                         metrics = request.metrics,
                         bucketStartTime = bucketStartTime,
                         bucketEndTime =
-                            if (requestTimeRangeFilter.endTime!!.isBefore(bucketEndTime)) {
-                                requestTimeRangeFilter.endTime!!
-                            } else {
-                                bucketEndTime
-                            }
+                        if (requestTimeRangeFilter.endTime!!.isBefore(bucketEndTime)) {
+                            requestTimeRangeFilter.endTime!!
+                        } else {
+                            bucketEndTime
+                        }
                     )
                 }
             }
@@ -273,14 +277,14 @@
 
     override suspend fun getChangesToken(request: ChangesTokenRequest): String {
         return wrapPlatformException {
-                suspendCancellableCoroutine { continuation ->
-                    healthConnectManager.getChangeLogToken(
-                        request.toPlatformRequest(),
-                        executor,
-                        continuation.asOutcomeReceiver()
-                    )
-                }
+            suspendCancellableCoroutine { continuation ->
+                healthConnectManager.getChangeLogToken(
+                    request.toPlatformRequest(),
+                    executor,
+                    continuation.asOutcomeReceiver()
+                )
             }
+        }
             .token
     }
 
@@ -324,7 +328,7 @@
                     for (i in it.requestedPermissions.indices) {
                         if (
                             it.requestedPermissions[i].startsWith(PERMISSION_PREFIX) &&
-                                it.requestedPermissionsFlags[i] and REQUESTED_PERMISSION_GRANTED > 0
+                            it.requestedPermissionsFlags[i] and REQUESTED_PERMISSION_GRANTED > 0
                         ) {
                             add(it.requestedPermissions[i])
                         }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/TimeExtensions.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/TimeExtensions.kt
new file mode 100644
index 0000000..a8705ff
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/TimeExtensions.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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.impl.platform
+
+import androidx.health.connect.client.records.IntervalRecord
+import androidx.health.connect.client.time.TimeRangeFilter
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.ZoneOffset
+
+internal operator fun Duration.div(divisor: Duration): Double {
+    if (divisor.isZero) {
+        return 0.0
+    }
+    return toMillis().toDouble() / divisor.toMillis()
+}
+
+internal operator fun Instant.minus(other: Instant): Duration {
+    return Duration.between(other, this)
+}
+
+internal fun TimeRangeFilter.useLocalTime(): Boolean {
+    return localStartTime != null || localEndTime != null
+}
+
+internal fun LocalDateTime.toInstantWithDefaultZoneFallback(zoneOffset: ZoneOffset?): Instant {
+    return atZone(zoneOffset ?: ZoneId.systemDefault()).toInstant()
+}
+
+internal val IntervalRecord.duration: Duration
+    get() = endTime - startTime
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationExtensions.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationExtensions.kt
new file mode 100644
index 0000000..f323864
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationExtensions.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 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.
+ */
+
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.aggregate
+
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresApi
+import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.aggregate.AggregationResult
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.StepsCadenceRecord
+import androidx.health.connect.client.request.AggregateRequest
+
+internal val AggregateRequest.platformMetrics: Set<AggregateMetric<*>>
+    get() {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) {
+            return metrics
+        }
+        return metrics.filterNot { it in SDK_EXT_10_AGGREGATE_METRICS }.toSet()
+    }
+
+internal val AggregateRequest.fallbackMetrics: Set<AggregateMetric<*>>
+    get() {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) {
+            return emptySet()
+        }
+        return metrics.filter { it in SDK_EXT_10_AGGREGATE_METRICS }.toSet()
+    }
+
+internal operator fun AggregationResult.plus(other: AggregationResult): AggregationResult {
+    return AggregationResult(
+        longValues + other.longValues,
+        doubleValues + other.doubleValues,
+        dataOrigins + other.dataOrigins
+    )
+}
+
+internal val SDK_EXT_10_AGGREGATE_METRICS: Set<AggregateMetric<*>> =
+    setOf(
+        BloodPressureRecord.DIASTOLIC_AVG,
+        BloodPressureRecord.DIASTOLIC_MAX,
+        BloodPressureRecord.DIASTOLIC_MIN,
+        BloodPressureRecord.SYSTOLIC_AVG,
+        BloodPressureRecord.SYSTOLIC_MAX,
+        BloodPressureRecord.SYSTOLIC_MIN,
+        CyclingPedalingCadenceRecord.RPM_AVG,
+        CyclingPedalingCadenceRecord.RPM_MAX,
+        CyclingPedalingCadenceRecord.RPM_MIN,
+        NutritionRecord.TRANS_FAT_TOTAL,
+        SpeedRecord.SPEED_AVG,
+        SpeedRecord.SPEED_MAX,
+        SpeedRecord.SPEED_MIN,
+        StepsCadenceRecord.RATE_AVG,
+        StepsCadenceRecord.RATE_MAX,
+        StepsCadenceRecord.RATE_MIN
+    )
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/AggregationMappings.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappings.kt
similarity index 74%
rename from health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/AggregationMappings.kt
rename to health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappings.kt
index 42d97d3..1d45df8 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/AggregationMappings.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappings.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 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.
@@ -17,7 +17,7 @@
 @file:RestrictTo(RestrictTo.Scope.LIBRARY)
 @file:RequiresApi(api = 34)
 
-package androidx.health.connect.client.impl.platform.records
+package androidx.health.connect.client.impl.platform.aggregate
 
 import android.health.connect.datatypes.ActiveCaloriesBurnedRecord as PlatformActiveCaloriesBurnedRecord
 import android.health.connect.datatypes.AggregationType as PlatformAggregateMetric
@@ -39,11 +39,24 @@
 import android.health.connect.datatypes.units.Mass as PlatformMass
 import android.health.connect.datatypes.units.Power as PlatformPower
 import android.health.connect.datatypes.units.Volume as PlatformVolume
+import android.os.Build
+import android.os.ext.SdkExtensions
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.impl.platform.records.PlatformBloodPressureRecord
+import androidx.health.connect.client.impl.platform.records.PlatformCyclingPedalingCadenceRecord
+import androidx.health.connect.client.impl.platform.records.PlatformExerciseSessionRecord
+import androidx.health.connect.client.impl.platform.records.PlatformPressure
+import androidx.health.connect.client.impl.platform.records.PlatformRestingHeartRateRecord
+import androidx.health.connect.client.impl.platform.records.PlatformSleepSessionRecord
+import androidx.health.connect.client.impl.platform.records.PlatformSpeedRecord
+import androidx.health.connect.client.impl.platform.records.PlatformStepsCadenceRecord
+import androidx.health.connect.client.impl.platform.records.PlatformVelocity
 import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
 import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.BloodPressureRecord
+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.ExerciseSessionRecord
@@ -55,6 +68,8 @@
 import androidx.health.connect.client.records.PowerRecord
 import androidx.health.connect.client.records.RestingHeartRateRecord
 import androidx.health.connect.client.records.SleepSessionRecord
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.StepsCadenceRecord
 import androidx.health.connect.client.records.StepsRecord
 import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
 import androidx.health.connect.client.records.WeightRecord
@@ -63,14 +78,31 @@
 import androidx.health.connect.client.units.Length
 import androidx.health.connect.client.units.Mass
 import androidx.health.connect.client.units.Power
+import androidx.health.connect.client.units.Pressure
+import androidx.health.connect.client.units.Velocity
 import androidx.health.connect.client.units.Volume
 import java.time.Duration
 
+private val DOUBLE_AGGREGATION_METRIC_TYPE_SDK_EXT_10_PAIRS =
+    if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) {
+        arrayOf(
+            CyclingPedalingCadenceRecord.RPM_AVG to PlatformCyclingPedalingCadenceRecord.RPM_AVG,
+            CyclingPedalingCadenceRecord.RPM_MAX to PlatformCyclingPedalingCadenceRecord.RPM_MAX,
+            CyclingPedalingCadenceRecord.RPM_MIN to PlatformCyclingPedalingCadenceRecord.RPM_MIN,
+            StepsCadenceRecord.RATE_AVG to PlatformStepsCadenceRecord.STEPS_CADENCE_RATE_AVG,
+            StepsCadenceRecord.RATE_MAX to PlatformStepsCadenceRecord.STEPS_CADENCE_RATE_MAX,
+            StepsCadenceRecord.RATE_MIN to PlatformStepsCadenceRecord.STEPS_CADENCE_RATE_MIN
+        )
+    } else {
+        emptyArray()
+    }
+
 internal val DOUBLE_AGGREGATION_METRIC_TYPE_MAP:
     Map<AggregateMetric<Double>, PlatformAggregateMetric<Double>> =
     mapOf(
         FloorsClimbedRecord.FLOORS_CLIMBED_TOTAL to
             PlatformFloorsClimbedRecord.FLOORS_CLIMBED_TOTAL,
+        *DOUBLE_AGGREGATION_METRIC_TYPE_SDK_EXT_10_PAIRS
     )
 
 internal val DURATION_AGGREGATION_METRIC_TYPE_MAP:
@@ -163,7 +195,14 @@
         NutritionRecord.VITAMIN_D_TOTAL to PlatformNutritionRecord.VITAMIN_D_TOTAL,
         NutritionRecord.VITAMIN_E_TOTAL to PlatformNutritionRecord.VITAMIN_E_TOTAL,
         NutritionRecord.VITAMIN_K_TOTAL to PlatformNutritionRecord.VITAMIN_K_TOTAL,
-        NutritionRecord.ZINC_TOTAL to PlatformNutritionRecord.ZINC_TOTAL
+        NutritionRecord.ZINC_TOTAL to PlatformNutritionRecord.ZINC_TOTAL,
+        *if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) {
+            arrayOf(
+                NutritionRecord.TRANS_FAT_TOTAL to PlatformNutritionRecord.TRANS_FAT_TOTAL
+            )
+        } else {
+            emptyArray()
+        }
     )
 
 internal val KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP:
@@ -182,6 +221,33 @@
         PowerRecord.POWER_MIN to PlatformPowerRecord.POWER_MIN,
     )
 
+internal val PRESSURE_AGGREGATION_METRIC_TYPE_MAP:
+    Map<AggregateMetric<Pressure>, PlatformAggregateMetric<PlatformPressure>> =
+    if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) {
+        arrayOf(
+            BloodPressureRecord.DIASTOLIC_AVG to PlatformBloodPressureRecord.DIASTOLIC_AVG,
+            BloodPressureRecord.DIASTOLIC_MAX to PlatformBloodPressureRecord.DIASTOLIC_MAX,
+            BloodPressureRecord.DIASTOLIC_MIN to PlatformBloodPressureRecord.DIASTOLIC_MIN,
+            BloodPressureRecord.SYSTOLIC_AVG to PlatformBloodPressureRecord.SYSTOLIC_AVG,
+            BloodPressureRecord.SYSTOLIC_MAX to PlatformBloodPressureRecord.SYSTOLIC_MAX,
+            BloodPressureRecord.SYSTOLIC_MIN to PlatformBloodPressureRecord.SYSTOLIC_MIN
+        )
+    } else {
+        emptyArray()
+    }.toMap()
+
+internal val VELOCITY_AGGREGATION_METRIC_TYPE_MAP:
+    Map<AggregateMetric<Velocity>, PlatformAggregateMetric<PlatformVelocity>> =
+    if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) {
+        arrayOf(
+            SpeedRecord.SPEED_AVG to PlatformSpeedRecord.SPEED_AVG,
+            SpeedRecord.SPEED_MAX to PlatformSpeedRecord.SPEED_MAX,
+            SpeedRecord.SPEED_MIN to PlatformSpeedRecord.SPEED_MIN
+        )
+    } else {
+        emptyArray()
+    }.toMap()
+
 internal val VOLUME_AGGREGATION_METRIC_TYPE_MAP:
     Map<AggregateMetric<Volume>, PlatformAggregateMetric<PlatformVolume>> =
     mapOf(
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensions.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensions.kt
new file mode 100644
index 0000000..e8ea7d7
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensions.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2024 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.
+ */
+
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.aggregate
+
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.aggregate.AggregationResult
+import androidx.health.connect.client.impl.platform.div
+import androidx.health.connect.client.impl.platform.duration
+import androidx.health.connect.client.impl.platform.minus
+import androidx.health.connect.client.impl.platform.toInstantWithDefaultZoneFallback
+import androidx.health.connect.client.impl.platform.useLocalTime
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
+import androidx.health.connect.client.records.IntervalRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.StepsCadenceRecord
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.request.AggregateRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.time.TimeRangeFilter
+import java.time.Duration
+import java.time.Instant
+import kotlin.math.max
+import kotlin.reflect.KClass
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.fold
+
+// Max buffer to account for overlapping records that have startTime < timeRangeFilter.startTime
+val RECORD_START_TIME_BUFFER: Duration = Duration.ofDays(1)
+
+internal suspend fun HealthConnectClient.aggregateFallback(request: AggregateRequest):
+    AggregationResult {
+    return request.fallbackMetrics.fold(
+        AggregationResult(
+            longValues = mapOf(),
+            doubleValues = mapOf(),
+            dataOrigins = setOf()
+        )
+    ) { currentAggregateResult, metric ->
+        currentAggregateResult + aggregate(
+            metric,
+            request.timeRangeFilter,
+            request.dataOriginFilter
+        )
+    }
+}
+
+private suspend fun <T : Any> HealthConnectClient.aggregate(
+    metric: AggregateMetric<T>,
+    timeRangeFilter: TimeRangeFilter,
+    dataOriginFilter: Set<DataOrigin>
+): AggregationResult {
+    return when (metric) {
+        NutritionRecord.TRANS_FAT_TOTAL -> aggregateNutritionTransFatTotal(
+            timeRangeFilter,
+            dataOriginFilter
+        )
+
+        BloodPressureRecord.DIASTOLIC_AVG -> TODO(reason = "b/326414908")
+        BloodPressureRecord.DIASTOLIC_MAX -> TODO(reason = "b/326414908")
+        BloodPressureRecord.DIASTOLIC_MIN -> TODO(reason = "b/326414908")
+        BloodPressureRecord.SYSTOLIC_AVG -> TODO(reason = "b/326414908")
+        BloodPressureRecord.SYSTOLIC_MAX -> TODO(reason = "b/326414908")
+        BloodPressureRecord.SYSTOLIC_MIN -> TODO(reason = "b/326414908")
+        CyclingPedalingCadenceRecord.RPM_AVG -> TODO(reason = "b/326414908")
+        CyclingPedalingCadenceRecord.RPM_MAX -> TODO(reason = "b/326414908")
+        CyclingPedalingCadenceRecord.RPM_MIN -> TODO(reason = "b/326414908")
+        SpeedRecord.SPEED_AVG -> TODO(reason = "b/326414908")
+        SpeedRecord.SPEED_MAX -> TODO(reason = "b/326414908")
+        SpeedRecord.SPEED_MIN -> TODO(reason = "b/326414908")
+        StepsCadenceRecord.RATE_AVG -> TODO(reason = "b/326414908")
+        StepsCadenceRecord.RATE_MAX -> TODO(reason = "b/326414908")
+        StepsCadenceRecord.RATE_MIN -> TODO(reason = "b/326414908")
+        else -> error("Invalid fallback aggregation type ${metric.metricKey}")
+    }
+}
+
+@VisibleForTesting
+internal suspend fun HealthConnectClient.aggregateNutritionTransFatTotal(
+    timeRangeFilter: TimeRangeFilter,
+    dataOriginFilter: Set<DataOrigin>
+): AggregationResult {
+    val readRecordsFlow = readRecordsFlow(
+        NutritionRecord::class,
+        timeRangeFilter.withBufferedStart(),
+        dataOriginFilter
+    )
+
+    val aggregatedData = readRecordsFlow
+        .fold(AggregatedData(0.0)) { currentAggregatedData, records ->
+            val filteredRecords = records.filter {
+                it.overlaps(timeRangeFilter) && it.transFat != null &&
+                    sliceFactor(it, timeRangeFilter) > 0
+            }
+
+            filteredRecords.forEach {
+                currentAggregatedData.value +=
+                    it.transFat!!.inGrams * sliceFactor(it, timeRangeFilter)
+            }
+
+            filteredRecords.mapTo(currentAggregatedData.dataOrigins) { it.metadata.dataOrigin }
+            currentAggregatedData
+        }
+
+    if (aggregatedData.dataOrigins.isEmpty()) {
+        return emptyAggregationResult()
+    }
+
+    return AggregationResult(
+        longValues = mapOf(),
+        doubleValues = mapOf(NutritionRecord.TRANS_FAT_TOTAL.metricKey to aggregatedData.value),
+        dataOrigins = aggregatedData.dataOrigins
+    )
+}
+
+/** Reads all existing records that satisfy [timeRangeFilter] and [dataOriginFilter]. */
+@VisibleForTesting
+suspend fun <T : Record> HealthConnectClient.readRecordsFlow(
+    recordType: KClass<T>,
+    timeRangeFilter: TimeRangeFilter,
+    dataOriginFilter: Set<DataOrigin>
+): Flow<List<T>> {
+    return flow {
+        var pageToken: String? = null
+        do {
+            val response = readRecords(
+                ReadRecordsRequest(
+                    recordType = recordType,
+                    timeRangeFilter = timeRangeFilter,
+                    dataOriginFilter = dataOriginFilter,
+                    pageToken = pageToken
+                )
+            )
+            emit(response.records)
+            pageToken = response.pageToken
+        } while (pageToken != null)
+    }
+}
+
+private fun IntervalRecord.overlaps(timeRangeFilter: TimeRangeFilter): Boolean {
+    val startTimeOverlaps: Boolean
+    val endTimeOverlaps: Boolean
+    if (timeRangeFilter.useLocalTime()) {
+        startTimeOverlaps = timeRangeFilter.localEndTime == null ||
+            startTime.isBefore(
+                timeRangeFilter.localEndTime.toInstantWithDefaultZoneFallback(startZoneOffset)
+            )
+        endTimeOverlaps = timeRangeFilter.localStartTime == null ||
+            endTime.isAfter(
+                timeRangeFilter.localStartTime.toInstantWithDefaultZoneFallback(endZoneOffset)
+            )
+    } else {
+        startTimeOverlaps = timeRangeFilter.endTime == null ||
+            startTime.isBefore(timeRangeFilter.endTime)
+        endTimeOverlaps = timeRangeFilter.startTime == null ||
+            endTime.isAfter(timeRangeFilter.startTime)
+    }
+    return startTimeOverlaps && endTimeOverlaps
+}
+
+private fun TimeRangeFilter.withBufferedStart(): TimeRangeFilter {
+    return TimeRangeFilter(
+        startTime = startTime?.minus(RECORD_START_TIME_BUFFER),
+        endTime = endTime,
+        localStartTime = localStartTime?.minus(RECORD_START_TIME_BUFFER),
+        localEndTime = localEndTime
+    )
+}
+
+private fun sliceFactor(record: NutritionRecord, timeRangeFilter: TimeRangeFilter): Double {
+    val startTime: Instant
+    val endTime: Instant
+
+    if (timeRangeFilter.useLocalTime()) {
+        val requestStartTime =
+            timeRangeFilter.localStartTime?.toInstantWithDefaultZoneFallback(record.startZoneOffset)
+        val requestEndTime =
+            timeRangeFilter.localEndTime?.toInstantWithDefaultZoneFallback(record.endZoneOffset)
+        startTime = maxOf(record.startTime, requestStartTime ?: record.startTime)
+        endTime = minOf(record.endTime, requestEndTime ?: record.endTime)
+    } else {
+        startTime = maxOf(record.startTime, timeRangeFilter.startTime ?: record.startTime)
+        endTime = minOf(record.endTime, timeRangeFilter.endTime ?: record.endTime)
+    }
+
+    return max(0.0, (endTime - startTime) / record.duration)
+}
+
+private fun emptyAggregationResult() =
+    AggregationResult(longValues = mapOf(), doubleValues = mapOf(), dataOrigins = setOf())
+
+private data class AggregatedData<T>(
+    var value: T,
+    var dataOrigins: MutableSet<DataOrigin> = mutableSetOf()
+)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/request/RequestConverters.kt
similarity index 78%
rename from health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt
rename to health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/request/RequestConverters.kt
index 34699be..ac3fd89 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/request/RequestConverters.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 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.
@@ -17,7 +17,7 @@
 @file:RestrictTo(RestrictTo.Scope.LIBRARY)
 @file:RequiresApi(api = 34)
 
-package androidx.health.connect.client.impl.platform.records
+package androidx.health.connect.client.impl.platform.request
 
 import android.health.connect.AggregateRecordsRequest
 import android.health.connect.LocalTimeRangeFilter
@@ -30,6 +30,20 @@
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.impl.platform.aggregate.DOUBLE_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.DURATION_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.ENERGY_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.GRAMS_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.LENGTH_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.LONG_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.POWER_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.PRESSURE_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.VELOCITY_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.VOLUME_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.platformMetrics
+import androidx.health.connect.client.impl.platform.records.toPlatformDataOrigin
+import androidx.health.connect.client.impl.platform.records.toPlatformRecordClass
 import androidx.health.connect.client.records.Record
 import androidx.health.connect.client.request.AggregateGroupByDurationRequest
 import androidx.health.connect.client.request.AggregateGroupByPeriodRequest
@@ -101,7 +115,7 @@
     return AggregateRecordsRequest.Builder<Any>(timeRangeFilter.toPlatformTimeRangeFilter())
         .apply {
             dataOriginFilter.forEach { addDataOriginsFilter(it.toPlatformDataOrigin()) }
-            metrics.forEach { addAggregationType(it.toAggregationType()) }
+            platformMetrics.forEach { addAggregationType(it.toAggregationType()) }
         }
         .build()
 }
@@ -132,11 +146,13 @@
     return DOUBLE_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
         ?: DURATION_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
         ?: ENERGY_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+        ?: GRAMS_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
         ?: LENGTH_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
         ?: LONG_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
-        ?: GRAMS_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
         ?: KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
         ?: POWER_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+        ?: PRESSURE_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+        ?: VELOCITY_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
         ?: VOLUME_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
         ?: throw IllegalArgumentException("Unsupported aggregation type $metricKey")
 }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/ResponseConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/ResponseConverters.kt
similarity index 70%
rename from health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/ResponseConverters.kt
rename to health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/ResponseConverters.kt
index 30e2279..a6ff703 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/ResponseConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/ResponseConverters.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 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.
@@ -17,7 +17,7 @@
 @file:RestrictTo(RestrictTo.Scope.LIBRARY)
 @file:RequiresApi(api = 34)
 
-package androidx.health.connect.client.impl.platform.records
+package androidx.health.connect.client.impl.platform.response
 
 import android.health.connect.AggregateRecordsGroupedByDurationResponse
 import android.health.connect.AggregateRecordsGroupedByPeriodResponse
@@ -32,6 +32,25 @@
 import androidx.health.connect.client.aggregate.AggregationResult
 import androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration
 import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod
+import androidx.health.connect.client.impl.platform.aggregate.DOUBLE_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.DURATION_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.ENERGY_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.GRAMS_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.LENGTH_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.LONG_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.POWER_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.PRESSURE_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.VELOCITY_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.aggregate.VOLUME_AGGREGATION_METRIC_TYPE_MAP
+import androidx.health.connect.client.impl.platform.records.PlatformDataOrigin
+import androidx.health.connect.client.impl.platform.records.PlatformLength
+import androidx.health.connect.client.impl.platform.records.PlatformMass
+import androidx.health.connect.client.impl.platform.records.PlatformPower
+import androidx.health.connect.client.impl.platform.records.PlatformPressure
+import androidx.health.connect.client.impl.platform.records.PlatformVelocity
+import androidx.health.connect.client.impl.platform.records.toSdkDataOrigin
+import androidx.health.connect.client.impl.platform.request.toAggregationType
 import androidx.health.connect.client.units.Energy
 import androidx.health.connect.client.units.Mass
 import java.time.LocalDateTime
@@ -99,7 +118,7 @@
         metricValueMap.forEach { (key, value) ->
             if (
                 key in DURATION_AGGREGATION_METRIC_TYPE_MAP ||
-                    key in LONG_AGGREGATION_METRIC_TYPE_MAP
+                key in LONG_AGGREGATION_METRIC_TYPE_MAP
             ) {
                 this[key.metricKey] = value as Long
             }
@@ -117,22 +136,36 @@
                 in DOUBLE_AGGREGATION_METRIC_TYPE_MAP -> {
                     this[key.metricKey] = value as Double
                 }
+
                 in ENERGY_AGGREGATION_METRIC_TYPE_MAP -> {
                     this[key.metricKey] =
                         Energy.calories((value as PlatformEnergy).inCalories).inKilocalories
                 }
-                in LENGTH_AGGREGATION_METRIC_TYPE_MAP -> {
-                    this[key.metricKey] = (value as PlatformLength).inMeters
-                }
+
                 in GRAMS_AGGREGATION_METRIC_TYPE_MAP -> {
                     this[key.metricKey] = (value as PlatformMass).inGrams
                 }
+
+                in LENGTH_AGGREGATION_METRIC_TYPE_MAP -> {
+                    this[key.metricKey] = (value as PlatformLength).inMeters
+                }
+
                 in KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP -> {
                     this[key.metricKey] = Mass.grams((value as PlatformMass).inGrams).inKilograms
                 }
+
+                in PRESSURE_AGGREGATION_METRIC_TYPE_MAP -> {
+                    this[key.metricKey] = (value as PlatformPressure).inMillimetersOfMercury
+                }
+
                 in POWER_AGGREGATION_METRIC_TYPE_MAP -> {
                     this[key.metricKey] = (value as PlatformPower).inWatts
                 }
+
+                in VELOCITY_AGGREGATION_METRIC_TYPE_MAP -> {
+                    this[key.metricKey] = (value as PlatformVelocity).inMetersPerSecond
+                }
+
                 in VOLUME_AGGREGATION_METRIC_TYPE_MAP -> {
                     this[key.metricKey] = (value as PlatformVolume).inLiters
                 }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/TimeExtensionsTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/TimeExtensionsTest.kt
new file mode 100644
index 0000000..a6cdd65
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/TimeExtensionsTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 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.impl.platform
+
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.time.TimeRangeFilter
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+import kotlin.test.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TimeExtensionsTest {
+
+    @Test
+    fun div() {
+        val dividend = Duration.ofHours(1)
+        val divisor = Duration.ofHours(4)
+        assertThat(dividend / divisor).isEqualTo(0.25)
+    }
+
+    @Test
+    fun dibByZero_returnsZero() {
+        val dividend = Duration.ofHours(1)
+        val divisor = Duration.ofSeconds(0)
+        assertThat(dividend / divisor).isEqualTo(0.0)
+    }
+
+    @Test
+    fun minus() {
+        val a = Instant.now()
+        val b = a.plusSeconds(5)
+        assertThat(b - a).isEqualTo(Duration.ofSeconds(5))
+    }
+
+    @Test
+    fun useLocalTime() {
+        assertThat(TimeRangeFilter.none().useLocalTime()).isFalse()
+        assertThat(
+            TimeRangeFilter.between(Instant.now(), Instant.now().plusSeconds(2)).useLocalTime()
+        ).isFalse()
+        assertThat(TimeRangeFilter.after(Instant.now()).useLocalTime()).isFalse()
+        assertThat(TimeRangeFilter.before(Instant.now()).useLocalTime()).isFalse()
+
+        assertThat(
+            TimeRangeFilter.between(LocalDateTime.now(), LocalDateTime.now().plusSeconds(2))
+                .useLocalTime()
+        ).isTrue()
+        assertThat(TimeRangeFilter.after(LocalDateTime.now()).useLocalTime()).isTrue()
+        assertThat(TimeRangeFilter.before(LocalDateTime.now()).useLocalTime()).isTrue()
+    }
+
+    @Test
+    fun toInstantWithDefaultZoneFallback() {
+        val instant = Instant.now()
+        val localDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC)
+
+        assertThat(localDateTime.toInstantWithDefaultZoneFallback(ZoneOffset.UTC))
+        .isEqualTo(instant)
+        assertThat(localDateTime.toInstantWithDefaultZoneFallback(ZoneOffset.ofHours(2)))
+            .isEqualTo(instant - Duration.ofHours(2))
+    }
+
+    @Test
+    fun intervalRecord_duration() {
+        val startTime = Instant.now()
+        val nutritionRecord = NutritionRecord(
+            startTime = startTime,
+            endTime = startTime.plusSeconds(10),
+            startZoneOffset = null,
+            endZoneOffset = null
+        )
+        assertThat(nutritionRecord.duration).isEqualTo(Duration.ofSeconds(10))
+    }
+}