[go: nahoru, domu]

Merge changes Ic779a935,Idd04f23f into androidx-main

* changes:
  Add tests for MouseInjectionScope
  Add PointerType to PointerInputChange verifications
diff --git a/bluetooth/OWNERS b/bluetooth/OWNERS
index 9b3a924..3ca2e7e 100644
--- a/bluetooth/OWNERS
+++ b/bluetooth/OWNERS
@@ -1,3 +1,8 @@
 sungsoo@google.com
 hallstrom@google.com
+hdmoon@google.com
+kihongs@google.com
+klhyun@google.com
+ofy@google.com
+thebestguy@google.com
 
diff --git a/bluetooth/integration-tests/testapp/build.gradle b/bluetooth/integration-tests/testapp/build.gradle
index b9e16b3..7f79127 100644
--- a/bluetooth/integration-tests/testapp/build.gradle
+++ b/bluetooth/integration-tests/testapp/build.gradle
@@ -22,10 +22,42 @@
     id("kotlin-android")
 }
 
-dependencies {
-    implementation(libs.kotlinStdlib)
+android {
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 31
+        versionCode 1
+        versionName "1.0"
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = "1.8"
+    }
+    buildFeatures {
+        viewBinding true
+    }
+
+    namespace "androidx.bluetooth.integration.testapp"
 }
 
-android {
-    namespace "androidx.bluetooth.integration.testapp"
+dependencies {
+    implementation(libs.kotlinStdlib)
+
+    implementation(project(":bluetooth:bluetooth"))
+
+//    Removing temporarily because core-ktx:1.8.0 is using activity:1.1.0 instead of activity:1.4.0
+//    implementation("androidx.core:core-ktx:1.8.0")
+//    implementation("androidx.appcompat:appcompat:1.4.2")
+
+    implementation("androidx.activity:activity-ktx:1.4.0")
+    implementation("androidx.fragment:fragment-ktx:1.4.1")
+
+    implementation(libs.constraintLayout)
+
+    implementation("androidx.navigation:navigation-fragment-ktx:2.4.2")
+    implementation("androidx.navigation:navigation-ui-ktx:2.4.2")
 }
diff --git a/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml b/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml
index 2027b80..c48ff97 100644
--- a/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -5,9 +5,10 @@
     <application
         android:allowBackup="false"
         android:label="@string/app_name"
+        android:theme="@style/Theme.AppCompat.Light"
         tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon">
         <activity
-            android:name=".BluetoothXTestAppActivity"
+            android:name=".MainActivity"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -17,4 +18,9 @@
         </activity>
     </application>
 
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+
 </manifest>
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
new file mode 100644
index 0000000..a5d149b
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 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.bluetooth.integration.testapp
+
+import android.Manifest
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.bluetooth.integration.testapp.databinding.ActivityMainBinding
+import androidx.core.view.WindowCompat
+import androidx.navigation.findNavController
+import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.navigateUp
+import androidx.navigation.ui.setupActionBarWithNavController
+
+class MainActivity : AppCompatActivity() {
+
+    companion object {
+        const val TAG = "MainActivity"
+    }
+
+    private val requestBluetoothPermissions =
+        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms ->
+            perms.entries.forEach { permission ->
+                Log.d(TAG, "${permission.key} = ${permission.value}")
+            }
+        }
+
+    private lateinit var appBarConfiguration: AppBarConfiguration
+    private lateinit var binding: ActivityMainBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+        super.onCreate(savedInstanceState)
+
+        binding = ActivityMainBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        val navController = findNavController(R.id.nav_host_fragment)
+        appBarConfiguration = AppBarConfiguration(navController.graph)
+        setupActionBarWithNavController(navController, appBarConfiguration)
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        requestBluetoothPermissions.launch(
+            arrayOf(
+                Manifest.permission.BLUETOOTH_SCAN,
+                Manifest.permission.ACCESS_FINE_LOCATION
+            )
+        )
+    }
+
+    override fun onSupportNavigateUp(): Boolean {
+        val navController = findNavController(R.id.nav_host_fragment)
+        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
+    }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/bluetoothx/BtxFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/bluetoothx/BtxFragment.kt
new file mode 100644
index 0000000..db47746
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/bluetoothx/BtxFragment.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022 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.bluetooth.integration.testapp.ui.bluetoothx
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.bluetooth.integration.testapp.R
+import androidx.bluetooth.integration.testapp.databinding.FragmentBtxBinding
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+
+class BtxFragment : Fragment() {
+
+    companion object {
+        const val TAG = "BtxFragment"
+    }
+
+    private var _binding: FragmentBtxBinding? = null
+
+    // This property is only valid between onCreateView and onDestroyView.
+    private val binding get() = _binding!!
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentBtxBinding.inflate(inflater, container, false)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        binding.buttonPrevious.setOnClickListener {
+            findNavController().navigate(R.id.action_BtxFragment_to_FwkFragment)
+        }
+
+        binding.buttonScan.setOnClickListener {
+            scan()
+        }
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
+    private fun scan() {
+        Toast.makeText(context, getString(R.string.scan_not_yet_implemented), Toast.LENGTH_SHORT)
+            .show()
+    }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/framework/FwkFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/framework/FwkFragment.kt
new file mode 100644
index 0000000..220061d
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/framework/FwkFragment.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 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.bluetooth.integration.testapp.ui.framework
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothManager
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanResult
+import android.bluetooth.le.ScanSettings
+import android.content.Context
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.bluetooth.integration.testapp.R
+import androidx.bluetooth.integration.testapp.databinding.FragmentFwkBinding
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+
+class FwkFragment : Fragment() {
+
+    companion object {
+        const val TAG = "FwkFragment"
+    }
+
+    private val scanCallback = object : ScanCallback() {
+        override fun onScanResult(callbackType: Int, result: ScanResult?) {
+            Log.d(TAG, "onScanResult() called with: callbackType = $callbackType, result = $result")
+        }
+
+        override fun onBatchScanResults(results: MutableList<ScanResult>?) {
+            Log.d(TAG, "onBatchScanResults() called with: results = $results")
+        }
+
+        override fun onScanFailed(errorCode: Int) {
+            Log.d(TAG, "onScanFailed() called with: errorCode = $errorCode")
+        }
+    }
+
+    private var _binding: FragmentFwkBinding? = null
+
+    // This property is only valid between onCreateView and onDestroyView.
+    private val binding get() = _binding!!
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentFwkBinding.inflate(inflater, container, false)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        binding.buttonNext.setOnClickListener {
+            findNavController().navigate(R.id.action_FwkFragment_to_BtxFragment)
+        }
+
+        binding.buttonScan.setOnClickListener {
+            scan()
+        }
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
+    // Permissions are handled by MainActivity requestBluetoothPermissions
+    @SuppressLint("MissingPermission")
+    private fun scan() {
+        Log.d(TAG, "scan() called")
+
+        val bluetoothManager =
+            context?.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
+
+        val bluetoothAdapter = bluetoothManager?.adapter
+
+        val bleScanner = bluetoothAdapter?.bluetoothLeScanner
+
+        val scanSettings = ScanSettings.Builder()
+            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+            .build()
+
+        bleScanner?.startScan(null, scanSettings, scanCallback)
+
+        Toast.makeText(context, getString(R.string.scan_start_message), Toast.LENGTH_LONG)
+            .show()
+    }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/activity_bluetoothx_test_app.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
similarity index 65%
rename from bluetooth/integration-tests/testapp/src/main/res/layout/activity_bluetoothx_test_app.xml
rename to bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
index 6e4fd22..8f063c6 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/activity_bluetoothx_test_app.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -15,13 +15,21 @@
   limitations under the License.
   -->
 
-<LinearLayout
+<androidx.coordinatorlayout.widget.CoordinatorLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical"
-    tools:context="androidx.bluetooth.integration.testapp.BluetoothXTestAppActivity">
+    android:fitsSystemWindows="true"
+    tools:context=".MainActivity">
 
-</LinearLayout>
+    <fragment
+        android:id="@+id/nav_host_fragment"
+        android:name="androidx.navigation.fragment.NavHostFragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:defaultNavHost="true"
+        app:navGraph="@navigation/nav_graph" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_btx.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_btx.xml
new file mode 100644
index 0000000..0e75c18
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_btx.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 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.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="16dp"
+    tools:context=".ui.bluetoothx.BtxFragment">
+
+    <Button
+        android:id="@+id/button_previous"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="16dp"
+        android:layout_marginEnd="16dp"
+        android:text="@string/previous"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <Button
+        android:id="@+id/button_scan"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="16dp"
+        android:text="@string/scan_using_btx"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_fwk.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_fwk.xml
new file mode 100644
index 0000000..754f93b
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_fwk.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 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.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="16dp"
+    tools:context=".ui.framework.FwkFragment">
+
+    <Button
+        android:id="@+id/button_next"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="16dp"
+        android:layout_marginEnd="16dp"
+        android:text="@string/next"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <Button
+        android:id="@+id/button_scan"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="16dp"
+        android:text="@string/scan_using_fwk"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml b/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..8cb583c
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 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.
+  -->
+
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/nav_graph"
+    app:startDestination="@id/FwkFragment">
+
+    <fragment
+        android:id="@+id/FwkFragment"
+        android:name="androidx.bluetooth.integration.testapp.ui.framework.FwkFragment"
+        android:label="@string/fwk_fragment_label"
+        tools:layout="@layout/fragment_fwk">
+        <action
+            android:id="@+id/action_FwkFragment_to_BtxFragment"
+            app:destination="@id/BtxFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/BtxFragment"
+        android:name="androidx.bluetooth.integration.testapp.ui.bluetoothx.BtxFragment"
+        android:label="@string/btx_fragment_label"
+        tools:layout="@layout/fragment_btx">
+        <action
+            android:id="@+id/action_BtxFragment_to_FwkFragment"
+            app:destination="@id/FwkFragment" />
+    </fragment>
+
+</navigation>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index ad839c3..b0142e3 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -17,4 +17,16 @@
 
 <resources>
     <string name="app_name">BluetoothX Test App</string>
+
+    <!-- Strings used for fragments for navigation -->
+    <string name="fwk_fragment_label">Framework Bluetooth Fragment</string>
+    <string name="btx_fragment_label">BluetoothX Fragment</string>
+    <string name="next">Next</string>
+    <string name="previous">Previous</string>
+
+    <string name="scan_using_fwk">Scan using Framework Bluetooth APIs</string>
+    <string name="scan_using_btx">Scan using BluetoothX APIs</string>
+
+    <string name="scan_start_message">Scan should have started. Results are in Logcat</string>
+    <string name="scan_not_yet_implemented">Scan not yet implemented.</string>
 </resources>
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt
index 63eeb3f..a36d09a 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt
@@ -35,18 +35,18 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executors
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.lang.IllegalStateException
-import java.util.ArrayList
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.Semaphore
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -56,34 +56,7 @@
     private var backgroundHandler: Handler? = null
     private val captureStage: CaptureStage = CaptureStage.DefaultCaptureStage()
     private val processingSurfaces: MutableList<ProcessingSurface> = ArrayList()
-
-    /*
-     * Capture processor that simply writes out an empty image to exercise the pipeline
-     */
-    @RequiresApi(23)
-    private val captureProcessor: CaptureProcessor = object : CaptureProcessor {
-        var mImageWriter: ImageWriter? = null
-        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
-            mImageWriter = ImageWriter.newInstance(surface, 2)
-        }
-
-        override fun process(bundle: ImageProxyBundle) {
-            try {
-                val imageProxyListenableFuture = bundle.getImageProxy(
-                    captureStage.id
-                )
-                val imageProxy = imageProxyListenableFuture[100, TimeUnit.MILLISECONDS]
-                val image = mImageWriter!!.dequeueInputImage()
-                image.timestamp = imageProxy.imageInfo.timestamp
-                mImageWriter!!.queueInputImage(image)
-            } catch (e: ExecutionException) {
-            } catch (e: TimeoutException) {
-            } catch (e: InterruptedException) {
-            }
-        }
-
-        override fun onResolutionUpdate(size: Size) {}
-    }
+    private val captureProcessor: FakeCaptureProcessor = FakeCaptureProcessor(captureStage.id)
 
     @Before
     fun setup() {
@@ -102,17 +75,15 @@
     }
 
     @Test
-    @Throws(ExecutionException::class, InterruptedException::class)
     fun validInputSurface() {
         val processingSurface = createProcessingSurface(
             newImmediateSurfaceDeferrableSurface()
         )
         val surface = processingSurface.surface.get()
-        Truth.assertThat(surface).isNotNull()
+        assertThat(surface).isNotNull()
     }
 
     @Test
-    @Throws(ExecutionException::class, InterruptedException::class)
     fun writeToInputSurface_userOutputSurfaceReceivesFrame() {
         // Arrange.
         val frameReceivedSemaphore = Semaphore(0)
@@ -137,7 +108,7 @@
         triggerImage(processingSurface, 1)
 
         // Assert: verify that the frame has been received or time-out after 3 second.
-        Truth.assertThat(frameReceivedSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        assertThat(frameReceivedSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
     }
 
     // Exception should be thrown here
@@ -156,8 +127,7 @@
         } catch (e: InterruptedException) {
             cause = e.cause
         }
-        Truth.assertThat(cause)
-            .isInstanceOf(DeferrableSurface.SurfaceClosedException::class.java)
+        assertThat(cause).isInstanceOf(DeferrableSurface.SurfaceClosedException::class.java)
     }
 
     // Exception should be thrown here
@@ -170,6 +140,43 @@
         processingSurface.cameraCaptureCallback
     }
 
+    @Test
+    fun completeTerminationFutureAfterProcessIsFinished() {
+        // Arrange.
+        val processingSurface = createProcessingSurface(newImmediateSurfaceDeferrableSurface())
+
+        // Sets up the processor blocker to block the CaptureProcessor#process() function execution.
+        captureProcessor.setupProcessorBlocker()
+
+        // Monitors whether the ProcessingSurface termination future has been completed.
+        val terminationCountDownLatch = CountDownLatch(1)
+        processingSurface.terminationFuture.addListener({
+            terminationCountDownLatch.countDown()
+        }, Executors.newSingleThreadExecutor())
+
+        // Act: Sends one frame to processingSurface.
+        triggerImage(processingSurface, 1)
+
+        // Waits for that the CaptureProcessor#process() function is called. Otherwise, the
+        // following ProcessingSurface#close() function call may directly close the
+        // ProcessingSurface.
+        assertThat(captureProcessor.awaitProcessingState(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        // Act: triggers the ProcessingSurface close flow.
+        processingSurface.close()
+
+        // Assert: verify that the termination future won't be completed before
+        // CaptureProcessor#process() execution is finished.
+        assertThat(terminationCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isFalse()
+
+        // Act: releases the processor blocker to finish the CaptureProcessor#process() execution.
+        captureProcessor.releaseProcessorBlocker()
+
+        // Assert: verify that the termination future is completed after CaptureProcessor#process()
+        // execution is finished.
+        assertThat(terminationCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
     @RequiresApi(23)
     @Throws(ExecutionException::class, InterruptedException::class)
     private fun triggerImage(processingSurface: ProcessingSurface, timestamp: Long) {
@@ -217,4 +224,53 @@
         private val RESOLUTION: Size by lazy { Size(640, 480) }
         private const val FORMAT = ImageFormat.YUV_420_888
     }
-}
\ No newline at end of file
+
+    /**
+     * Capture processor that can write out an empty image to exercise the pipeline.
+     *
+     * <p>The fake capture processor can be controlled to be blocked in the processing state and
+     * then release the blocker to complete it.
+     */
+    @RequiresApi(23)
+    private class FakeCaptureProcessor(private val captureStageId: Int) : CaptureProcessor {
+        private val processingCountDownLatch = CountDownLatch(1)
+        private var processorBlockerCountDownLatch: CountDownLatch? = null
+
+        var imageWriter: ImageWriter? = null
+        override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+            imageWriter = ImageWriter.newInstance(surface, 2)
+        }
+
+        override fun process(bundle: ImageProxyBundle) {
+            processingCountDownLatch.countDown()
+            try {
+                val imageProxyListenableFuture = bundle.getImageProxy(captureStageId)
+                val imageProxy = imageProxyListenableFuture[100, TimeUnit.MILLISECONDS]
+                val image = imageWriter!!.dequeueInputImage()
+                image.timestamp = imageProxy.imageInfo.timestamp
+                imageWriter!!.queueInputImage(image)
+
+                processorBlockerCountDownLatch?.await()
+            } catch (_: ExecutionException) {
+            } catch (_: TimeoutException) {
+            } catch (_: InterruptedException) {
+            }
+        }
+
+        override fun onResolutionUpdate(size: Size) {}
+
+        fun awaitProcessingState(timeout: Long, timeUnit: TimeUnit): Boolean {
+            return processingCountDownLatch.await(timeout, timeUnit)
+        }
+
+        fun setupProcessorBlocker() {
+            if (processorBlockerCountDownLatch == null) {
+                processorBlockerCountDownLatch = CountDownLatch(1)
+            }
+        }
+
+        fun releaseProcessorBlocker() {
+            processorBlockerCountDownLatch?.countDown()
+        }
+    }
+}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
index f77cc73..e2b0f0f 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
@@ -155,30 +155,6 @@
     }
 
     @Test
-    fun isServiced_trueAfterProvideSurface() {
-        val request = createNewRequest(FAKE_SIZE)
-        request.provideSurface(
-            MOCK_SURFACE,
-            CameraXExecutors.directExecutor(),
-            NO_OP_RESULT_LISTENER
-        )
-        Truth.assertThat(request.isServiced).isTrue()
-    }
-
-    @Test
-    fun isServiced_trueAfterWillNotProvideSurface() {
-        val request = createNewRequest(FAKE_SIZE)
-        request.willNotProvideSurface()
-        Truth.assertThat(request.isServiced).isTrue()
-    }
-
-    @Test
-    fun isServiced_falseInitially() {
-        val request = createNewRequest(FAKE_SIZE)
-        Truth.assertThat(request.isServiced).isFalse()
-    }
-
-    @Test
     @Suppress("UNCHECKED_CAST")
     fun cancelledRequest_resultsInREQUEST_CANCELLED() {
         val request = createNewRequest(FAKE_SIZE)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 1aede27..5b6abb7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -124,7 +124,6 @@
 import java.nio.ByteBuffer;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Deque;
 import java.util.List;
 import java.util.Locale;
@@ -487,6 +486,11 @@
             mImageReader = new SafeCloseImageReaderProxy(metadataImageReader);
         }
 
+        if (mImageCaptureRequestProcessor != null) {
+            mImageCaptureRequestProcessor.cancelRequests(
+                    new CancellationException("Request is canceled."));
+        }
+
         final YuvToJpegProcessor finalSoftwareJpegProcessor = softwareJpegProcessor;
 
         mImageCaptureRequestProcessor = new ImageCaptureRequestProcessor(MAX_IMAGES,
@@ -536,11 +540,6 @@
         sessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
 
         sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
-            // Get the unfinished requests before re-create the pipeline
-            List<ImageCaptureRequest> pendingRequests = (mImageCaptureRequestProcessor != null)
-                    ? mImageCaptureRequestProcessor.pullOutUnfinishedRequests()
-                    : Collections.emptyList();
-
             clearPipeline();
             // Ensure the attached camera has not changed before resetting.
             // TODO(b/143915543): Ensure this never gets called by a camera that is not attached
@@ -548,14 +547,6 @@
             if (isCurrentCamera(cameraId)) {
                 // Only reset the pipeline when the bound camera is the same.
                 mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
-
-                if (mImageCaptureRequestProcessor != null) {
-                    // Restore the unfinished requests to the created pipeline
-                    for (ImageCaptureRequest request: pendingRequests) {
-                        mImageCaptureRequestProcessor.sendRequest(request);
-                    }
-                }
-
                 updateSessionConfig(mSessionConfigBuilder.build());
                 notifyReset();
             }
@@ -572,7 +563,6 @@
         CameraConfig cameraConfig = getCamera().getExtendedConfig();
         return cameraConfig == null ? false : cameraConfig.getSessionProcessor(null) != null;
     }
-
     /**
      * Clear the internal pipeline so that the pipeline can be set up again.
      */
@@ -580,6 +570,11 @@
     @SuppressWarnings("WeakerAccess")
     void clearPipeline() {
         Threads.checkMainThread();
+        if (mImageCaptureRequestProcessor != null) {
+            mImageCaptureRequestProcessor.cancelRequests(
+                    new CancellationException("Request is canceled."));
+            mImageCaptureRequestProcessor = null;
+        }
         DeferrableSurface deferrableSurface = mDeferrableSurface;
         mDeferrableSurface = null;
         mImageReader = null;
@@ -1133,15 +1128,14 @@
     @UiThread
     @Override
     public void onStateDetached() {
-        detachImageCaptureRequestProcessor();
+        abortImageCaptureRequests();
     }
 
     @UiThread
-    private void detachImageCaptureRequestProcessor() {
+    private void abortImageCaptureRequests() {
         if (mImageCaptureRequestProcessor != null) {
             Throwable throwable = new CameraClosedException("Camera is closed.");
             mImageCaptureRequestProcessor.cancelRequests(throwable);
-            mImageCaptureRequestProcessor = null;
         }
     }
 
@@ -1426,36 +1420,6 @@
             }
         }
 
-        /**
-         * Removes and returns all unfinished requests.
-         *
-         * <p>The unfinished requests include:
-         * <ul>
-         *     <li>Current running request if it is not complete yet.</li>
-         *     <li>All pending requests.</li>
-         * </ul>
-         *
-         * @return list of the remaining requests
-         */
-        @NonNull
-        public List<ImageCaptureRequest> pullOutUnfinishedRequests() {
-            List<ImageCaptureRequest> remainingRequests;
-            synchronized (mLock) {
-                remainingRequests = new ArrayList<>(mPendingRequests);
-                // Clear the pending requests before canceling the mCurrentRequestFuture.
-                mPendingRequests.clear();
-
-                ImageCaptureRequest currentRequest = mCurrentRequest;
-                mCurrentRequest = null;
-                if (currentRequest != null && mCurrentRequestFuture != null
-                        && mCurrentRequestFuture.cancel(true)) {
-                    remainingRequests.add(0, currentRequest);
-                }
-            }
-
-            return remainingRequests;
-        }
-
         @Override
         public void onImageClose(ImageProxy image) {
             synchronized (mLock) {
@@ -1611,7 +1575,7 @@
     public void onDetached() {
         ListenableFuture<Void> imageReaderCloseFuture = mImageReaderCloseFuture;
 
-        detachImageCaptureRequestProcessor();
+        abortImageCaptureRequests();
         clearPipeline();
         mUseSoftwareJpeg = false;
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java
index acb6077..8cd49c8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java
@@ -262,8 +262,20 @@
         } else {
             SingleImageProxyBundle imageProxyBundle = new SingleImageProxyBundle(image,
                     mTagBundleKey);
+            try {
+                // Increments the use count to prevent the surface from being closed during the
+                // processing duration.
+                incrementUseCount();
+            } catch (SurfaceClosedException e) {
+                Logger.d(TAG, "The ProcessingSurface has been closed. Don't process the incoming "
+                        + "image.");
+                imageProxyBundle.close();
+                return;
+            }
             mCaptureProcessor.process(imageProxyBundle);
             imageProxyBundle.close();
+            // Decrements the use count to allow the surface to be closed.
+            decrementUseCount();
         }
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
index c83b35d..b6b54aa 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
@@ -241,20 +241,6 @@
     }
 
     /**
-     * Returns whether this surface request has been serviced.
-     *
-     * <p>A surface request is considered serviced if
-     * {@link #provideSurface(Surface, Executor, Consumer)} or {@link #willNotProvideSurface()}
-     * has been called.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public boolean isServiced() {
-        return mSurfaceFuture.isDone();
-    }
-
-    /**
      * Returns the resolution of the requested {@link Surface}.
      *
      * The surface which fulfills this request must have the resolution specified here in
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 1a6fcad..84c6554 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -62,7 +62,6 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito.atLeastOnce
-import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.timeout
@@ -654,66 +653,6 @@
         }
     }
 
-    @Test
-    fun canReuseRecorder() {
-        val recorder = Recorder.Builder().build()
-        @Suppress("UNCHECKED_CAST")
-        val mockListener = mock(Consumer::class.java) as Consumer<VideoRecordEvent>
-        val videoCapture1 = VideoCapture.withOutput(recorder)
-        val videoCapture2 = VideoCapture.withOutput(recorder)
-
-        instrumentation.runOnMainSync {
-            cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, videoCapture1)
-        }
-
-        val file1 = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val file2 = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        val inOrder = inOrder(mockListener)
-        try {
-            videoCapture1.output.prepareRecording(context, FileOutputOptions.Builder(file1).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockListener).use {
-                    inOrder.verify(mockListener, timeout(5000L))
-                        .accept(any(VideoRecordEvent.Start::class.java))
-                    inOrder.verify(mockListener, timeout(15000L).atLeast(5))
-                        .accept(any(VideoRecordEvent.Status::class.java))
-                }
-
-            inOrder.verify(mockListener, timeout(5000L))
-                .accept(any(VideoRecordEvent.Finalize::class.java))
-
-            verifyRecordingResult(file1, true)
-        } finally {
-            file1.delete()
-        }
-
-        clearInvocations(mockListener)
-
-        instrumentation.runOnMainSync {
-            cameraProvider.unbindAll()
-            cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, videoCapture2)
-        }
-
-        try {
-            videoCapture2.output.prepareRecording(context, FileOutputOptions.Builder(file2).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockListener).use {
-                    inOrder.verify(mockListener, timeout(5000L))
-                        .accept(any(VideoRecordEvent.Start::class.java))
-                    inOrder.verify(mockListener, timeout(15000L).atLeast(5))
-                        .accept(any(VideoRecordEvent.Status::class.java))
-                }
-
-            inOrder.verify(mockListener, timeout(5000L))
-                .accept(any(VideoRecordEvent.Finalize::class.java))
-
-            verifyRecordingResult(file2, true)
-        } finally {
-            file2.delete()
-        }
-    }
-
     private fun startVideoRecording(videoCapture: VideoCapture<Recorder>, file: File):
         Recording {
             val recording = videoCapture.output
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index a48f764..29cb814 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -159,23 +159,17 @@
 
     enum State {
         /**
-         * The Recorder is being configured.
+         * The Recorder is being initialized.
          *
          * <p>The Recorder will reach this state whenever it is waiting for a surface request.
          */
-        CONFIGURING,
+        INITIALIZING,
         /**
-         * There's a recording waiting for being started.
-         *
-         * <p>The Recorder will reach this state whenever a recording can not be serviced
-         * immediately.
+         * The Recorder is being initialized and a recording is waiting for being run.
          */
         PENDING_RECORDING,
         /**
-         * There's a recording waiting for being paused.
-         *
-         * <p>The Recorder will reach this state whenever a recording can not be serviced
-         * immediately.
+         * The Recorder is being initialized and a recording is waiting for being paused.
          */
         PENDING_PAUSED,
         /**
@@ -243,7 +237,7 @@
      */
     private static final Set<State> VALID_NON_PENDING_STATES_WHILE_PENDING =
             Collections.unmodifiableSet(EnumSet.of(
-                    State.CONFIGURING, // Waiting for camera before starting recording.
+                    State.INITIALIZING, // Waiting for camera before starting recording.
                     State.IDLING, // Waiting for sequential executor to start pending recording.
                     State.RESETTING, // Waiting for camera/encoders to reset before starting.
                     State.STOPPING, // Waiting for previous recording to finalize before starting.
@@ -301,14 +295,12 @@
     private final EncoderFactory mVideoEncoderFactory;
     private final EncoderFactory mAudioEncoderFactory;
     private final Object mLock = new Object();
-    private final boolean mEncoderNotUsePersistentInputSurface = DeviceQuirks.get(
-            EncoderNotUsePersistentInputSurfaceQuirk.class) != null;
 
     ////////////////////////////////////////////////////////////////////////////////////////////////
     //                          Members only accessed when holding mLock                          //
     ////////////////////////////////////////////////////////////////////////////////////////////////
     @GuardedBy("mLock")
-    private State mState = State.CONFIGURING;
+    private State mState = State.INITIALIZING;
     // Tracks the underlying state when in a PENDING_* state. When not in a PENDING_* state, this
     // should be null.
     @GuardedBy("mLock")
@@ -332,6 +324,7 @@
     //                      Members only accessed on mSequentialExecutor                          //
     ////////////////////////////////////////////////////////////////////////////////////////////////
     private RecordingRecord mInProgressRecording = null;
+    private boolean mShouldWaitForNewSurface;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     boolean mInProgressRecordingStopping = false;
     private SurfaceRequest.TransformationInfo mSurfaceTransformationInfo = null;
@@ -343,7 +336,7 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     Integer mVideoTrackIndex = null;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    SurfaceRequest mLatestSurfaceRequest;
+    SurfaceRequest mSurfaceRequest;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     Surface mLatestSurface = null;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -399,8 +392,6 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     SourceState mSourceState = SourceState.INACTIVE;
     private ScheduledFuture<?> mSourceNonStreamingTimeout = null;
-    // The Recorder has to be reset first before being configured again.
-    private boolean mNeedsReset = false;
     //--------------------------------------------------------------------------------------------//
 
     Recorder(@Nullable Executor executor, @NonNull MediaSpec mediaSpec,
@@ -422,11 +413,39 @@
         synchronized (mLock) {
             Logger.d(TAG, "Surface is requested in state: " + mState + ", Current surface: "
                     + mStreamId);
-            if (mState == State.ERROR) {
-                setState(State.CONFIGURING);
+            switch (mState) {
+                case STOPPING:
+                    // Fall-through
+                case RESETTING:
+                    // Fall-through
+                case PENDING_RECORDING:
+                    // Fall-through
+                case PENDING_PAUSED:
+                    // Fall-through
+                case INITIALIZING:
+                    mSequentialExecutor.execute(
+                            () -> initializeInternal(mSurfaceRequest = request));
+                    break;
+                case IDLING:
+                    // Fall-through
+                case RECORDING:
+                    // Fall-through
+                case PAUSED:
+                    throw new IllegalStateException("Surface was requested when the Recorder had "
+                            + "been initialized with state " + mState);
+                case ERROR:
+                    Logger.w(TAG, "Surface was requested when the Recorder had encountered error.");
+                    setState(State.INITIALIZING);
+                    mSequentialExecutor.execute(() -> {
+                        if (mSurfaceRequest != null) {
+                            // If the surface request is already complete, this is a no-op.
+                            mSurfaceRequest.willNotProvideSurface();
+                        }
+                        initializeInternal(mSurfaceRequest = request);
+                    });
+                    break;
             }
         }
-        mSequentialExecutor.execute(() -> onSurfaceRequestedInternal(request));
     }
 
     /** @hide */
@@ -643,7 +662,7 @@
                     // Fall-through
                 case STOPPING:
                     // Fall-through
-                case CONFIGURING:
+                case INITIALIZING:
                     // Fall-through
                 case ERROR:
                     // Fall-through
@@ -668,12 +687,12 @@
                             setState(State.PENDING_RECORDING);
                             // Retry initialization.
                             mSequentialExecutor.execute(() -> {
-                                if (mLatestSurfaceRequest == null) {
+                                if (mSurfaceRequest == null) {
                                     throw new AssertionError(
                                             "surface request is required to retry "
                                                     + "initialization.");
                                 }
-                                configureInternal(mLatestSurfaceRequest);
+                                initializeInternal(mSurfaceRequest);
                             });
                         } else {
                             setState(State.PENDING_RECORDING);
@@ -721,7 +740,7 @@
                     // The recording will automatically pause once the initialization completes.
                     setState(State.PENDING_PAUSED);
                     break;
-                case CONFIGURING:
+                case INITIALIZING:
                     // Fall-through
                 case IDLING:
                     throw new IllegalStateException("Called pause() from invalid state: " + mState);
@@ -765,7 +784,7 @@
                     // The recording will automatically start once the initialization completes.
                     setState(State.PENDING_RECORDING);
                     break;
-                case CONFIGURING:
+                case INITIALIZING:
                     // Should not be able to resume when initializing. Should be in a PENDING state.
                     // Fall-through
                 case IDLING:
@@ -827,7 +846,7 @@
                     Preconditions.checkState(isSameRecording(activeRecording,
                             mActiveRecordingRecord));
                     break;
-                case CONFIGURING:
+                case INITIALIZING:
                     // Fall-through
                 case IDLING:
                     throw new IllegalStateException("Calling stop() while idling or initializing "
@@ -870,20 +889,6 @@
     }
 
     @ExecutedBy("mSequentialExecutor")
-    private void onSurfaceRequestedInternal(@NonNull SurfaceRequest request) {
-        if (mLatestSurfaceRequest != null && !mLatestSurfaceRequest.isServiced()) {
-            mLatestSurfaceRequest.willNotProvideSurface();
-        }
-        // Cache the surface request. If the Recorder has to be reset first, the Recorder will be
-        // configured after it's reset in the surface request complete callback. Otherwise,
-        // configure the Recorder directly.
-        mLatestSurfaceRequest = request;
-        if (!mNeedsReset) {
-            configureInternal(mLatestSurfaceRequest);
-        }
-    }
-
-    @ExecutedBy("mSequentialExecutor")
     void onSourceStateChangedInternal(@NonNull SourceState newState) {
         SourceState oldState = mSourceState;
         mSourceState = newState;
@@ -899,17 +904,12 @@
                 // If we're inactive and have no active surface, we'll reset the encoder directly.
                 // Otherwise, we'll wait for the active surface's surface request listener to
                 // reset the encoder.
-                requestReset(ERROR_SOURCE_INACTIVE, null);
-            } else {
-                // The source becomes inactive, the incoming new surface request has to be cached
-                // and be serviced after the Recorder is reset when receiving the previous
-                // surface request complete callback.
-                mNeedsReset = true;
-                if (mInProgressRecording != null) {
-                    // Stop any in progress recording with "source inactive" error
-                    onInProgressRecordingInternalError(mInProgressRecording, ERROR_SOURCE_INACTIVE,
-                            null);
-                }
+                reset(ERROR_SOURCE_INACTIVE, null);
+                setLatestSurface(null);
+            } else if (mInProgressRecording != null) {
+                // Stop any in progress recording with "source inactive" error
+                onInProgressRecordingInternalError(mInProgressRecording, ERROR_SOURCE_INACTIVE,
+                        null);
             }
         } else if (newState == SourceState.ACTIVE_NON_STREAMING) {
             // We are expecting the source to transition to NON_STREAMING state.
@@ -921,16 +921,16 @@
     }
 
     /**
-     * Requests the Recorder to be reset.
+     * Resets the state on the sequential executor for a new recording.
      *
      * <p>If a recording is in progress, it will be stopped asynchronously and reset once it has
      * been finalized.
      *
-     * <p>The Recorder is expected to be reset when there's no active surface. Otherwise, wait for
-     * the surface request complete callback first.
+     * <p>If there is a recording in progress, reset() will stop the recording and rely on the
+     * recording's onRecordingFinalized() to actually release resources.
      */
     @ExecutedBy("mSequentialExecutor")
-    void requestReset(@VideoRecordError int errorCode, @Nullable Throwable errorCause) {
+    void reset(@VideoRecordError int errorCode, @Nullable Throwable errorCause) {
         boolean shouldReset = false;
         boolean shouldStop = false;
         synchronized (mLock) {
@@ -945,8 +945,9 @@
                 case ERROR:
                     // Fall-through
                 case IDLING:
+                    setState(State.INITIALIZING);
                     // Fall-through
-                case CONFIGURING:
+                case INITIALIZING:
                     shouldReset = true;
                     break;
                 case PAUSED:
@@ -958,13 +959,14 @@
                     }
                     // If there's an active recording, stop it first then release the resources
                     // at onRecordingFinalized().
+                    setState(State.RESETTING);
                     shouldStop = true;
-                    // Fall-through
+                    break;
                 case STOPPING:
                     // Already stopping. Set state to RESETTING so resources will be released once
                     // onRecordingFinalized() runs.
                     setState(State.RESETTING);
-                    break;
+                    // Fall-through
                 case RESETTING:
                     // No-Op, the Recorder is already being reset.
                     break;
@@ -974,20 +976,20 @@
         // These calls must not be posted to the executor to ensure they are executed inline on
         // the sequential executor and the state changes above are correctly handled.
         if (shouldReset) {
-            reset();
+            resetInternal();
         } else if (shouldStop) {
             stopInternal(mInProgressRecording, null, errorCode, errorCause);
         }
     }
 
     @ExecutedBy("mSequentialExecutor")
-    private void configureInternal(@NonNull SurfaceRequest surfaceRequest) {
+    private void initializeInternal(@NonNull SurfaceRequest surfaceRequest) {
         if (mLatestSurface != null) {
             // There's a valid surface. Provide it directly.
             mActiveSurface = mLatestSurface;
             surfaceRequest.provideSurface(mLatestSurface, mSequentialExecutor,
                     this::onSurfaceRequestComplete);
-            onConfigured();
+            onInitialized();
         } else {
             surfaceRequest.setTransformationInfoListener(mSequentialExecutor,
                     (transformationInfo) -> mSurfaceTransformationInfo =
@@ -1012,7 +1014,7 @@
     }
 
     @ExecutedBy("mSequentialExecutor")
-    private void onConfigured() {
+    private void onInitialized() {
         RecordingRecord recordingToStart = null;
         RecordingRecord pendingRecordingToFinalize = null;
         @VideoRecordError int error = ERROR_NONE;
@@ -1028,19 +1030,21 @@
                     // Fall-through
                 case RESETTING:
                     throw new AssertionError(
-                            "Incorrectly invoke onConfigured() in state " + mState);
+                            "Incorrectly invoke onInitialized() in state " + mState);
                 case STOPPING:
-                    if (!mEncoderNotUsePersistentInputSurface) {
-                        throw new AssertionError("Unexpectedly invoke onConfigured() in a "
+                    if (mShouldWaitForNewSurface) {
+                        mShouldWaitForNewSurface = false;
+                    } else {
+                        throw new AssertionError("Unexpectedly invoke onInitialized() in a "
                                 + "STOPPING state when it's not waiting for a new surface.");
                     }
                     break;
-                case CONFIGURING:
+                case INITIALIZING:
                     setState(State.IDLING);
                     break;
                 case ERROR:
                     Logger.e(TAG,
-                            "onConfigured() was invoked when the Recorder had encountered error");
+                            "onInitialized() was invoked when the Recorder had encountered error");
                     break;
                 case PENDING_PAUSED:
                     startRecordingPaused = true;
@@ -1345,7 +1349,7 @@
                                 // Fall-through
                             case PENDING_PAUSED:
                                 // Fall-through
-                            case CONFIGURING:
+                            case INITIALIZING:
                                 // Fall-through
                             case STOPPING:
                                 // Fall-through
@@ -1380,7 +1384,7 @@
                 mActiveSurface = surface;
                 surfaceRequest.provideSurface(surface, mSequentialExecutor,
                         this::onSurfaceRequestComplete);
-                onConfigured();
+                onInitialized();
             } else {
                 // Encoder updates the surface while there's already an active surface.
                 // setLatestSurface() will update the StreamInfo with the new stream ID, which will
@@ -1419,7 +1423,8 @@
             mActiveSurface = null;
 
             if (needsReset) {
-                requestReset(ERROR_SOURCE_INACTIVE, null);
+                reset(ERROR_SOURCE_INACTIVE, null);
+                setLatestSurface(null);
             }
         } else {
             // If the surface isn't the active surface, it also can't be the latest surface
@@ -1438,7 +1443,7 @@
                     pendingRecordingToFinalize = mPendingRecordingRecord;
                     mPendingRecordingRecord = null;
                     // Fall-through
-                case CONFIGURING:
+                case INITIALIZING:
                     setStreamId(StreamInfo.STREAM_ID_ERROR);
                     setState(State.ERROR);
                     break;
@@ -1914,6 +1919,8 @@
             @Nullable Throwable errorCause) {
         // Only stop recording if recording is in-progress and it is not already stopping.
         if (mInProgressRecording == recordingToStop && !mInProgressRecordingStopping) {
+            mShouldWaitForNewSurface = DeviceQuirks.get(
+                    EncoderNotUsePersistentInputSurfaceQuirk.class) != null;
             mInProgressRecordingStopping = true;
             mRecordingStopError = stopError;
             mRecordingStopErrorCause = errorCause;
@@ -1984,7 +1991,7 @@
     }
 
     @ExecutedBy("mSequentialExecutor")
-    private void reset() {
+    private void resetInternal() {
         if (mAudioEncoder != null) {
             Logger.d(TAG, "Releasing audio encoder.");
             mAudioEncoder.release();
@@ -1996,7 +2003,6 @@
             mVideoEncoder.release();
             mVideoEncoder = null;
             mVideoOutputConfig = null;
-            setLatestSurface(null);
         }
         if (mAudioSource != null) {
             Logger.d(TAG, "Releasing audio source.");
@@ -2005,43 +2011,6 @@
         }
 
         setAudioState(AudioState.INITIALIZING);
-        onReset();
-    }
-
-    @ExecutedBy("mSequentialExecutor")
-    private void onReset() {
-        synchronized (mLock) {
-            switch (mState) {
-                case PENDING_PAUSED:
-                    // Fall-through
-                case PENDING_RECORDING:
-                    updateNonPendingState(State.CONFIGURING);
-                    break;
-                case ERROR:
-                    // Fall-through
-                case PAUSED:
-                    // Fall-through
-                case RECORDING:
-                    // Fall-through
-                case IDLING:
-                    // Fall-through
-                case RESETTING:
-                    // Fall-through
-                case STOPPING:
-                    setState(State.CONFIGURING);
-                    break;
-                case CONFIGURING:
-                    // No-op
-                    break;
-            }
-        }
-
-        mNeedsReset = false;
-
-        // If the latest surface request hasn't been serviced, use it to re-configure the Recorder.
-        if (mLatestSurfaceRequest != null && !mLatestSurfaceRequest.isServiced()) {
-            configureInternal(mLatestSurfaceRequest);
-        }
     }
 
     @ExecutedBy("mSequentialExecutor")
@@ -2168,7 +2137,6 @@
     private void onRecordingFinalized(@NonNull RecordingRecord finalizedRecording) {
         boolean needsReset = false;
         boolean startRecordingPaused = false;
-        boolean needsConfigure = false;
         RecordingRecord recordingToStart = null;
         RecordingRecord pendingRecordingToFinalize = null;
         @VideoRecordError int error = ERROR_NONE;
@@ -2182,11 +2150,8 @@
             mActiveRecordingRecord = null;
             switch (mState) {
                 case RESETTING:
-                    // If there's no active surface, reset the encoders. Otherwise, wait for the
-                    // surface request complete callback.
-                    if (mActiveSurface == null) {
-                        needsReset = true;
-                    }
+                    setState(State.INITIALIZING);
+                    needsReset = true;
                     break;
                 case PAUSED:
                     // Fall-through
@@ -2195,16 +2160,10 @@
                     // likely finalized due to an error.
                     // Fall-through
                 case STOPPING:
-                    if (mEncoderNotUsePersistentInputSurface) {
-                        // If the encoder doesn't use persistent input surface, the active
-                        // surface will become invalid after a recording is finalized. If there's
-                        // an unserviced surface request, configure with it directly, wait for a
-                        // surface update.
-                        mActiveSurface = null;
-                        if (mLatestSurfaceRequest != null && !mLatestSurfaceRequest.isServiced()) {
-                            needsConfigure = true;
-                        }
-                        setState(State.CONFIGURING);
+                    if (mShouldWaitForNewSurface) {
+                        // If the encoder doesn't use persistent input surface, reset the internal
+                        // state to INITIALIZING to wait for a surface update.
+                        setState(State.INITIALIZING);
                     } else {
                         setState(State.IDLING);
                     }
@@ -2216,13 +2175,13 @@
                     if (mSourceState == SourceState.INACTIVE) {
                         pendingRecordingToFinalize = mPendingRecordingRecord;
                         mPendingRecordingRecord = null;
-                        setState(State.CONFIGURING);
+                        setState(State.INITIALIZING);
                         error = ERROR_SOURCE_INACTIVE;
                         errorCause = PENDING_RECORDING_ERROR_CAUSE_SOURCE_INACTIVE;
-                    } else if (mEncoderNotUsePersistentInputSurface) {
+                    } else if (mShouldWaitForNewSurface) {
                         // If the encoder doesn't use persistent input surface, reset the
                         // non-pending state to INITIALIZING to wait for a surface update.
-                        updateNonPendingState(State.CONFIGURING);
+                        updateNonPendingState(State.INITIALIZING);
                     } else {
                         recordingToStart = makePendingRecordingActiveLocked(mState);
                     }
@@ -2230,10 +2189,8 @@
                 case ERROR:
                     // Error state is non-recoverable. Nothing to do here.
                     break;
-                case CONFIGURING:
-                    // No-op, the Recorder has been reset before the recording is finalized. So
-                    // keep the state in CONFIGURING.
-                    break;
+                case INITIALIZING:
+                    // Fall-through
                 case IDLING:
                     throw new AssertionError("Unexpected state on finalize of recording: "
                             + mState);
@@ -2241,14 +2198,12 @@
         }
 
         // Perform required actions from state changes inline on sequential executor but unlocked.
-        if (needsConfigure) {
-            configureInternal(mLatestSurfaceRequest);
-        } else if (needsReset) {
-            requestReset(error, errorCause);
+        if (needsReset) {
+            resetInternal();
         } else if (recordingToStart != null) {
             // A pending recording will only be started if we're not waiting for a new surface.
             // Otherwise the recording will be started after receiving a new surface request.
-            if (mEncoderNotUsePersistentInputSurface) {
+            if (mShouldWaitForNewSurface) {
                 throw new AssertionError("Attempt to start a pending recording while the Recorder"
                         + " is waiting for a new surface request.");
             }
@@ -2288,7 +2243,7 @@
                                 + " not the active recording.");
                     }
                     break;
-                case CONFIGURING:
+                case INITIALIZING:
                     // Fall-through
                 case IDLING:
                     // Fall-through
@@ -2316,10 +2271,9 @@
                     startRecordingPaused = true;
                     // Fall-through
                 case PENDING_RECORDING:
-                    if (mActiveRecordingRecord != null || mNeedsReset) {
-                        // Active recording is still finalizing or the Recorder is expected to be
-                        // reset. Pending recording will be serviced in onRecordingFinalized() or
-                        // in onReset().
+                    if (mActiveRecordingRecord != null) {
+                        // Active recording is still finalizing. Pending recording will be
+                        // serviced in onRecordingFinalized().
                         break;
                     }
                     if (mSourceState == SourceState.INACTIVE) {
@@ -2332,7 +2286,7 @@
                         recordingToStart = makePendingRecordingActiveLocked(mState);
                     }
                     break;
-                case CONFIGURING:
+                case INITIALIZING:
                     // Fall-through
                 case IDLING:
                     // Fall-through
diff --git a/camera/integration-tests/avsynctestapp/build.gradle b/camera/integration-tests/avsynctestapp/build.gradle
index a1188cb..410777d 100644
--- a/camera/integration-tests/avsynctestapp/build.gradle
+++ b/camera/integration-tests/avsynctestapp/build.gradle
@@ -49,11 +49,22 @@
     // Compose
     def compose_version = "1.1.1"
     kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
-    implementation 'androidx.activity:activity-compose:1.5.0-alpha03'
+    implementation("androidx.activity:activity-compose:1.5.0-alpha03")
     implementation("androidx.compose.material:material:$compose_version")
-    implementation("androidx.compose.runtime:runtime-livedata:$compose_version")
     implementation("androidx.compose.ui:ui:$compose_version")
     implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
+    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1")
 
     compileOnly(libs.kotlinCompiler)
+
+    // Testing framework
+    testImplementation(libs.kotlinCoroutinesTest)
+    testImplementation(libs.junit)
+    testImplementation(libs.truth)
+    androidTestImplementation(project(":camera:camera-testing"))
+    androidTestImplementation(libs.kotlinCoroutinesTest)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.truth)
 }
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
new file mode 100644
index 0000000..557c23a
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2022 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.
+ */
+
+/*
+ * Copyright 2022 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.camera.integration.avsync
+
+import android.content.Context
+import android.os.Build
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CameraXUtil
+import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStore
+import androidx.lifecycle.ViewModelStoreOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.rule.GrantPermissionRule
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class SignalGeneratorViewModelTest {
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private lateinit var viewModel: SignalGeneratorViewModel
+    private lateinit var lifecycleOwner: FakeLifecycleOwner
+    private val fakeViewModelStoreOwner = object : ViewModelStoreOwner {
+        private val viewModelStore = ViewModelStore()
+
+        override fun getViewModelStore() = viewModelStore
+
+        fun clear() {
+            viewModelStore.clear()
+        }
+    }
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+    )
+
+    @get:Rule
+    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
+        android.Manifest.permission.RECORD_AUDIO
+    )
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Before
+    fun setUp() = runTest {
+        // Skip for b/168175357, b/233661493
+        Assume.assumeFalse(
+            "Skip tests for Cuttlefish MediaCodec issues",
+            Build.MODEL.contains("Cuttlefish") &&
+                (Build.VERSION.SDK_INT == 29 || Build.VERSION.SDK_INT == 33)
+        )
+
+        val viewModelProvider = ViewModelProvider(fakeViewModelStoreOwner)
+        viewModel = viewModelProvider[SignalGeneratorViewModel::class.java]
+
+        withContext(Dispatchers.Main) {
+            lifecycleOwner = FakeLifecycleOwner()
+            lifecycleOwner.startAndResume()
+        }
+    }
+
+    @After
+    fun tearDown(): Unit = runBlocking {
+        fakeViewModelStoreOwner.clear()
+
+        if (::lifecycleOwner.isInitialized) {
+            withContext(Dispatchers.Main) {
+                lifecycleOwner.pauseAndStop()
+                lifecycleOwner.destroy()
+            }
+        }
+
+        // Ensure all cameras are released for the next test
+        CameraXUtil.shutdown()[10, TimeUnit.SECONDS]
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun initialRecorder_canMakeRecorderReady() = runTest {
+        viewModel.initialRecorder(context, lifecycleOwner)
+        advanceUntilIdle()
+
+        assertThat(viewModel.isRecorderReady).isTrue()
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun initialSignalGenerator_canMakeGeneratorReady() = runTest {
+        val beepFrequency = 1500
+        viewModel.initialSignalGenerator(beepFrequency)
+        advanceUntilIdle()
+
+        assertThat(viewModel.isGeneratorReady).isTrue()
+    }
+
+    @Test
+    fun startSignalGeneration_canMakeActiveFlagChangePeriodically(): Unit = runBlocking {
+        // Arrange.
+        val beepFrequency = 1500
+        val latch = CountDownLatch(5)
+
+        // Act.
+        viewModel.initialSignalGenerator(beepFrequency)
+        viewModel.startSignalGeneration()
+        countActiveFlagChangeBlocking(latch)
+
+        // Verify.
+        assertThat(latch.count).isEqualTo(0)
+    }
+
+    @Test
+    fun stopSignalGeneration_canMakeActiveFlagStopChanging(): Unit = runBlocking {
+        // Arrange.
+        val beepFrequency = 1500
+        val latch = CountDownLatch(5)
+
+        // Act.
+        viewModel.initialSignalGenerator(beepFrequency)
+        viewModel.startSignalGeneration()
+        viewModel.stopSignalGeneration()
+        countActiveFlagChangeBlocking(latch)
+
+        // Verify.
+        assertThat(latch.count).isNotEqualTo(0)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun startAndStopRecording_canWorkCorrectlyAfterRecorderReady() = runTest {
+        // Arrange.
+        viewModel.initialRecorder(context, lifecycleOwner)
+        advanceUntilIdle()
+
+        assertThat(viewModel.isRecorderReady).isTrue()
+
+        // Act. and Verify.
+        viewModel.startRecording(context)
+        assertThat(viewModel.isRecording).isTrue()
+
+        viewModel.stopRecording()
+        assertThat(viewModel.isRecording).isFalse()
+    }
+
+    private fun countActiveFlagChangeBlocking(latch: CountDownLatch, timeoutSec: Long = 5L) {
+        var preFlag = false
+        val endTimeMillis = System.currentTimeMillis() + timeoutSec * 1000
+        while (endTimeMillis > System.currentTimeMillis()) {
+            if (viewModel.isActivePeriod != preFlag) {
+                preFlag = viewModel.isActivePeriod
+                latch.countDown()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
new file mode 100644
index 0000000..74a5c35
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 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.camera.integration.avsync.model
+
+import android.media.AudioTrack
+import androidx.test.filters.LargeTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class AudioGeneratorDeviceTest {
+
+    private lateinit var audioGenerator: AudioGenerator
+
+    @Before
+    fun setUp() {
+        audioGenerator = AudioGenerator()
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initAudioTrack_throwExceptionWhenFrequencyNegative() = runTest {
+        audioGenerator.initAudioTrack(-5300, 11.0)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initAudioTrack_throwExceptionWhenLengthNegative() = runTest {
+        audioGenerator.initAudioTrack(5300, -11.0)
+    }
+
+    @Test
+    fun initAudioTrack_canWorkCorrectly() = runTest {
+        initialAudioTrack(5300, 11.0)
+    }
+
+    @Test
+    fun canStartAndStopAudioTrack_withoutExceptionAfterInitialized() = runBlocking {
+        // Arrange.
+        initialAudioTrack(5300, 11.0)
+
+        // Act. and Verify.
+        audioGenerator.start()
+        delay(1000)
+        assertThat(audioGenerator.audioTrack!!.playState).isEqualTo(AudioTrack.PLAYSTATE_PLAYING)
+        assertThat(audioGenerator.audioTrack!!.playbackHeadPosition).isGreaterThan(0)
+
+        audioGenerator.stop()
+        assertThat(audioGenerator.audioTrack!!.playState).isEqualTo(AudioTrack.PLAYSTATE_STOPPED)
+    }
+
+    private suspend fun initialAudioTrack(frequency: Int, beepLengthInSec: Double) {
+        val isInitialized = audioGenerator.initAudioTrack(frequency, beepLengthInSec)
+        assertThat(isInitialized).isTrue()
+        assertThat(audioGenerator.audioTrack!!.state).isEqualTo(AudioTrack.STATE_INITIALIZED)
+        assertThat(audioGenerator.audioTrack!!.playbackHeadPosition).isEqualTo(0)
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/MainActivity.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/MainActivity.kt
index d14126b..942eb56 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/MainActivity.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/MainActivity.kt
@@ -19,39 +19,39 @@
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
+
+private const val KEY_BEEP_FREQUENCY = "beep_frequency"
+private const val DEFAULT_BEEP_FREQUENCY = 1500
 
 class MainActivity : ComponentActivity() {
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContent {
-            MaterialTheme {
-                Surface(
-                    modifier = Modifier.fillMaxSize(),
-                    color = MaterialTheme.colors.background
-                ) {
-                    Greeting("CameraX")
-                }
+            App(getBeepFrequency())
+        }
+    }
+
+    private fun getBeepFrequency(): Int {
+        val frequency = intent.getStringExtra(KEY_BEEP_FREQUENCY)
+
+        if (frequency != null) {
+            try {
+                return Integer.parseInt(frequency)
+            } catch (e: NumberFormatException) {
+                e.printStackTrace()
             }
         }
+
+        return DEFAULT_BEEP_FREQUENCY
     }
 }
 
 @Composable
-fun Greeting(name: String) {
-    Text(text = "Hello $name!")
-}
-
-@Preview(showBackground = true)
-@Composable
-fun DefaultPreview() {
+fun App(beepFrequency: Int) {
     MaterialTheme {
-        Greeting("CameraX")
+        SignalGeneratorScreen(beepFrequency)
     }
 }
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
new file mode 100644
index 0000000..d30e6b8
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2022 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.camera.integration.avsync
+
+import androidx.camera.integration.avsync.ui.theme.LightOff
+import androidx.camera.integration.avsync.ui.theme.LightOn
+import androidx.camera.integration.avsync.ui.widget.AdvancedFloatingActionButton
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Icon
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+
+@Composable
+fun SignalGeneratorScreen(
+    beepFrequency: Int,
+    viewModel: SignalGeneratorViewModel = viewModel()
+) {
+    val context = LocalContext.current
+    val lifecycleOwner = LocalLifecycleOwner.current
+
+    LaunchedEffect(true) {
+        viewModel.initialRecorder(context, lifecycleOwner)
+        viewModel.initialSignalGenerator(beepFrequency)
+    }
+
+    MainContent(
+        isGeneratorReady = viewModel.isGeneratorReady,
+        isRecorderReady = viewModel.isRecorderReady,
+        isSignalActive = viewModel.isActivePeriod,
+        isSignalStarted = viewModel.isSignalGenerating,
+        isRecording = viewModel.isRecording,
+         viewModel.startSignalGeneration() },
+         viewModel.stopSignalGeneration() },
+         viewModel.startRecording(context) },
+         viewModel.stopRecording() },
+    )
+}
+
+@Composable
+private fun MainContent(
+    isGeneratorReady: Boolean = false,
+    isRecorderReady: Boolean = false,
+    isSignalActive: Boolean = false,
+    isSignalStarted: Boolean = false,
+    isRecording: Boolean = false,
+    onSignalStartClick: () -> Unit = {},
+    onSignalStopClick: () -> Unit = {},
+    onRecordingStartClick: () -> Unit = {},
+    onRecordingStopClick: () -> Unit = {},
+) {
+    Box(modifier = Modifier.fillMaxSize()) {
+        LightingScreen(isOn = isSignalActive)
+        SignalControl(
+            enabled = isGeneratorReady,
+            isStarted = isSignalStarted,
+            >
+            >
+        )
+        RecordingControl(
+            enabled = isRecorderReady,
+            isStarted = isRecording,
+            >
+            >
+        )
+    }
+}
+
+@Composable
+private fun LightingScreen(modifier: Modifier = Modifier, isOn: Boolean = false) {
+    val backgroundColor = if (isOn) LightOn else LightOff
+    Box(
+        modifier = modifier.fillMaxSize().background(color = backgroundColor)
+    )
+}
+
+@Composable
+private fun SignalControl(
+    modifier: Modifier = Modifier,
+    enabled: Boolean,
+    isStarted: Boolean,
+    onStartClick: () -> Unit = {},
+    onStopClick: () -> Unit = {},
+) {
+    val icon = if (isStarted) Icons.Filled.Close else Icons.Filled.PlayArrow
+
+    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+        AdvancedFloatingActionButton(
+            enabled = enabled,
+             (isStarted) onStopClick else onStartClick,
+            backgroundColor = Color.Cyan
+        ) {
+            Icon(icon, stringResource(R.string.desc_signal_control))
+        }
+    }
+}
+
+@Composable
+private fun RecordingControl(
+    modifier: Modifier = Modifier,
+    enabled: Boolean,
+    isStarted: Boolean,
+    onStartClick: () -> Unit = {},
+    onStopClick: () -> Unit = {},
+) {
+    val icon = painterResource(if (isStarted) R.drawable.ic_stop else R.drawable.ic_record)
+
+    Row(modifier = modifier.fillMaxSize()) {
+        Box(
+            modifier = modifier.weight(1f).fillMaxHeight(),
+            contentAlignment = Alignment.Center,
+        ) {
+            AdvancedFloatingActionButton(
+                enabled = enabled,
+                 (isStarted) onStopClick else onStartClick,
+                backgroundColor = Color.Cyan
+            ) {
+                Icon(icon, stringResource(R.string.desc_recording_control), modifier.size(16.dp))
+            }
+        }
+        Spacer(modifier = modifier.weight(1f).fillMaxHeight())
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun Preview_SignalGeneratorPage() {
+    MainContent()
+}
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
new file mode 100644
index 0000000..c0cf478
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2022 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.camera.integration.avsync
+
+import android.content.Context
+import androidx.camera.integration.avsync.model.AudioGenerator
+import androidx.camera.integration.avsync.model.CameraHelper
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.core.util.Preconditions
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.withContext
+
+private const val ACTIVE_LENGTH_SEC: Double = 0.5
+private const val ACTIVE_INTERVAL_SEC: Double = 1.0
+private const val ACTIVE_DELAY_SEC: Double = 0.0
+
+enum class ActivationSignal {
+    Active, Inactive
+}
+
+class SignalGeneratorViewModel : ViewModel() {
+
+    private var signalGenerationJob: Job? = null
+    private val audioGenerator = AudioGenerator()
+    private val cameraHelper = CameraHelper()
+
+    var isGeneratorReady: Boolean by mutableStateOf(false)
+        private set
+    var isRecorderReady: Boolean by mutableStateOf(false)
+        private set
+    var isSignalGenerating: Boolean by mutableStateOf(false)
+        private set
+    var isActivePeriod: Boolean by mutableStateOf(false)
+        private set
+    var isRecording: Boolean by mutableStateOf(false)
+        private set
+
+    suspend fun initialRecorder(context: Context, lifecycleOwner: LifecycleOwner) {
+        withContext(Dispatchers.Main) {
+            isRecorderReady = cameraHelper.bindCamera(context, lifecycleOwner)
+        }
+    }
+
+    suspend fun initialSignalGenerator(beepFrequency: Int) {
+        withContext(Dispatchers.Default) {
+            audioGenerator.initAudioTrack(
+                frequency = beepFrequency,
+                beepLengthInSec = ACTIVE_LENGTH_SEC,
+            )
+            isGeneratorReady = true
+        }
+    }
+
+    fun startSignalGeneration() {
+        Preconditions.checkState(isGeneratorReady)
+
+        signalGenerationJob?.cancel()
+
+        isSignalGenerating = true
+        signalGenerationJob = activationSignalFlow().map { activationSignal ->
+            when (activationSignal) {
+                ActivationSignal.Active -> {
+                    isActivePeriod = true
+                    playBeepSound()
+                }
+                ActivationSignal.Inactive -> {
+                    isActivePeriod = false
+                    stopBeepSound()
+                }
+            }
+        }.onCompletion {
+            stopBeepSound()
+            isActivePeriod = false
+        }.launchIn(viewModelScope)
+    }
+
+    fun stopSignalGeneration() {
+        Preconditions.checkState(isGeneratorReady)
+
+        isSignalGenerating = false
+        signalGenerationJob?.cancel()
+        signalGenerationJob = null
+    }
+
+    fun startRecording(context: Context) {
+        Preconditions.checkState(isRecorderReady)
+
+        cameraHelper.startRecording(context)
+        isRecording = true
+    }
+
+    fun stopRecording() {
+        Preconditions.checkState(isRecorderReady)
+
+        cameraHelper.stopRecording()
+        isRecording = false
+    }
+
+    private fun activationSignalFlow() = flow {
+        delay((ACTIVE_DELAY_SEC * 1000).toLong())
+        while (true) {
+            emit(ActivationSignal.Active)
+            delay((ACTIVE_LENGTH_SEC * 1000).toLong())
+            emit(ActivationSignal.Inactive)
+            delay((ACTIVE_INTERVAL_SEC * 1000).toLong())
+        }
+    }
+
+    private fun playBeepSound() {
+        audioGenerator.start()
+    }
+
+    private fun stopBeepSound() {
+        audioGenerator.stop()
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
new file mode 100644
index 0000000..80c3936
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2022 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.camera.integration.avsync.model
+
+import android.media.AudioAttributes
+import android.media.AudioFormat
+import android.media.AudioManager
+import android.media.AudioTrack
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.Logger
+import androidx.core.util.Preconditions.checkArgument
+import androidx.core.util.Preconditions.checkArgumentNonnegative
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlin.math.pow
+import kotlin.math.sin
+
+private const val TAG = "AudioGenerator"
+private const val SAMPLE_WIDTH: Int = 2
+private const val SAMPLE_RATE: Int = 44100
+private const val MAGNITUDE = 0.5
+private const val ENCODING: Int = AudioFormat.ENCODING_PCM_16BIT
+private const val CHANNEL = AudioFormat.CHANNEL_OUT_MONO
+
+class AudioGenerator {
+
+    @VisibleForTesting
+    var audioTrack: AudioTrack? = null
+
+    fun start() {
+        audioTrack!!.play()
+    }
+
+    fun stop() {
+        audioTrack!!.stop()
+    }
+
+    suspend fun initAudioTrack(
+        frequency: Int,
+        beepLengthInSec: Double,
+    ): Boolean {
+        checkArgumentNonnegative(frequency, "The input frequency should not be negative.")
+        checkArgument(beepLengthInSec >= 0, "The beep length should not be negative.")
+
+        Logger.i(TAG, "initAudioTrack with beep frequency: $frequency")
+
+        val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, SAMPLE_RATE)
+        val bufferSize = samples.size
+        val audioAttributes = AudioAttributes.Builder()
+            .setUsage(AudioAttributes.USAGE_MEDIA)
+            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+            .build()
+        val audioFormat = AudioFormat.Builder()
+            .setSampleRate(SAMPLE_RATE)
+            .setEncoding(ENCODING)
+            .setChannelMask(CHANNEL)
+            .build()
+
+        audioTrack = AudioTrack(
+            audioAttributes,
+            audioFormat,
+            bufferSize,
+            AudioTrack.MODE_STATIC,
+            AudioManager.AUDIO_SESSION_ID_GENERATE
+        )
+
+        audioTrack!!.write(samples, 0, samples.size)
+
+        return true
+    }
+
+    @VisibleForTesting
+    suspend fun generateSineSamples(
+        frequency: Int,
+        beepLengthInSec: Double,
+        sampleWidth: Int,
+        sampleRate: Int
+    ): ByteArray {
+        val waveData = generateSineData(frequency, beepLengthInSec, sampleRate)
+        val samples = toSamples(waveData, sampleWidth)
+
+        return samples.toByteArray()
+    }
+
+    /**
+     * magnitude is expected to be from 0 to 1
+     */
+    @VisibleForTesting
+    suspend fun generateSineData(
+        frequency: Int,
+        lengthInSec: Double,
+        sampleRate: Int,
+        magnitude: Double = MAGNITUDE
+    ): List<Double> = withContext(Dispatchers.Default) {
+        val n = (lengthInSec * sampleRate).toInt()
+        val angularFrequency = 2.0 * Math.PI * frequency
+
+        val res = mutableListOf<Double>()
+        for (i in 0 until n) {
+            val x = i * lengthInSec / n
+            val y = magnitude * sin(angularFrequency * x)
+            res.add(y)
+        }
+
+        res
+    }
+
+    @VisibleForTesting
+    suspend fun toSamples(
+        data: List<Double>,
+        sampleWidth: Int
+    ): List<Byte> = withContext(Dispatchers.Default) {
+        val scaleFactor = 2.toDouble().pow(8 * sampleWidth - 1) - 1
+
+        data.flatMap {
+            (it * scaleFactor).toInt().toBytes(sampleWidth)
+        }
+    }
+
+    @VisibleForTesting
+    fun Int.toBytes(sampleWidth: Int): List<Byte> {
+        val res = mutableListOf<Byte>()
+        for (i in 0 until sampleWidth) {
+            val byteValue = (this shr (8 * i)).toByte()
+            res.add(byteValue)
+        }
+
+        return res
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/CameraHelper.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/CameraHelper.kt
new file mode 100644
index 0000000..b066ef4
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/CameraHelper.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022 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.camera.integration.avsync.model
+
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.content.Context
+import android.provider.MediaStore
+import androidx.annotation.MainThread
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.Logger
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.Recorder
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoRecordEvent
+import androidx.concurrent.futures.await
+import androidx.core.content.ContextCompat
+import androidx.core.util.Consumer
+import androidx.lifecycle.LifecycleOwner
+
+private const val TAG = "CameraHelper"
+
+class CameraHelper {
+
+    private val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
+    private var videoCapture: VideoCapture<Recorder>? = null
+    private var activeRecording: Recording? = null
+
+    @MainThread
+    suspend fun bindCamera(context: Context, lifecycleOwner: LifecycleOwner): Boolean {
+        val cameraProvider = ProcessCameraProvider.getInstance(context).await()
+        val recorder = Recorder.Builder().build()
+        videoCapture = VideoCapture.withOutput(recorder)
+
+        return try {
+            cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, videoCapture)
+            true
+        } catch (exception: Exception) {
+            Logger.e(TAG, "Camera binding failed", exception)
+            videoCapture = null
+            false
+        }
+    }
+
+    /**
+     * Start video recording.
+     *
+     * <p> For E2E test, permissions will be handled by the launch script.
+     */
+    @SuppressLint("MissingPermission")
+    fun startRecording(context: Context, eventListener: Consumer<VideoRecordEvent>? = null) {
+        activeRecording = videoCapture!!.let {
+            val pendingRecording = it.output.prepareRecording(
+                context,
+                generateVideoMediaStoreOptions(context.contentResolver)
+            )
+
+            val listener = eventListener ?: generateVideoRecordEventListener()
+            pendingRecording.withAudioEnabled().start(
+                ContextCompat.getMainExecutor(context),
+                listener
+            )
+        }
+    }
+
+    fun stopRecording() {
+        activeRecording!!.stop()
+        activeRecording = null
+    }
+
+    private fun generateVideoMediaStoreOptions(
+        contentResolver: ContentResolver
+    ): MediaStoreOutputOptions {
+        val contentValues = generateVideoContentValues(generateVideoFileName())
+
+        return MediaStoreOutputOptions.Builder(
+            contentResolver,
+            MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+        ).setContentValues(contentValues).build()
+    }
+
+    private fun generateVideoFileName(): String {
+        return "video_" + System.currentTimeMillis()
+    }
+
+    private fun generateVideoContentValues(fileName: String): ContentValues {
+        val res = ContentValues()
+        res.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+        res.put(MediaStore.Video.Media.TITLE, fileName)
+        res.put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
+        res.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
+        res.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis())
+
+        return res
+    }
+
+    private fun generateVideoRecordEventListener(): Consumer<VideoRecordEvent> {
+        return Consumer<VideoRecordEvent> { videoRecordEvent ->
+            if (videoRecordEvent is VideoRecordEvent.Finalize) {
+                val uri = videoRecordEvent.outputResults.outputUri
+                if (videoRecordEvent.error == VideoRecordEvent.Finalize.ERROR_NONE) {
+                    Logger.d(TAG, "Video saved to: $uri")
+                } else {
+                    val msg = "save to uri $uri with error code (${videoRecordEvent.error})"
+                    Logger.e(TAG, "Failed to save video: $msg")
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/BluetoothXTestAppActivity.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/theme/Color.kt
similarity index 63%
rename from bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/BluetoothXTestAppActivity.kt
rename to camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/theme/Color.kt
index a272ef2..b3ca452 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/BluetoothXTestAppActivity.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/theme/Color.kt
@@ -14,19 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.bluetooth.integration.testapp
+package androidx.camera.integration.avsync.ui.theme
 
-import android.app.Activity
-import android.os.Bundle
+import androidx.compose.ui.graphics.Color
 
-/**
- * Simple activity.
- */
-class BluetoothXTestAppActivity : Activity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        setContentView(R.layout.activity_bluetoothx_test_app)
-    }
-}
+val LightOn = Color.White
+val LightOff = Color.Black
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt
new file mode 100644
index 0000000..0189901
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2022 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.camera.integration.avsync.ui.widget
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.FloatingActionButtonDefaults
+import androidx.compose.material.FloatingActionButtonElevation
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.contentColorFor
+import androidx.compose.material.ripple.LocalRippleTheme
+import androidx.compose.material.ripple.RippleAlpha
+import androidx.compose.material.ripple.RippleTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+
+@Composable
+fun AdvancedFloatingActionButton(
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    onClick: () -> Unit,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
+    backgroundColor: Color = MaterialTheme.colors.secondary,
+    contentColor: Color = contentColorFor(backgroundColor),
+    elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
+    content: @Composable () -> Unit
+) {
+    val rippleTheme = if (enabled) LocalRippleTheme.current else DisabledRippleTheme
+
+    CompositionLocalProvider(LocalRippleTheme provides rippleTheme) {
+        FloatingActionButton(
+             (enabled) onClick else { {} },
+            modifier = modifier,
+            interactionSource = interactionSource,
+            shape = shape,
+            backgroundColor = if (enabled) backgroundColor else Color.Gray,
+            contentColor = contentColor,
+            elevation = elevation,
+            content = content
+        )
+    }
+}
+
+private object DisabledRippleTheme : RippleTheme {
+
+    @Composable
+    override fun defaultColor(): Color = Color.Transparent
+
+    @Composable
+    override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f)
+}
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_play.xml b/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_play.xml
new file mode 100644
index 0000000..0870be8
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_play.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
+</vector>
diff --git a/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_record.xml b/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_record.xml
new file mode 100644
index 0000000..7a9e47b
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_record.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2z"/>
+</vector>
diff --git a/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_stop.xml b/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_stop.xml
new file mode 100644
index 0000000..6f6d3ac
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_stop.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M3,3h18v18h-18z"/>
+</vector>
diff --git a/camera/integration-tests/avsynctestapp/src/main/res/values/strings.xml b/camera/integration-tests/avsynctestapp/src/main/res/values/strings.xml
index 345f803..dca7f0e 100644
--- a/camera/integration-tests/avsynctestapp/src/main/res/values/strings.xml
+++ b/camera/integration-tests/avsynctestapp/src/main/res/values/strings.xml
@@ -16,4 +16,6 @@
 
 <resources>
     <string name="app_name">AV sync test app</string>
+    <string name="desc_signal_control">Signal Control</string>
+    <string name="desc_recording_control">Recording Control</string>
 </resources>
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/test/java/androidx/camera/integration/avsync/model/AudioGeneratorTest.kt b/camera/integration-tests/avsynctestapp/src/test/java/androidx/camera/integration/avsync/model/AudioGeneratorTest.kt
new file mode 100644
index 0000000..b017a55
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/test/java/androidx/camera/integration/avsync/model/AudioGeneratorTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2022 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.camera.integration.avsync.model
+
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val FLOAT_ERROR = 1E-4
+
+@RunWith(JUnit4::class)
+class AudioGeneratorTest {
+
+    private lateinit var audioGenerator: AudioGenerator
+
+    @Before
+    fun setUp() {
+        audioGenerator = AudioGenerator()
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun generateSineSamples_canGenerateCorrectResult() = runTest {
+        val res = audioGenerator.generateSineSamples(1, 1.0, 2, 8)
+        val expected = listOf(
+            0x00.toByte(), 0x00.toByte(),
+            0x40.toByte(), 0x2D.toByte(),
+            0xFF.toByte(), 0x3F.toByte(),
+            0x40.toByte(), 0x2D.toByte(),
+            0x00.toByte(), 0x00.toByte(),
+            0xC0.toByte(), 0xD2.toByte(),
+            0x01.toByte(), 0xC0.toByte(),
+            0xC0.toByte(), 0xD2.toByte(),
+        ).toByteArray()
+
+        assertThat(res).isEqualTo(expected)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun generateSineData_canGenerateCorrectResult() = runTest {
+        val res = audioGenerator.generateSineData(1, 1.0, 8, 0.5)
+        val expected = listOf(0.0, 0.3535, 0.5, 0.3535, 0.0, -0.3535, -0.5, -0.3535)
+
+        assertThat(res.size).isEqualTo(expected.size)
+        for (index in res.indices) {
+            assertThat(res[index]).isWithin(FLOAT_ERROR).of(expected[index])
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun generateSineData_allZeroWhenFrequencyZero() = runTest {
+        val res = audioGenerator.generateSineData(0, 11.0, 10, 0.5)
+
+        assertThat(res.size).isEqualTo(110)
+        res.forEach { value ->
+            assertThat(value).isZero()
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun generateSineData_emptyWhenLengthZero() = runTest {
+        val res = audioGenerator.generateSineData(53, 0.0, 10, 0.5)
+        val expected = listOf<Double>()
+
+        assertThat(res).isEqualTo(expected)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun generateSineData_emptyWhenSampleRateZero() = runTest {
+        val res = audioGenerator.generateSineData(53, 11.0, 0, 0.5)
+        val expected = listOf<Double>()
+
+        assertThat(res).isEqualTo(expected)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun generateSineData_allZeroWhenMagnitudeZero() = runTest {
+        val res = audioGenerator.generateSineData(53, 11.0, 10, 0.0)
+
+        assertThat(res.size).isEqualTo(110)
+        res.forEach { value ->
+            assertThat(value).isZero()
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun toSamples_canGenerateCorrectResult() = runTest {
+        val input = listOf(-0.1, 0.2, -0.3, 0.5, -0.75, 0.9, -1.0)
+        val expected = listOf(
+            0x34.toByte(), 0xF3.toByte(),
+            0x99.toByte(), 0x19.toByte(),
+            0x9A.toByte(), 0xD9.toByte(),
+            0xFF.toByte(), 0x3F.toByte(),
+            0x01.toByte(), 0xA0.toByte(),
+            0x32.toByte(), 0x73.toByte(),
+            0x01.toByte(), 0x80.toByte(),
+        )
+
+        assertThat(audioGenerator.toSamples(input, 2)).isEqualTo(expected)
+    }
+
+    @Test
+    fun toBytes_canGenerateCorrectResult() {
+        val expected = listOf(0xBF.toByte(), 0x14.toByte())
+
+        audioGenerator.run {
+            assertThat(5311.toBytes(2)).isEqualTo(expected)
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 2897ffd..83d4568 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -78,7 +78,6 @@
 import androidx.test.filters.SdkSuppress
 import androidx.test.rule.GrantPermissionRule
 import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
 import java.io.ByteArrayInputStream
 import java.io.File
 import java.io.FileOutputStream
@@ -87,12 +86,10 @@
 import java.util.concurrent.atomic.AtomicInteger
 import kotlin.math.abs
 import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
-import kotlinx.coroutines.withTimeoutOrNull
 import org.junit.After
 import org.junit.Assume.assumeFalse
 import org.junit.Assume.assumeNotNull
@@ -1597,47 +1594,6 @@
         simpleCaptureProcessor.close()
     }
 
-    @Test
-    fun unbindPreview_imageCapturingShouldSuccess() = runBlocking {
-        skipTestOnCameraPipeConfig()
-
-        // Arrange.
-        val imageCapture = ImageCapture.Builder().build()
-        val previewStreamReceived = CompletableDeferred<Boolean>()
-        val preview = Preview.Builder().also {
-            Camera2Interop.Extender(it).setSessionCaptureCallback(
-                object : CaptureCallback() {
-                    override fun onCaptureCompleted(
-                        session: CameraCaptureSession,
-                        request: CaptureRequest,
-                        result: TotalCaptureResult
-                    ) {
-                        previewStreamReceived.complete(true)
-                    }
-                }
-            )
-        }.build()
-        withContext(Dispatchers.Main) {
-            preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
-            cameraProvider.bindToLifecycle(
-                fakeLifecycleOwner, BACK_SELECTOR, imageCapture, preview)
-        }
-        assertWithMessage("Preview doesn't start").that(
-            previewStreamReceived.awaitWithTimeoutOrNull()
-        ).isTrue()
-
-        // Act.
-        val callback = FakeImageCaptureCallback(capturesCount = 1)
-        withContext(Dispatchers.Main) {
-            // Test the reproduce step in b/235119898
-            cameraProvider.unbind(preview)
-            imageCapture.takePicture(mainExecutor, callback)
-        }
-
-        // Assert.
-        callback.awaitCapturesAndAssert(capturedImagesCount = 1)
-    }
-
     private fun createNonRotatedConfiguration(): ImageCaptureConfig {
         // Create a configuration with target rotation that matches the sensor rotation.
         // This assumes a back-facing camera (facing away from screen)
@@ -1813,12 +1769,4 @@
             deferredItems.forEach { it.await() }
         }
     }
-
-    private suspend fun <T> Deferred<T>.awaitWithTimeoutOrNull(
-        timeMillis: Long = TimeUnit.SECONDS.toMillis(5)
-    ): T? {
-        return withTimeoutOrNull(timeMillis) {
-            await()
-        }
-    }
 }
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
index 0e9daa6..7b6f208 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
@@ -198,7 +198,7 @@
     @Test
     fun returnCaptureStages_whenCaptureProcessorIsNotNull(): Unit = runBlocking {
         // Clear the device UI and check if there is no dialog or lock screen on the top of the
-        // window before start the test.
+        // window before starting the test.
         CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
 
         val intent = ApplicationProvider.getApplicationContext<Context>().packageManager
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
index a483822..4b52df4 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
@@ -73,7 +73,7 @@
     fun setUp() {
         assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
         // Clear the device UI and check if there is no dialog or lock screen on the top of the
-        // window before start the test.
+        // window before starting the test.
         CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
     }
 
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
new file mode 100644
index 0000000..b79ca60
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2021 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.camera.integration.extensions
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.extensions.ExtensionMode
+import androidx.camera.integration.extensions.util.ExtensionsTestUtil
+import androidx.camera.integration.extensions.util.ExtensionsTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CameraUtil.PreTestCameraIdList
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.waitForIdle
+import androidx.lifecycle.Lifecycle.State.CREATED
+import androidx.lifecycle.Lifecycle.State.RESUMED
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.idling.CountingIdlingResource
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.RepeatRule
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.extensions"
+
+/**
+ * Stress tests to verify that Preview and ImageCapture can work well when changing lifecycle
+ * status.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+class LifecycleStatusChangeStressTest(
+    private val cameraId: String,
+    private val extensionMode: Int
+) {
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        PreTestCameraIdList(Camera2Config.defaultConfig())
+    )
+
+    @get:Rule
+    val repeatRule = RepeatRule()
+
+    @get:Rule
+    val storagePermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)!!
+
+    private lateinit var activityScenario: ActivityScenario<CameraExtensionsActivity>
+
+    private lateinit var initializationIdlingResource: CountingIdlingResource
+    private lateinit var previewViewIdlingResource: CountingIdlingResource
+    private lateinit var takePictureIdlingResource: CountingIdlingResource
+
+    companion object {
+        @Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
+        @JvmStatic
+        fun parameters() = ExtensionsTestUtil.getAllCameraIdModeCombinations()
+    }
+
+    @Before
+    fun setUp() {
+        if (extensionMode != ExtensionMode.NONE) {
+            assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
+        }
+        // Clear the device UI and check if there is no dialog or lock screen on the top of the
+        // window before starting the test.
+        CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+    }
+
+    @After
+    fun tearDown() {
+        if (::activityScenario.isInitialized) {
+            activityScenario.onActivity { it.finish() }
+        }
+    }
+
+    @Test
+    @RepeatRule.Repeat(times = ExtensionsTestUtil.STRESS_TEST_REPEAT_COUNT)
+    fun pauseResumeActivityTenTimes_canCaptureImageInEachTime() {
+        launchActivityAndRetrieveIdlingResources()
+
+        for (i in 1..STRESS_TEST_OPERATION_REPEAT_COUNT) {
+            // Issues take picture.
+            Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(ViewActions.click())
+
+            // Waits for the take picture success callback.
+            takePictureIdlingResource.waitForIdle()
+
+            previewViewIdlingResource.increment()
+
+            // Pauses and resumes activity
+            activityScenario.moveToState(CREATED)
+            activityScenario.moveToState(RESUMED)
+            previewViewIdlingResource.waitForIdle()
+        }
+    }
+
+    @Test
+    @RepeatRule.Repeat(times = ExtensionsTestUtil.STRESS_TEST_REPEAT_COUNT)
+    fun canCaptureImage_afterPauseResumeActivityTenTimes() {
+        launchActivityAndRetrieveIdlingResources()
+
+        // Pauses and resumes activity 10 times
+        for (i in 1..STRESS_TEST_OPERATION_REPEAT_COUNT) {
+            activityScenario.moveToState(CREATED)
+            activityScenario.moveToState(RESUMED)
+        }
+
+        // Presses capture button
+        Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(ViewActions.click())
+
+        // Waits for the take picture success callback.
+        takePictureIdlingResource.waitForIdle()
+    }
+
+    private fun launchActivityAndRetrieveIdlingResources() {
+        val intent = ApplicationProvider.getApplicationContext<Context>().packageManager
+            .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE)?.apply {
+                putExtra(CameraExtensionsActivity.INTENT_EXTRA_CAMERA_ID, cameraId)
+                putExtra(CameraExtensionsActivity.INTENT_EXTRA_EXTENSION_MODE, extensionMode)
+                putExtra(CameraExtensionsActivity.INTENT_EXTRA_DELETE_CAPTURED_IMAGE, true)
+                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+            }
+
+        activityScenario = ActivityScenario.launch(intent)
+
+        activityScenario.onActivity {
+            initializationIdlingResource = it.mInitializationIdlingResource
+            previewViewIdlingResource = it.mPreviewViewIdlingResource
+            takePictureIdlingResource = it.mTakePictureIdlingResource
+        }
+
+        // Waits for CameraExtensionsActivity's initialization to be complete
+        initializationIdlingResource.waitForIdle()
+
+        activityScenario.onActivity {
+            assumeTrue(it.isExtensionModeSupported(cameraId, extensionMode))
+        }
+
+        // Waits for preview view turned to STREAMING state to make sure that the capture session
+        // has been created and the capture stages can be retrieved from the vendor library
+        // successfully.
+        previewViewIdlingResource.waitForIdle()
+
+        activityScenario.onActivity {
+            // Checks that CameraExtensionsActivity's current extension mode is correct.
+            assertThat(it.currentExtensionMode).isEqualTo(extensionMode)
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewTest.kt
index 504c0e2..7460b90 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewTest.kt
@@ -70,7 +70,7 @@
     fun setUp() {
         assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
         // Clear the device UI and check if there is no dialog or lock screen on the top of the
-        // window before start the test.
+        // window before starting the test.
         CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
     }
 
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
new file mode 100644
index 0000000..b9641d1
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2021 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.camera.integration.extensions
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.extensions.ExtensionMode
+import androidx.camera.integration.extensions.util.ExtensionsTestUtil
+import androidx.camera.integration.extensions.util.ExtensionsTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CameraUtil.PreTestCameraIdList
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.waitForIdle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.idling.CountingIdlingResource
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.RepeatRule
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.extensions"
+
+/**
+ * Stress tests to verify that Preview and ImageCapture can work well when switching modes.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+class SwitchAvailableModesStressTest(private val cameraId: String) {
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        PreTestCameraIdList(Camera2Config.defaultConfig())
+    )
+
+    @get:Rule
+    val repeatRule = RepeatRule()
+
+    @get:Rule
+    val storagePermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)!!
+
+    private lateinit var activityScenario: ActivityScenario<CameraExtensionsActivity>
+
+    private lateinit var initializationIdlingResource: CountingIdlingResource
+    private lateinit var previewViewIdlingResource: CountingIdlingResource
+    private lateinit var takePictureIdlingResource: CountingIdlingResource
+
+    companion object {
+        @Parameterized.Parameters(name = "cameraId = {0}")
+        @JvmStatic
+        fun parameters() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+    }
+
+    @Before
+    fun setUp() {
+        assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
+        // Clear the device UI and check if there is no dialog or lock screen on the top of the
+        // window before starting the test.
+        CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+    }
+
+    @After
+    fun tearDown() {
+        if (::activityScenario.isInitialized) {
+            activityScenario.onActivity { it.finish() }
+        }
+    }
+
+    @Test
+    @RepeatRule.Repeat(times = ExtensionsTestUtil.STRESS_TEST_REPEAT_COUNT)
+    fun switchModeTenTimes_canCaptureImageInEachTime() {
+        launchActivityAndRetrieveIdlingResources()
+
+        for (i in 1..STRESS_TEST_OPERATION_REPEAT_COUNT) {
+            // Issues take picture.
+            Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(ViewActions.click())
+
+            // Waits for the take picture success callback.
+            takePictureIdlingResource.waitForIdle()
+
+            previewViewIdlingResource.increment()
+
+            // Switches to next available mode
+            Espresso.onView(ViewMatchers.withId(R.id.PhotoToggle)).perform(ViewActions.click())
+
+            // Waits for preview view turned to STREAMING state after switching camera
+            previewViewIdlingResource.waitForIdle()
+        }
+    }
+
+    @Test
+    @RepeatRule.Repeat(times = ExtensionsTestUtil.STRESS_TEST_REPEAT_COUNT)
+    fun canCaptureImage_afterSwitchModeTenTimes() {
+        launchActivityAndRetrieveIdlingResources()
+
+        // Switches to next available mode ten times
+        for (i in 1..STRESS_TEST_OPERATION_REPEAT_COUNT) {
+            Espresso.onView(ViewMatchers.withId(R.id.PhotoToggle)).perform(ViewActions.click())
+        }
+
+        // Issues take picture.
+        Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(ViewActions.click())
+
+        // Waits for the take picture success callback.
+        takePictureIdlingResource.waitForIdle()
+    }
+
+    private fun launchActivityAndRetrieveIdlingResources() {
+        val intent = ApplicationProvider.getApplicationContext<Context>().packageManager
+            .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE)?.apply {
+                putExtra(CameraExtensionsActivity.INTENT_EXTRA_CAMERA_ID, cameraId)
+                putExtra(CameraExtensionsActivity.INTENT_EXTRA_EXTENSION_MODE, ExtensionMode.NONE)
+                putExtra(CameraExtensionsActivity.INTENT_EXTRA_DELETE_CAPTURED_IMAGE, true)
+                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+            }
+
+        activityScenario = ActivityScenario.launch(intent)
+
+        activityScenario.onActivity {
+            initializationIdlingResource = it.mInitializationIdlingResource
+            previewViewIdlingResource = it.mPreviewViewIdlingResource
+            takePictureIdlingResource = it.mTakePictureIdlingResource
+        }
+
+        // Waits for CameraExtensionsActivity's initialization to be complete
+        initializationIdlingResource.waitForIdle()
+
+        // Waits for preview view turned to STREAMING state to make sure that the capture session
+        // has been created and the capture stages can be retrieved from the vendor library
+        // successfully.
+        previewViewIdlingResource.waitForIdle()
+    }
+}
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
new file mode 100644
index 0000000..df4da22
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2021 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.camera.integration.extensions
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraSelector
+import androidx.camera.extensions.ExtensionMode
+import androidx.camera.integration.extensions.util.ExtensionsTestUtil
+import androidx.camera.integration.extensions.util.ExtensionsTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CameraUtil.PreTestCameraIdList
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.waitForIdle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.idling.CountingIdlingResource
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.RepeatRule
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.extensions"
+
+/**
+ * Stress tests to verify that Preview and ImageCapture can work well when switching cameras.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+class SwitchCameraStressTest(private val extensionMode: Int) {
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        PreTestCameraIdList(Camera2Config.defaultConfig())
+    )
+
+    @get:Rule
+    val repeatRule = RepeatRule()
+
+    @get:Rule
+    val storagePermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)!!
+
+    private lateinit var activityScenario: ActivityScenario<CameraExtensionsActivity>
+
+    private lateinit var initializationIdlingResource: CountingIdlingResource
+    private lateinit var previewViewIdlingResource: CountingIdlingResource
+    private lateinit var takePictureIdlingResource: CountingIdlingResource
+
+    companion object {
+        @Parameterized.Parameters(name = "extensionMode = {0}")
+        @JvmStatic
+        fun parameters() = arrayOf(
+            ExtensionMode.NONE,
+            ExtensionMode.BOKEH,
+            ExtensionMode.HDR,
+            ExtensionMode.NIGHT,
+            ExtensionMode.FACE_RETOUCH,
+            ExtensionMode.AUTO
+        )
+    }
+
+    @Before
+    fun setUp() {
+        if (extensionMode != ExtensionMode.NONE) {
+            assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
+        }
+        // Clear the device UI and check if there is no dialog or lock screen on the top of the
+        // window before starting the test.
+        CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+    }
+
+    @After
+    fun tearDown() {
+        if (::activityScenario.isInitialized) {
+            activityScenario.onActivity { it.finish() }
+        }
+    }
+
+    @Test
+    @RepeatRule.Repeat(times = ExtensionsTestUtil.STRESS_TEST_REPEAT_COUNT)
+    fun switchCameraTenTimes_canCaptureImageInEachTime() {
+        launchActivityAndRetrieveIdlingResources()
+
+        for (i in 1..STRESS_TEST_OPERATION_REPEAT_COUNT) {
+            // Issues take picture.
+            Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(ViewActions.click())
+
+            // Waits for the take picture success callback.
+            takePictureIdlingResource.waitForIdle()
+
+            previewViewIdlingResource.increment()
+
+            activityScenario.onActivity {
+                // Switches camera
+                it.switchCameras()
+                // Switches to the target testing extension mode as possible because some extension
+                // modes may not be supported in some lens facing of cameras.
+                it.switchToExtensionMode(extensionMode)
+            }
+
+            // Waits for preview view turned to STREAMING state after switching camera
+            previewViewIdlingResource.waitForIdle()
+        }
+    }
+
+    @Test
+    @RepeatRule.Repeat(times = ExtensionsTestUtil.STRESS_TEST_REPEAT_COUNT)
+    fun canCaptureImage_afterSwitchCameraTenTimes() {
+        launchActivityAndRetrieveIdlingResources()
+
+        // Pauses and resumes activity 10 times
+        for (i in 1..STRESS_TEST_OPERATION_REPEAT_COUNT) {
+            activityScenario.onActivity {
+                // Switches camera
+                it.switchCameras()
+                // Switches to the target testing extension mode as possible because some extension
+                // modes may not be supported in some lens facing of cameras.
+                it.switchToExtensionMode(extensionMode)
+            }
+        }
+
+        // Presses capture button
+        Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(ViewActions.click())
+
+        // Waits for the take picture success callback.
+        takePictureIdlingResource.waitForIdle()
+    }
+
+    private fun launchActivityAndRetrieveIdlingResources() {
+        val intent = ApplicationProvider.getApplicationContext<Context>().packageManager
+            .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE)?.apply {
+                putExtra(CameraExtensionsActivity.INTENT_EXTRA_CAMERA_ID, "0")
+                putExtra(CameraExtensionsActivity.INTENT_EXTRA_EXTENSION_MODE, extensionMode)
+                putExtra(CameraExtensionsActivity.INTENT_EXTRA_DELETE_CAPTURED_IMAGE, true)
+                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+            }
+
+        activityScenario = ActivityScenario.launch(intent)
+
+        activityScenario.onActivity {
+            initializationIdlingResource = it.mInitializationIdlingResource
+            previewViewIdlingResource = it.mPreviewViewIdlingResource
+            takePictureIdlingResource = it.mTakePictureIdlingResource
+        }
+
+        // Waits for CameraExtensionsActivity's initialization to be complete
+        initializationIdlingResource.waitForIdle()
+
+        // Only runs the test when at least one of the back or front cameras support the target
+        // testing extension mode
+        activityScenario.onActivity {
+            assumeTrue(
+                it.isExtensionModeSupported(CameraSelector.DEFAULT_BACK_CAMERA, extensionMode) ||
+                    it.isExtensionModeSupported(CameraSelector.DEFAULT_FRONT_CAMERA, extensionMode)
+            )
+        }
+
+        // Waits for preview view turned to STREAMING state to make sure that the capture session
+        // has been created and the capture stages can be retrieved from the vendor library
+        // successfully.
+        previewViewIdlingResource.waitForIdle()
+    }
+}
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
index f6ca2a9..58ba22f 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -149,7 +149,25 @@
         mCameraProvider.unbindAll();
         mCurrentCameraSelector = (mCurrentCameraSelector == CameraSelector.DEFAULT_BACK_CAMERA)
                 ? CameraSelector.DEFAULT_FRONT_CAMERA : CameraSelector.DEFAULT_BACK_CAMERA;
+        if (!bindUseCasesWithCurrentExtensionMode()) {
+            bindUseCasesWithNextExtensionMode();
+        }
+    }
+
+    @VisibleForTesting
+    boolean switchToExtensionMode(@ExtensionMode.Mode int extensionMode) {
+        if (mCamera == null || mExtensionsManager == null) {
+            return false;
+        }
+
+        if (!mExtensionsManager.isExtensionAvailable(mCurrentCameraSelector, extensionMode)) {
+            return false;
+        }
+
+        mCurrentExtensionMode = extensionMode;
         bindUseCasesWithCurrentExtensionMode();
+
+        return true;
     }
 
     void bindUseCasesWithNextExtensionMode() {
@@ -575,6 +593,12 @@
     }
 
     @VisibleForTesting
+    boolean isExtensionModeSupported(@NonNull CameraSelector cameraSelector,
+            @ExtensionMode.Mode int mode) {
+        return mExtensionsManager.isExtensionAvailable(cameraSelector, mode);
+    }
+
+    @VisibleForTesting
     @ExtensionMode.Mode
     int getCurrentExtensionMode() {
         return mCurrentExtensionMode;
diff --git a/camera/integration-tests/uiwidgetstestapp/build.gradle b/camera/integration-tests/uiwidgetstestapp/build.gradle
index 57c2ba8..4879ddf 100644
--- a/camera/integration-tests/uiwidgetstestapp/build.gradle
+++ b/camera/integration-tests/uiwidgetstestapp/build.gradle
@@ -90,6 +90,8 @@
     implementation 'androidx.compose.animation:animation:1.1.1'
     implementation 'androidx.compose.ui:ui-tooling:1.1.1'
     implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1'
+    implementation 'androidx.navigation:navigation-compose:2.4.2'
+    implementation 'androidx.compose.material:material-icons-extended:1.1.1'
     androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.1.1'
 
     // Testing framework
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraActivity.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraActivity.kt
index e2795b5..3dcab7a 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraActivity.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraActivity.kt
@@ -19,32 +19,13 @@
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.camera.integration.uiwidgets.compose.ui.ComposeCameraApp
 
 class ComposeCameraActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContent {
-            MyApp()
+            ComposeCameraApp()
         }
     }
 }
-
-@Preview
-@Composable
-fun MyApp() {
-    MaterialTheme {
-        Surface {
-            Greeting()
-        }
-    }
-}
-
-@Composable
-fun Greeting() {
-    Text("Hello")
-}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/ComposeCameraApp.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/ComposeCameraApp.kt
new file mode 100644
index 0000000..59c6979
--- /dev/null
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/ComposeCameraApp.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022 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.camera.integration.uiwidgets.compose.ui
+
+import androidx.camera.integration.uiwidgets.compose.ui.navigation.ComposeCameraNavHost
+import androidx.camera.integration.uiwidgets.compose.ui.navigation.ComposeCameraScreen
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.ComposeCameraScreenTabRow
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+
+@Composable
+fun ComposeCameraApp() {
+    MaterialTheme {
+        val allScreens = ComposeCameraScreen.values().toList()
+        val navController = rememberNavController()
+        val backstackEntry = navController.currentBackStackEntryAsState()
+        val currentScreen = ComposeCameraScreen.fromRoute(
+            route = backstackEntry.value?.destination?.route,
+            defaultRoute = ComposeCameraScreen.ImageCapture
+        )
+
+        Scaffold(
+            topBar = {
+                ComposeCameraScreenTabRow(
+                    allScreens = allScreens,
+                     screen ->
+                        navController.navigate(screen.name)
+                    },
+                    currentScreen = currentScreen
+                )
+            }
+        ) { innerPadding ->
+            ComposeCameraNavHost(
+                navController = navController,
+                modifier = Modifier.padding(innerPadding)
+            )
+        }
+    }
+}
+
+@Composable
+fun Greeting() {
+    Text("Hello")
+}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
new file mode 100644
index 0000000..7516248
--- /dev/null
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 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.camera.integration.uiwidgets.compose.ui.navigation
+
+import androidx.camera.integration.uiwidgets.compose.ui.Greeting
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+
+@Composable
+fun ComposeCameraNavHost(
+    navController: NavHostController,
+    modifier: Modifier = Modifier
+) {
+    NavHost(
+        navController = navController,
+        startDestination = ComposeCameraScreen.ImageCapture.name,
+        modifier = modifier
+    ) {
+        composable(ComposeCameraScreen.ImageCapture.name) {
+            Greeting()
+        }
+
+        composable(ComposeCameraScreen.VideoCapture.name) {
+            Greeting()
+        }
+
+        composable(ComposeCameraScreen.Gallery.name) {
+            Greeting()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
new file mode 100644
index 0000000..8923506
--- /dev/null
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 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.camera.integration.uiwidgets.compose.ui.navigation
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CameraAlt
+import androidx.compose.material.icons.filled.PhotoLibrary
+import androidx.compose.material.icons.filled.Videocam
+import androidx.compose.ui.graphics.vector.ImageVector
+
+// Contains each destination screen as an enum
+// and associates an icon to show in the navigation tabs
+enum class ComposeCameraScreen(
+    val icon: ImageVector
+) {
+    ImageCapture(
+        icon = Icons.Filled.CameraAlt
+    ),
+    VideoCapture(
+        icon = Icons.Filled.Videocam
+    ),
+    Gallery(
+        icon = Icons.Filled.PhotoLibrary
+    );
+
+    companion object {
+        fun fromRoute(route: String?, defaultRoute: ComposeCameraScreen): ComposeCameraScreen {
+            return when (route?.substringBefore("/")) {
+                ImageCapture.name -> ImageCapture
+                VideoCapture.name -> VideoCapture
+                Gallery.name -> Gallery
+                null -> defaultRoute
+                else -> throw IllegalArgumentException("Route $route is not recognized.")
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/TabRow.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/TabRow.kt
new file mode 100644
index 0000000..40f5ed1
--- /dev/null
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/TabRow.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2022 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.camera.integration.uiwidgets.compose.ui.screen.components
+
+import androidx.camera.integration.uiwidgets.compose.ui.navigation.ComposeCameraScreen
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import java.util.Locale
+
+private val TabHeight = 56.dp
+private val TabPadding = 16.dp
+private val SpacerWidth = 12.dp
+private const val InactiveTabOpacity = 0.60f
+
+private const val TabFadeInAnimationDuration = 150
+private const val TabFadeInAnimationDelay = 100
+private const val TabFadeOutAnimationDuration = 100
+
+// References: Navigation CodeLab for Android
+
+// Header Tab Row for Navigation
+@Composable
+fun ComposeCameraScreenTabRow(
+    allScreens: List<ComposeCameraScreen>,
+    onTabSelected: (ComposeCameraScreen) -> Unit,
+    currentScreen: ComposeCameraScreen
+) {
+    Surface(
+        Modifier
+            .height(TabHeight)
+            .fillMaxWidth()
+    ) {
+        Row(Modifier.selectableGroup()) {
+            allScreens.forEach { screen ->
+                ComposeCameraTab(
+                    text = screen.name,
+                    icon = screen.icon,
+                     onTabSelected(screen) },
+                    selected = currentScreen == screen
+                )
+            }
+        }
+    }
+}
+
+// Individual Tab items for Tab Row
+@Composable
+private fun ComposeCameraTab(
+    text: String,
+    icon: ImageVector,
+    onSelected: () -> Unit,
+    selected: Boolean
+) {
+    val color = MaterialTheme.colors.onSurface
+    val durationMillis = if (selected) TabFadeInAnimationDuration else TabFadeOutAnimationDuration
+    val animSpec = remember {
+        tween<Color>(
+            durationMillis = durationMillis,
+            easing = LinearEasing,
+            delayMillis = TabFadeInAnimationDelay
+        )
+    }
+
+    val tabTintColor by animateColorAsState(
+        targetValue = if (selected) color else color.copy(alpha = InactiveTabOpacity),
+        animationSpec = animSpec
+    )
+
+    Row(
+        modifier = Modifier
+            .padding(TabPadding)
+            .animateContentSize()
+            .height(TabHeight)
+            .selectable(
+                selected = selected,
+                >
+                role = Role.Tab,
+                interactionSource = remember { MutableInteractionSource() },
+                indication = rememberRipple(
+                    bounded = false,
+                    radius = Dp.Unspecified,
+                    color = Color.Unspecified
+                )
+            )
+            .clearAndSetSemantics { contentDescription = text }
+    ) {
+        Icon(imageVector = icon, contentDescription = text, tint = tabTintColor)
+        if (selected) {
+            Spacer(Modifier.width(SpacerWidth))
+            Text(text.uppercase(Locale.getDefault()), color = tabTintColor)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
index ff09447..aceb657 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
@@ -23,7 +23,6 @@
 import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
-import org.jetbrains.kotlin.resolve.DelegatingBindingTrace
 import org.junit.Test
 
 class LiveLiteralTransformTests : AbstractIrTransformTest() {
@@ -564,7 +563,6 @@
         context: IrPluginContext
     ) {
         @Suppress("DEPRECATION")
-        val bindingTrace = DelegatingBindingTrace(context.bindingContext, "test trace")
         val symbolRemapper = DeepCopySymbolRemapper()
         val keyVisitor = DurableKeyVisitor(builtKeys)
         val transformer = object : LiveLiteralTransformer(
@@ -573,7 +571,6 @@
             keyVisitor,
             context,
             symbolRemapper,
-            bindingTrace,
             ModuleMetricsImpl("temp", context)
         ) {
             override fun makeKeySet(): MutableSet<String> {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt
index 183104f..9a46677 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt
@@ -23,7 +23,6 @@
 import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
-import org.jetbrains.kotlin.resolve.DelegatingBindingTrace
 import org.junit.Test
 
 class LiveLiteralV2TransformTests : AbstractIrTransformTest() {
@@ -566,7 +565,6 @@
         context: IrPluginContext
     ) {
         @Suppress("DEPRECATION")
-        val bindingTrace = DelegatingBindingTrace(context.bindingContext, "test trace")
         val symbolRemapper = DeepCopySymbolRemapper()
         val keyVisitor = DurableKeyVisitor(builtKeys)
         val transformer = object : LiveLiteralTransformer(
@@ -575,7 +573,6 @@
             keyVisitor,
             context,
             symbolRemapper,
-            bindingTrace,
             ModuleMetricsImpl("temp", context)
         ) {
             override fun makeKeySet(): MutableSet<String> {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
index f7e7c57..d4561d2d 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
@@ -37,13 +37,11 @@
 import org.jetbrains.kotlin.backend.common.serialization.DeclarationTable
 import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureSerializer
 import org.jetbrains.kotlin.backend.common.serialization.signature.PublicIdSignatureComputer
-import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
 import org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir.JsGlobalDeclarationTable
 import org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir.JsManglerIr
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.jetbrains.kotlin.platform.js.isJs
 import org.jetbrains.kotlin.platform.jvm.isJvm
-import org.jetbrains.kotlin.resolve.DelegatingBindingTrace
 
 class ComposeIrGenerationExtension(
     @Suppress("unused") private val liveLiteralsEnabled: Boolean = false,
@@ -56,7 +54,7 @@
     private val reportsDestination: String? = null
 ) : IrGenerationExtension {
     var metrics: ModuleMetrics = EmptyModuleMetrics
-    @OptIn(ObsoleteDescriptorBasedAPI::class)
+
     override fun generate(
         moduleFragment: IrModuleFragment,
         pluginContext: IrPluginContext
@@ -64,13 +62,6 @@
         val isKlibTarget = !pluginContext.platform.isJvm()
         VersionChecker(pluginContext).check()
 
-        // TODO: refactor transformers to work with just BackendContext
-        val bindingTrace = DelegatingBindingTrace(
-            pluginContext.bindingContext,
-            "trace in " +
-                "ComposeIrGenerationExtension"
-        )
-
         // create a symbol remapper to be used across all transforms
         val symbolRemapper = ComposableSymbolRemapper()
 
@@ -84,7 +75,6 @@
         ClassStabilityTransformer(
             pluginContext,
             symbolRemapper,
-            bindingTrace,
             metrics
         ).lower(moduleFragment)
 
@@ -94,7 +84,6 @@
             DurableKeyVisitor(),
             pluginContext,
             symbolRemapper,
-            bindingTrace,
             metrics
         ).lower(moduleFragment)
 
@@ -103,7 +92,6 @@
         val functionKeyTransformer = DurableFunctionKeyTransformer(
             pluginContext,
             symbolRemapper,
-            bindingTrace,
             metrics
         )
 
@@ -113,7 +101,6 @@
         ComposerLambdaMemoization(
             pluginContext,
             symbolRemapper,
-            bindingTrace,
             metrics
         ).lower(moduleFragment)
 
@@ -139,7 +126,6 @@
             CreateDecoysTransformer(
                 pluginContext,
                 symbolRemapper,
-                bindingTrace,
                 idSignatureBuilder,
                 metrics,
             ).lower(moduleFragment)
@@ -147,7 +133,6 @@
             SubstituteDecoyCallsTransformer(
                 pluginContext,
                 symbolRemapper,
-                bindingTrace,
                 idSignatureBuilder,
                 metrics,
             ).lower(moduleFragment)
@@ -159,7 +144,6 @@
         ComposerParamTransformer(
             pluginContext,
             symbolRemapper,
-            bindingTrace,
             decoysEnabled,
             metrics,
         ).lower(moduleFragment)
@@ -167,7 +151,6 @@
         ComposableTargetAnnotationsTransformer(
             pluginContext,
             symbolRemapper,
-            bindingTrace,
             metrics
         ).lower(moduleFragment)
 
@@ -178,7 +161,6 @@
         ComposableFunctionBodyTransformer(
             pluginContext,
             symbolRemapper,
-            bindingTrace,
             metrics,
             sourceInformationEnabled,
             intrinsicRememberEnabled
@@ -192,7 +174,6 @@
             RecordDecoySignaturesTransformer(
                 pluginContext,
                 symbolRemapper,
-                bindingTrace,
                 idSignatureBuilder,
                 metrics,
                 mangler!!
@@ -203,7 +184,6 @@
             KlibAssignableParamTransformer(
                 pluginContext,
                 symbolRemapper,
-                bindingTrace,
                 metrics,
             ).lower(moduleFragment)
         }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index 9372fe0..4d40f46 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -149,7 +149,6 @@
 import org.jetbrains.kotlin.psi.psiUtil.endOffset
 import org.jetbrains.kotlin.psi.psiUtil.startOffset
 import org.jetbrains.kotlin.psi2ir.generators.TypeTranslatorImpl
-import org.jetbrains.kotlin.resolve.BindingTrace
 import org.jetbrains.kotlin.resolve.DescriptorFactory
 import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
 import org.jetbrains.kotlin.resolve.source.PsiSourceElement
@@ -161,7 +160,6 @@
 abstract class AbstractComposeLowering(
     val context: IrPluginContext,
     val symbolRemapper: DeepCopySymbolRemapper,
-    val bindingTrace: BindingTrace,
     val metrics: ModuleMetrics
 ) : IrElementTransformerVoid(), ModuleLoweringPass {
     @ObsoleteDescriptorBasedAPI
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ClassStabilityTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ClassStabilityTransformer.kt
index 123a729..f882de8 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ClassStabilityTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ClassStabilityTransformer.kt
@@ -45,7 +45,6 @@
 import org.jetbrains.kotlin.ir.util.isInterface
 import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
 import org.jetbrains.kotlin.platform.jvm.isJvm
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 enum class StabilityBits(val bits: Int) {
     UNSTABLE(0b100),
@@ -60,9 +59,8 @@
 class ClassStabilityTransformer(
     context: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     metrics: ModuleMetrics,
-) : AbstractComposeLowering(context, symbolRemapper, bindingTrace, metrics),
+) : AbstractComposeLowering(context, symbolRemapper, metrics),
     ClassLoweringPass,
     ModuleLoweringPass {
 
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 5952f84..5438837 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -163,7 +163,6 @@
 import org.jetbrains.kotlin.name.Name
 import org.jetbrains.kotlin.platform.js.isJs
 import org.jetbrains.kotlin.platform.jvm.isJvm
-import org.jetbrains.kotlin.resolve.BindingTrace
 import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
 import org.jetbrains.kotlin.types.typeUtil.isUnit
 import org.jetbrains.kotlin.util.OperatorNameConventions
@@ -475,12 +474,11 @@
 class ComposableFunctionBodyTransformer(
     context: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     metrics: ModuleMetrics,
     sourceInformationEnabled: Boolean,
     private val intrinsicRememberEnabled: Boolean
 ) :
-    AbstractComposeLowering(context, symbolRemapper, bindingTrace, metrics),
+    AbstractComposeLowering(context, symbolRemapper, metrics),
     FileLoweringPass,
     ModuleLoweringPass {
 
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTargetAnnotationsTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTargetAnnotationsTransformer.kt
index 345addb..8b2f3bd 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTargetAnnotationsTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTargetAnnotationsTransformer.kt
@@ -88,7 +88,6 @@
 import org.jetbrains.kotlin.ir.util.parentAsClass
 import org.jetbrains.kotlin.ir.util.statements
 import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 /**
  * This transformer walks the IR tree to infer the applier annotations such as ComposableTarget,
@@ -97,9 +96,8 @@
 class ComposableTargetAnnotationsTransformer(
     context: IrPluginContext,
     symbolRemapper: ComposableSymbolRemapper,
-    bindingTrace: BindingTrace,
     metrics: ModuleMetrics
-) : AbstractComposeLowering(context, symbolRemapper, bindingTrace, metrics) {
+) : AbstractComposeLowering(context, symbolRemapper, metrics) {
     private val ComposableTargetClass = getTopLevelClassOrNull(ComposeFqNames.ComposableTarget)
     private val ComposableOpenTargetClass =
         getTopLevelClassOrNull(ComposeFqNames.ComposableOpenTarget)
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
index 3d425c0..d3215535 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
@@ -99,7 +99,6 @@
 import org.jetbrains.kotlin.name.SpecialNames
 import org.jetbrains.kotlin.platform.js.isJs
 import org.jetbrains.kotlin.platform.jvm.isJvm
-import org.jetbrains.kotlin.resolve.BindingTrace
 import org.jetbrains.kotlin.types.typeUtil.isUnit
 
 private class CaptureCollector {
@@ -290,10 +289,9 @@
 class ComposerLambdaMemoization(
     context: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     metrics: ModuleMetrics,
 ) :
-    AbstractComposeLowering(context, symbolRemapper, bindingTrace, metrics),
+    AbstractComposeLowering(context, symbolRemapper, metrics),
     ModuleLoweringPass {
 
     private val declarationContextStack = mutableListOf<DeclarationContext>()
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
index a9ffa42..a834316 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
@@ -89,7 +89,6 @@
 import org.jetbrains.kotlin.load.java.JvmAbi
 import org.jetbrains.kotlin.platform.js.isJs
 import org.jetbrains.kotlin.platform.jvm.isJvm
-import org.jetbrains.kotlin.resolve.BindingTrace
 import org.jetbrains.kotlin.resolve.DescriptorUtils
 import org.jetbrains.kotlin.resolve.multiplatform.findCompatibleExpectsForActual
 import org.jetbrains.kotlin.util.OperatorNameConventions
@@ -98,11 +97,10 @@
 class ComposerParamTransformer(
     context: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     private val decoysEnabled: Boolean,
     metrics: ModuleMetrics,
 ) :
-    AbstractComposeLowering(context, symbolRemapper, bindingTrace, metrics),
+    AbstractComposeLowering(context, symbolRemapper, metrics),
     ModuleLoweringPass {
 
     /**
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableFunctionKeyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableFunctionKeyTransformer.kt
index 56d65a34..ce7c269 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableFunctionKeyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableFunctionKeyTransformer.kt
@@ -46,7 +46,6 @@
 import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
 import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils
 import org.jetbrains.kotlin.name.Name
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 class KeyInfo(
     val name: String,
@@ -102,13 +101,11 @@
 class DurableFunctionKeyTransformer(
     context: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     metrics: ModuleMetrics,
 ) : DurableKeyTransformer(
     DurableKeyVisitor(),
     context,
     symbolRemapper,
-    bindingTrace,
     metrics
 ) {
     fun removeKeyMetaClasses(moduleFragment: IrModuleFragment) {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableKeyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableKeyTransformer.kt
index b562b62..9163125 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableKeyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableKeyTransformer.kt
@@ -65,16 +65,14 @@
 import org.jetbrains.kotlin.ir.util.parentAsClass
 import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
 import org.jetbrains.kotlin.name.Name
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 open class DurableKeyTransformer(
     private val keyVisitor: DurableKeyVisitor,
     context: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     metrics: ModuleMetrics,
 ) :
-    AbstractComposeLowering(context, symbolRemapper, bindingTrace, metrics),
+    AbstractComposeLowering(context, symbolRemapper, metrics),
     ModuleLoweringPass {
 
     override fun lower(module: IrModuleFragment) {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/KlibAssignableParamTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/KlibAssignableParamTransformer.kt
index 04bc3e9..94d46bf 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/KlibAssignableParamTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/KlibAssignableParamTransformer.kt
@@ -36,7 +36,6 @@
 import org.jetbrains.kotlin.ir.util.statements
 import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
 import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 /**
  * This transformer is a workaround for https://youtrack.jetbrains.com/issue/KT-44945 on non-JVM
@@ -60,9 +59,8 @@
 class KlibAssignableParamTransformer(
     context: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     metrics: ModuleMetrics,
-) : AbstractComposeLowering(context, symbolRemapper, bindingTrace, metrics), ModuleLoweringPass {
+) : AbstractComposeLowering(context, symbolRemapper, metrics), ModuleLoweringPass {
     override fun lower(module: IrModuleFragment) {
         module.transformChildrenVoid(this)
     }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/LiveLiteralTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/LiveLiteralTransformer.kt
index d4cbcdb..60220e4 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/LiveLiteralTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/LiveLiteralTransformer.kt
@@ -107,7 +107,6 @@
 import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
 import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils
 import org.jetbrains.kotlin.name.Name
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 /**
  * This transformer transforms constant literal expressions into expressions which read a
@@ -162,10 +161,9 @@
     private val keyVisitor: DurableKeyVisitor,
     context: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     metrics: ModuleMetrics,
 ) :
-    AbstractComposeLowering(context, symbolRemapper, bindingTrace, metrics),
+    AbstractComposeLowering(context, symbolRemapper, metrics),
     ModuleLoweringPass {
 
     override fun lower(module: IrModuleFragment) {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt
index d384e6c..97fca97 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt
@@ -23,18 +23,15 @@
 import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureSerializer
 import org.jetbrains.kotlin.ir.declarations.IrFile
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 abstract class AbstractDecoysLowering(
     pluginContext: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     metrics: ModuleMetrics,
     override val signatureBuilder: IdSignatureSerializer,
 ) : AbstractComposeLowering(
     context = pluginContext,
     symbolRemapper = symbolRemapper,
-    bindingTrace = bindingTrace,
     metrics = metrics
 ), DecoyTransformBase {
 
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt
index a46b5ac..620fcd5 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt
@@ -61,7 +61,6 @@
 import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
 import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
 import org.jetbrains.kotlin.name.Name
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 /**
  * Copies each IR declaration that won't match descriptors after Compose transforms (see [shouldBeRemapped]).
@@ -90,13 +89,11 @@
 class CreateDecoysTransformer(
     pluginContext: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     signatureBuilder: IdSignatureSerializer,
     metrics: ModuleMetrics,
 ) : AbstractDecoysLowering(
     pluginContext = pluginContext,
     symbolRemapper = symbolRemapper,
-    bindingTrace = bindingTrace,
     metrics = metrics,
     signatureBuilder = signatureBuilder
 ), ModuleLoweringPass {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/RecordDecoySignaturesTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/RecordDecoySignaturesTransformer.kt
index 60d6519..2e61324 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/RecordDecoySignaturesTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/RecordDecoySignaturesTransformer.kt
@@ -31,7 +31,6 @@
 import org.jetbrains.kotlin.ir.util.KotlinMangler
 import org.jetbrains.kotlin.ir.util.dump
 import org.jetbrains.kotlin.ir.util.getAnnotation
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 /**
  * Record signatures of the functions created by the [CreateDecoysTransformer] to match them from
@@ -41,14 +40,12 @@
 class RecordDecoySignaturesTransformer(
     pluginContext: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     override val signatureBuilder: IdSignatureSerializer,
     metrics: ModuleMetrics,
     val mangler: KotlinMangler.IrMangler
 ) : AbstractDecoysLowering(
     pluginContext = pluginContext,
     symbolRemapper = symbolRemapper,
-    bindingTrace = bindingTrace,
     metrics = metrics,
     signatureBuilder = signatureBuilder
 ), ModuleLoweringPass {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt
index 7763d4e..ac4fb76 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt
@@ -40,7 +40,6 @@
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
 import org.jetbrains.kotlin.ir.util.copyTypeAndValueArgumentsFrom
 import org.jetbrains.kotlin.ir.util.patchDeclarationParents
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 /**
  * Replaces all decoys references to their implementations created in [CreateDecoysTransformer].
@@ -48,13 +47,11 @@
 class SubstituteDecoyCallsTransformer(
     pluginContext: IrPluginContext,
     symbolRemapper: DeepCopySymbolRemapper,
-    bindingTrace: BindingTrace,
     signatureBuilder: IdSignatureSerializer,
     metrics: ModuleMetrics,
 ) : AbstractDecoysLowering(
     pluginContext = pluginContext,
     symbolRemapper = symbolRemapper,
-    bindingTrace = bindingTrace,
     metrics = metrics,
     signatureBuilder = signatureBuilder
 ), ModuleLoweringPass {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt
index 9f54447..dd5c30b 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.foundation.demos.text
 
+import androidx.compose.animation.Animatable
+import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.text.BasicText
@@ -115,15 +117,12 @@
         selection = TextRange(3)
     )
 
-    // don't animate each frame, as changing the brush resets the cursor timer
-    val color = remember { mutableStateOf(Red) }
+    val color = remember { Animatable(Red) }
     var shouldAnimate by remember { mutableStateOf(false) }
     LaunchedEffect(shouldAnimate) {
         while (shouldAnimate) {
             Rainbow.forEach {
-                color.value = it
-                // we don't control the timer, but sync with the BasicText timer of 1s blinks
-                delay(1000)
+                color.animateTo(it, TweenSpec(1_800))
             }
         }
     }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index db449fe..44589d7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -1938,7 +1938,7 @@
 
         rule.onNodeWithTag("innerScrollable").performTouchInput {
             down(center)
-            moveBy(Offset(this.center.x + 100f, this.center.y))
+            moveBy(Offset(100f, 0f))
             up()
         }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
index 7c4ccaa..319f135 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
@@ -22,8 +22,10 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.testutils.assertPixelColor
 import androidx.compose.testutils.assertPixels
 import androidx.compose.testutils.assertShape
@@ -332,6 +334,40 @@
         }
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun brushChanged_doesntResetTimer() {
+        var cursorBrush by mutableStateOf(SolidColor(cursorColor))
+        rule.setContent {
+            Box(Modifier.padding(boxPadding)) {
+                BasicTextField(
+                    value = "",
+                    >
+                    textStyle = textStyle,
+                    modifier = textFieldModifier,
+                    cursorBrush = cursorBrush,
+                    >
+                )
+            }
+        }
+
+        focusAndWait()
+
+        rule.mainClock.advanceTimeBy(800)
+        cursorBrush = SolidColor(Color.Green)
+        rule.mainClock.advanceTimeByFrame()
+
+        rule.onNode(hasSetTextAction())
+            .captureToImage()
+            .assertShape(
+                density = rule.density,
+                shape = RectangleShape,
+                shapeColor = Color.White,
+                backgroundColor = Color.White,
+                shapeOverlapPixelCount = 0.0f
+            )
+    }
+
     private fun focusAndWait() {
         rule.onNode(hasSetTextAction()).performClick()
         rule.mainClock.advanceTimeUntil { isFocused }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index a7a2df6..f2b1ad2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -79,65 +79,12 @@
     return awaitPointerSlopOrCancellation(
         pointerId,
         PointerType.Touch,
-        onTouchSlopReached
+        >
+        pointerDirectionConfig = HorizontalPointerDirectionConfig,
+        triggerOnMainAxisSlop = false
     )
 }
 
-// TODO(demin): probably we can get rid of copy-paste and reuse awaitPointerSlopOrCancellation
-//  at the bottom of this file. We just need to change its interface:
-//  awaitPointerSlopOrCancellation(getDragValue: (Offset) -> Offset)
-internal suspend fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
-    pointerId: PointerId,
-    pointerType: PointerType,
-    onPointerSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit
-): PointerInputChange? {
-    if (currentEvent.isPointerUp(pointerId)) {
-        return null // The pointer has already been lifted, so the gesture is canceled
-    }
-    var offset = Offset.Zero
-    val touchSlop = viewConfiguration.pointerSlop(pointerType)
-
-    var pointer = pointerId
-
-    while (true) {
-        val event = awaitPointerEvent()
-        val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
-        if (dragEvent.isConsumed) {
-            return null
-        } else if (dragEvent.changedToUpIgnoreConsumed()) {
-            val otherDown = event.changes.fastFirstOrNull { it.pressed }
-            if (otherDown == null) {
-                // This is the last "up"
-                return null
-            } else {
-                pointer = otherDown.id
-            }
-        } else {
-            offset += dragEvent.positionChange()
-            val distance = offset.getDistance()
-            var acceptedDrag = false
-            if (distance >= touchSlop) {
-                val touchSlopOffset = offset / distance * touchSlop
-                onPointerSlopReached(dragEvent, offset - touchSlopOffset)
-                if (dragEvent.isConsumed) {
-                    acceptedDrag = true
-                } else {
-                    offset = Offset.Zero
-                }
-            }
-
-            if (acceptedDrag) {
-                return dragEvent
-            } else {
-                awaitPointerEvent(PointerEventPass.Final)
-                if (dragEvent.isConsumed) {
-                    return null
-                }
-            }
-        }
-    }
-}
-
 /**
  * Reads position change events for [pointerId] and calls [onDrag] for every change in
  * position. If [pointerId] is raised, a new pointer is chosen from those that are down and if
@@ -228,7 +175,8 @@
             do {
                 drag = awaitPointerSlopOrCancellation(
                     down.id,
-                    down.type
+                    down.type,
+                    triggerOnMainAxisSlop = false
                 ) { change, over ->
                     change.consume()
                     overSlop = over
@@ -334,8 +282,8 @@
 ) = awaitPointerSlopOrCancellation(
     pointerId = pointerId,
     pointerType = PointerType.Touch,
-    >
-    getDragDirectionValue = { it.y }
+     change, overSlop -> onTouchSlopReached(change, overSlop.y) },
+    pointerDirectionConfig = VerticalPointerDirectionConfig
 )
 
 internal suspend fun AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(
@@ -345,8 +293,8 @@
 ) = awaitPointerSlopOrCancellation(
     pointerId = pointerId,
     pointerType = pointerType,
-    >
-    getDragDirectionValue = { it.y }
+     change, overSlop -> onTouchSlopReached(change, overSlop.y) },
+    pointerDirectionConfig = VerticalPointerDirectionConfig
 )
 
 /**
@@ -481,8 +429,8 @@
 ) = awaitPointerSlopOrCancellation(
     pointerId = pointerId,
     pointerType = PointerType.Touch,
-    >
-    getDragDirectionValue = { it.x }
+     change, overSlop -> onTouchSlopReached(change, overSlop.x) },
+    pointerDirectionConfig = HorizontalPointerDirectionConfig
 )
 
 internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
@@ -492,8 +440,8 @@
 ) = awaitPointerSlopOrCancellation(
     pointerId = pointerId,
     pointerType = pointerType,
-    >
-    getDragDirectionValue = { it.x }
+     change, overSlop -> onPointerSlopReached(change, overSlop.x) },
+    pointerDirectionConfig = HorizontalPointerDirectionConfig
 )
 
 /**
@@ -669,37 +617,44 @@
 }
 
 /**
- * Waits for drag motion along one axis based on [getDragDirectionValue] to pass pointer slop,
- * using [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer
+ * Waits for drag motion along one axis based on [PointerDirectionConfig.mainAxisDelta] to pass
+ * pointer slop, using [pointerId] as the pointer to examine if [triggerOnMainAxisSlop] is true.
+ * Otherwise, it will wait slop to be crossed on any axis. If [pointerId] is raised, another pointer
  * from those that are down will be chosen to lead the gesture, and if none are down,
  * `null` is returned. If [pointerId] is not down when [awaitPointerSlopOrCancellation] is called,
  * then `null` is returned.
  *
  * When pointer slop is detected, [onPointerSlopReached] is called with the change and the distance
- * beyond the pointer slop. [getDragDirectionValue] should return the position change in the
- * direction of the drag axis. If [onPointerSlopReached] does not consume the position change,
- * pointer slop will not have been considered detected and the detection will continue or,
- * if it is consumed, the [PointerInputChange] that was consumed will be returned.
+ * beyond the pointer slop. [PointerDirectionConfig.mainAxisDelta] should return the position
+ * change in the direction of the drag axis. If [onPointerSlopReached] does not consume the
+ * position change, pointer slop will not have been considered detected and the detection will
+ * continue or, if it is consumed, the [PointerInputChange] that was consumed will be returned.
  *
  * This works with [awaitTouchSlopOrCancellation] for the other axis to ensure that only horizontal
  * or vertical dragging is done, but not both.
  *
+ * [PointerDirectionConfig.offsetFromChanges] should return the offset considering x/y coordinates
+ * positioning and main/cross axis nomenclature. This means if the main axis is Y, we should add
+ * mainChange to the Y position of the resulting offset and vice versa.
+ *
  * @return The [PointerInputChange] of the event that was consumed in [onPointerSlopReached] or
  * `null` if all pointers are raised or the position change was consumed by another gesture
  * detector.
  */
-private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+internal suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
     pointerId: PointerId,
     pointerType: PointerType,
-    onPointerSlopReached: (PointerInputChange, Float) -> Unit,
-    getDragDirectionValue: (Offset) -> Float
+    pointerDirectionConfig: PointerDirectionConfig = HorizontalPointerDirectionConfig,
+    triggerOnMainAxisSlop: Boolean = true,
+    onPointerSlopReached: (PointerInputChange, Offset) -> Unit,
 ): PointerInputChange? {
     if (currentEvent.isPointerUp(pointerId)) {
         return null // The pointer has already been lifted, so the gesture is canceled
     }
     val touchSlop = viewConfiguration.pointerSlop(pointerType)
     var pointer: PointerId = pointerId
-    var totalPositionChange = 0f
+    var totalMainPositionChange = 0f
+    var totalCrossPositionChange = 0f
 
     while (true) {
         val event = awaitPointerEvent()
@@ -717,11 +672,23 @@
         } else {
             val currentPosition = dragEvent.position
             val previousPosition = dragEvent.previousPosition
-            val positionChange = getDragDirectionValue(currentPosition) -
-                getDragDirectionValue(previousPosition)
-            totalPositionChange += positionChange
 
-            val inDirection = abs(totalPositionChange)
+            val mainPositionChange = pointerDirectionConfig.mainAxisDelta(currentPosition) -
+                pointerDirectionConfig.mainAxisDelta(previousPosition)
+
+            val crossPositionChange = pointerDirectionConfig.crossAxisDelta(currentPosition) -
+                pointerDirectionConfig.crossAxisDelta(previousPosition)
+            totalMainPositionChange += mainPositionChange
+            totalCrossPositionChange += crossPositionChange
+
+            val inDirection = if (triggerOnMainAxisSlop) {
+                abs(totalMainPositionChange)
+            } else {
+                pointerDirectionConfig.offsetFromChanges(
+                    totalMainPositionChange,
+                    totalCrossPositionChange
+                ).getDistance()
+            }
             if (inDirection < touchSlop) {
                 // verify that nothing else consumed the drag event
                 awaitPointerEvent(PointerEventPass.Final)
@@ -729,20 +696,73 @@
                     return null
                 }
             } else {
+                val postSlopOffset = if (triggerOnMainAxisSlop) {
+                    val finalMainPositionChange = totalMainPositionChange -
+                        (sign(totalMainPositionChange) * touchSlop)
+                    pointerDirectionConfig.offsetFromChanges(
+                        finalMainPositionChange,
+                        totalCrossPositionChange
+                    )
+                } else {
+                    val offset = pointerDirectionConfig.offsetFromChanges(
+                        totalMainPositionChange,
+                        totalCrossPositionChange
+                    )
+                    val touchSlopOffset = offset / inDirection * touchSlop
+                    offset - touchSlopOffset
+                }
+
                 onPointerSlopReached(
                     dragEvent,
-                    totalPositionChange - (sign(totalPositionChange) * touchSlop)
+                    postSlopOffset
                 )
                 if (dragEvent.isConsumed) {
                     return dragEvent
                 } else {
-                    totalPositionChange = 0f
+                    totalMainPositionChange = 0f
+                    totalCrossPositionChange = 0f
                 }
             }
         }
     }
 }
 
+/**
+ * Configures the calculations to convert offset to deltas in the Main and Cross Axis.
+ * [offsetFromChanges] will also change depending on implementation.
+ */
+internal interface PointerDirectionConfig {
+    fun mainAxisDelta(offset: Offset): Float
+    fun crossAxisDelta(offset: Offset): Float
+    fun offsetFromChanges(mainChange: Float, crossChange: Float): Offset
+}
+
+/**
+ * Used for monitoring changes on X axis.
+ */
+internal val HorizontalPointerDirectionConfig = object : PointerDirectionConfig {
+    override fun mainAxisDelta(offset: Offset): Float = offset.x
+    override fun crossAxisDelta(offset: Offset): Float = offset.y
+    override fun offsetFromChanges(mainChange: Float, crossChange: Float): Offset =
+        Offset(mainChange, crossChange)
+}
+
+/**
+ * Used for monitoring changes on Y axis.
+ */
+internal val VerticalPointerDirectionConfig = object : PointerDirectionConfig {
+    override fun mainAxisDelta(offset: Offset): Float = offset.y
+
+    override fun crossAxisDelta(offset: Offset): Float = offset.x
+
+    override fun offsetFromChanges(mainChange: Float, crossChange: Float): Offset =
+        Offset(crossChange, mainChange)
+}
+
+internal fun Orientation.toPointerDirectionConfig(): PointerDirectionConfig =
+    if (this == Orientation.Vertical) VerticalPointerDirectionConfig
+    else HorizontalPointerDirectionConfig
+
 private suspend fun PointerInputScope.awaitLongPressOrCancellation(
     initialDown: PointerInputChange
 ): PointerInputChange? {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 3ff4450..5395c65 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -181,13 +181,15 @@
     onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
     reverseDirection: Boolean = false
 ): Modifier = draggable(
-    stateFactory = { remember(state) { IgnorePointerDraggableState(state) } },
+    stateFactory = { remember(state, orientation) {
+        IgnorePointerDraggableState(state, orientation) }
+    },
     orientation = orientation,
     enabled = enabled,
     interactionSource = interactionSource,
     startDragImmediately = { startDragImmediately },
     >
-    >
+     velocity -> onDragStopped(velocity.toFloat(orientation)) },
     reverseDirection = reverseDirection,
     canDrag = { true }
 )
@@ -200,7 +202,7 @@
     interactionSource: MutableInteractionSource? = null,
     startDragImmediately: () -> Boolean,
     onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
-    onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
+    onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit = {},
     reverseDirection: Boolean = false
 ): Modifier = composed(
     inspectorInfo = debugInspectorInfo {
@@ -274,7 +276,8 @@
                             var isDragSuccessful = false
                             try {
                                 isDragSuccessful = awaitDrag(
-                                    it,
+                                    it.first,
+                                    it.second,
                                     velocityTracker,
                                     channel,
                                     reverseDirection,
@@ -286,8 +289,8 @@
                             } finally {
                                 val event = if (isDragSuccessful) {
                                     val velocity =
-                                        velocityTracker.calculateVelocity().toFloat(orientation)
-                                    DragStopped(velocity * if (reverseDirection) -1 else 1)
+                                        velocityTracker.calculateVelocity()
+                                    DragStopped(velocity * if (reverseDirection) -1f else 1f)
                                 } else {
                                     DragCancelled
                                 }
@@ -310,7 +313,7 @@
     startDragImmediately: State<() -> Boolean>,
     velocityTracker: VelocityTracker,
     orientation: Orientation
-): Pair<PointerInputChange, Float>? {
+): Pair<PointerInputChange, Offset>? {
     val initialDown =
         awaitFirstDownOnPass(requireUnconsumed = false, pass = PointerEventPass.Initial)
     return if (!canDrag.value.invoke(initialDown)) {
@@ -319,54 +322,58 @@
         initialDown.consume()
         velocityTracker.addPointerInputChange(initialDown)
         // since we start immediately we don't wait for slop and the initial delta is 0
-        initialDown to 0f
+        initialDown to Offset.Zero
     } else {
         val down = awaitFirstDown(requireUnconsumed = false)
         velocityTracker.addPointerInputChange(down)
-        var initialDelta = 0f
-        val postPointerSlop = { event: PointerInputChange, offset: Float ->
+        var initialDelta = Offset.Zero
+        val postPointerSlop = { event: PointerInputChange, offset: Offset ->
             velocityTracker.addPointerInputChange(event)
             event.consume()
             initialDelta = offset
         }
-        val afterSlopResult = if (orientation == Orientation.Vertical) {
-            awaitVerticalPointerSlopOrCancellation(down.id, down.type, postPointerSlop)
-        } else {
-            awaitHorizontalPointerSlopOrCancellation(down.id, down.type, postPointerSlop)
-        }
+
+        val afterSlopResult = awaitPointerSlopOrCancellation(
+            down.id,
+            down.type,
+            pointerDirectionConfig = orientation.toPointerDirectionConfig(),
+            >
+        )
+
         if (afterSlopResult != null) afterSlopResult to initialDelta else null
     }
 }
 
 private suspend fun AwaitPointerEventScope.awaitDrag(
-    dragStart: Pair<PointerInputChange, Float>,
+    startEvent: PointerInputChange,
+    initialDelta: Offset,
     velocityTracker: VelocityTracker,
     channel: SendChannel<DragEvent>,
     reverseDirection: Boolean,
     orientation: Orientation
 ): Boolean {
-    val initialDelta = dragStart.second
-    val startEvent = dragStart.first
 
-    val overSlopOffset = initialDelta.toOffset(orientation)
-    val adjustedStart = startEvent.position - overSlopOffset *
-        sign(startEvent.position.toFloat(orientation))
+    val overSlopOffset = initialDelta
+    val xSign = sign(startEvent.position.x)
+    val ySign = sign(startEvent.position.y)
+    val adjustedStart = startEvent.position -
+        Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
     channel.trySend(DragStarted(adjustedStart))
 
     channel.trySend(
         DragDelta(
-            if (reverseDirection) initialDelta * -1 else initialDelta,
+            if (reverseDirection) initialDelta * -1f else initialDelta,
             adjustedStart
         )
     )
 
     val dragTick: (PointerInputChange) -> Unit = { event ->
         velocityTracker.addPointerInputChange(event)
-        val delta = event.positionChange().toFloat(orientation)
+        val delta = event.positionChange()
         event.consume()
         channel.trySend(
             DragDelta(
-                if (reverseDirection) delta * -1 else delta,
+                if (reverseDirection) delta * -1f else delta,
                 event.position
             )
         )
@@ -380,7 +387,7 @@
 
 private class DragLogic(
     val onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
-    val onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
+    val onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
     val dragStartInteraction: MutableState<DragInteraction.Start?>,
     val interactionSource: MutableInteractionSource?
 ) {
@@ -408,7 +415,7 @@
             interactionSource?.emit(DragInteraction.Cancel(interaction))
             dragStartInteraction.value = null
         }
-        onDragStopped.invoke(this, 0f)
+        onDragStopped.invoke(this, Velocity.Zero)
     }
 }
 
@@ -434,14 +441,11 @@
 
 private sealed class DragEvent {
     class DragStarted(val startPoint: Offset) : DragEvent()
-    class DragStopped(val velocity: Float) : DragEvent()
+    class DragStopped(val velocity: Velocity) : DragEvent()
     object DragCancelled : DragEvent()
-    class DragDelta(val delta: Float, val pointerPosition: Offset) : DragEvent()
+    class DragDelta(val delta: Offset, val pointerPosition: Offset) : DragEvent()
 }
 
-private fun Float.toOffset(orientation: Orientation) =
-    if (orientation == Orientation.Vertical) Offset(0f, this) else Offset(this, 0f)
-
 private fun Offset.toFloat(orientation: Orientation) =
     if (orientation == Orientation.Vertical) this.y else this.x
 
@@ -449,7 +453,7 @@
     if (orientation == Orientation.Vertical) this.y else this.x
 
 internal interface PointerAwareDragScope {
-    fun dragBy(pixels: Float, pointerPosition: Offset): Unit
+    fun dragBy(pixels: Offset, pointerPosition: Offset): Unit
 }
 
 internal interface PointerAwareDraggableState {
@@ -458,15 +462,17 @@
         block: suspend PointerAwareDragScope.() -> Unit
     )
 
-    fun dispatchRawDelta(delta: Float)
+    fun dispatchRawDelta(delta: Offset)
 }
 
-private class IgnorePointerDraggableState(val origin: DraggableState) :
-    PointerAwareDraggableState, PointerAwareDragScope {
+private class IgnorePointerDraggableState(
+    val origin: DraggableState,
+    val orientation: Orientation
+) : PointerAwareDraggableState, PointerAwareDragScope {
     var latestConsumptionScope: DragScope? = null
 
-    override fun dragBy(pixels: Float, pointerPosition: Offset) {
-        latestConsumptionScope?.dragBy(pixels)
+    override fun dragBy(pixels: Offset, pointerPosition: Offset) {
+        latestConsumptionScope?.dragBy(pixels.toFloat(orientation))
     }
 
     override suspend fun drag(
@@ -479,7 +485,7 @@
         }
     }
 
-    override fun dispatchRawDelta(delta: Float) {
-        origin.dispatchRawDelta(delta)
+    override fun dispatchRawDelta(delta: Offset) {
+        origin.dispatchRawDelta(delta.toFloat(orientation))
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 25be40f..1a75b6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -310,9 +310,6 @@
         else -> Offset(0f, this)
     }
 
-    fun Float.toVelocity(): Velocity =
-        if (orientation == Horizontal) Velocity(this, 0f) else Velocity(0f, this)
-
     fun Offset.toFloat(): Float =
         if (orientation == Horizontal) this.x else this.y
 
@@ -372,14 +369,14 @@
         }
     }
 
-    suspend fun onDragStopped(axisVelocity: Float) {
+    suspend fun onDragStopped(initialVelocity: Velocity) {
         val preOverscrollConsumed =
             if (overscrollEffect != null && overscrollEffect.isEnabled) {
-                overscrollEffect.consumePreFling(axisVelocity.toVelocity()).toFloat()
+                overscrollEffect.consumePreFling(initialVelocity)
             } else {
-                0f
+                Velocity.Zero
             }
-        val velocity = (axisVelocity - preOverscrollConsumed).toVelocity()
+        val velocity = (initialVelocity - preOverscrollConsumed)
         val preConsumedByParent = nestedScrollDispatcher.value.dispatchPreFling(velocity)
         val available = velocity - preConsumedByParent
         val velocityLeft = doFlingAnimation(available)
@@ -390,7 +387,7 @@
             )
         val totalLeft = velocityLeft - consumedPost
         if (overscrollEffect != null && overscrollEffect.isEnabled) {
-            overscrollEffect.consumePostFling(totalLeft.toFloat().toVelocity())
+            overscrollEffect.consumePostFling(totalLeft)
         }
     }
 
@@ -428,10 +425,10 @@
 ) : PointerAwareDraggableState, PointerAwareDragScope {
     var latestScrollScope: ScrollScope = NoOpScrollScope
 
-    override fun dragBy(pixels: Float, pointerPosition: Offset) {
+    override fun dragBy(pixels: Offset, pointerPosition: Offset) {
         with(scrollLogic.value) {
             with(latestScrollScope) {
-                dispatchScroll(pixels.toOffset(), pointerPosition, Drag)
+                dispatchScroll(pixels, pointerPosition, Drag)
             }
         }
     }
@@ -446,8 +443,8 @@
         }
     }
 
-    override fun dispatchRawDelta(delta: Float) {
-        with(scrollLogic.value) { performRawScroll(delta.toOffset()) }
+    override fun dispatchRawDelta(delta: Offset) {
+        with(scrollLogic.value) { performRawScroll(delta) }
     }
 }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
index 3701e07..bf87a63 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
@@ -45,7 +45,7 @@
     val cursorAlpha = remember { Animatable(1f) }
     val isBrushSpecified = !(cursorBrush is SolidColor && cursorBrush.value.isUnspecified)
     if (state.hasFocus && value.selection.collapsed && isBrushSpecified) {
-        LaunchedEffect(cursorBrush, value.annotatedString, value.selection) {
+        LaunchedEffect(value.annotatedString, value.selection) {
             // ensure that the value is always 1f _this_ frame by calling snapTo
             cursorAlpha.snapTo(1f)
             // then start the cursor blinking on animation clock (500ms on to start)
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
index a10706d..03d234b 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
@@ -33,7 +33,6 @@
 import androidx.compose.material.DismissValue.DismissedToEnd
 import androidx.compose.material.DismissValue.DismissedToStart
 import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.FractionalThreshold
 import androidx.compose.material.Icon
 import androidx.compose.material.ListItem
 import androidx.compose.material.SwipeToDismiss
@@ -94,9 +93,6 @@
                 state = dismissState,
                 modifier = Modifier.padding(vertical = 4.dp),
                 directions = setOf(StartToEnd, EndToStart),
-                dismissThresholds = { direction ->
-                    FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
-                },
                 background = {
                     val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
                     val color by animateColorAsState(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
index 31c09ad..0aa1377 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
@@ -37,6 +37,7 @@
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.CancellationException
 import kotlin.math.roundToInt
 
@@ -173,7 +174,9 @@
     state: DismissState,
     modifier: Modifier = Modifier,
     directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
-    dismissThresholds: (DismissDirection) -> ThresholdConfig = { FractionalThreshold(0.5f) },
+    dismissThresholds: (DismissDirection) -> ThresholdConfig = {
+        FixedThreshold(DISMISS_THRESHOLD)
+    },
     background: @Composable RowScope.() -> Unit,
     dismissContent: @Composable RowScope.() -> Unit
 ) = BoxWithConstraints(modifier) {
@@ -236,3 +239,5 @@
         else -> null
     }
 }
+
+private val DISMISS_THRESHOLD = 56.dp
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index 34750c6..009420a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -55,7 +55,6 @@
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
@@ -158,7 +157,12 @@
     colors: SliderColors = SliderDefaults.colors()
 ) {
     require(steps >= 0) { "steps should be >= 0" }
-    val >
+    val  -> Unit> {
+        if (it != value) {
+            onValueChange(it)
+        }
+    }
+
     val tickFractions = remember(steps) {
         stepsToTickFractions(steps)
     }
@@ -189,7 +193,6 @@
         fun scaleToOffset(userValue: Float) =
             scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
 
-        val scope = rememberCoroutineScope()
         val rawOffset = remember { mutableStateOf(scaleToOffset(value)) }
         val pressOffset = remember { mutableStateOf(0f) }
 
@@ -197,23 +200,14 @@
             SliderDraggableState {
                 rawOffset.value = (rawOffset.value + it + pressOffset.value)
                 pressOffset.value = 0f
-                val offsetInTrack = rawOffset.value.coerceIn(minPx, maxPx)
+                val offsetInTrack = snapValueToTick(rawOffset.value, tickFractions, minPx, maxPx)
                 onValueChangeState.value.invoke(scaleToUserValue(offsetInTrack))
             }
         }
 
-        CorrectValueSideEffect(::scaleToOffset, valueRange, minPx..maxPx, rawOffset, value)
-
-        val gestureEndAction = rememberUpdatedState<(Float) -> Unit> { velocity: Float ->
-            val current = rawOffset.value
-            val target = snapValueToTick(current, tickFractions, minPx, maxPx)
-            if (current != target) {
-                scope.launch {
-                    animateToTarget(draggableState, current, target, velocity)
-                    onValueChangeFinished?.invoke()
-                }
-            } else if (!draggableState.isDragging) {
-                // check ifDragging in case the change is still in progress (touch -> drag case)
+        val gestureEndAction = rememberUpdatedState {
+            if (!draggableState.isDragging) {
+                // check isDragging in case the change is still in progress (touch -> drag case)
                 onValueChangeFinished?.invoke()
             }
         }
@@ -234,7 +228,7 @@
             reverseDirection = isRtl,
             enabled = enabled,
             interactionSource = interactionSource,
-             velocity -> gestureEndAction.value.invoke(velocity) },
+             _ -> gestureEndAction.value.invoke() },
             startDragImmediately = draggableState.isDragging,
             state = draggableState
         )
@@ -303,7 +297,11 @@
     val endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
 
     require(steps >= 0) { "steps should be >= 0" }
-    val >
+    val  -> Unit> {
+        if (it != values) {
+            onValueChange(it)
+        }
+    }
     val tickFractions = remember(steps) {
         stepsToTickFractions(steps)
     }
@@ -332,44 +330,8 @@
         val rawOffsetStart = remember { mutableStateOf(scaleToOffset(values.start)) }
         val rawOffsetEnd = remember { mutableStateOf(scaleToOffset(values.endInclusive)) }
 
-        CorrectValueSideEffect(
-            ::scaleToOffset,
-            valueRange,
-            minPx..maxPx,
-            rawOffsetStart,
-            values.start
-        )
-        CorrectValueSideEffect(
-            ::scaleToOffset,
-            valueRange,
-            minPx..maxPx,
-            rawOffsetEnd,
-            values.endInclusive
-        )
-
-        val scope = rememberCoroutineScope()
-        val gestureEndAction = rememberUpdatedState<(Boolean) -> Unit> { isStart ->
-            val current = (if (isStart) rawOffsetStart else rawOffsetEnd).value
-            // target is a closest anchor to the `current`, if exists
-            val target = snapValueToTick(current, tickFractions, minPx, maxPx)
-            if (current == target) {
-                onValueChangeFinished?.invoke()
-                return@rememberUpdatedState
-            }
-
-            scope.launch {
-                Animatable(initialValue = current).animateTo(
-                    target, SliderToTickAnimation,
-                    0f
-                ) {
-                    (if (isStart) rawOffsetStart else rawOffsetEnd).value = this.value
-                    onValueChangeState.value.invoke(
-                        scaleToUserValue(rawOffsetStart.value..rawOffsetEnd.value)
-                    )
-                }
-
-                onValueChangeFinished?.invoke()
-            }
+        val gestureEndAction = rememberUpdatedState<(Boolean) -> Unit> {
+            onValueChangeFinished?.invoke()
         }
 
         val  Float) -> Unit> { isStart, offset ->
@@ -377,13 +339,15 @@
                 rawOffsetStart.value = (rawOffsetStart.value + offset)
                 rawOffsetEnd.value = scaleToOffset(values.endInclusive)
                 val offsetEnd = rawOffsetEnd.value
-                val offsetStart = rawOffsetStart.value.coerceIn(minPx, offsetEnd)
+                var offsetStart = rawOffsetStart.value.coerceIn(minPx, offsetEnd)
+                offsetStart = snapValueToTick(offsetStart, tickFractions, minPx, maxPx)
                 offsetStart..offsetEnd
             } else {
                 rawOffsetEnd.value = (rawOffsetEnd.value + offset)
                 rawOffsetStart.value = scaleToOffset(values.start)
                 val offsetStart = rawOffsetStart.value
-                val offsetEnd = rawOffsetEnd.value.coerceIn(offsetStart, maxPx)
+                var offsetEnd = rawOffsetEnd.value.coerceIn(offsetStart, maxPx)
+                offsetEnd = snapValueToTick(offsetEnd, tickFractions, minPx, maxPx)
                 offsetStart..offsetEnd
             }
 
@@ -810,25 +774,6 @@
 private fun calcFraction(a: Float, b: Float, pos: Float) =
     (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f)
 
-@Composable
-private fun CorrectValueSideEffect(
-    scaleToOffset: (Float) -> Float,
-    valueRange: ClosedFloatingPointRange<Float>,
-    trackRange: ClosedFloatingPointRange<Float>,
-    valueState: MutableState<Float>,
-    value: Float
-) {
-    SideEffect {
-        val error = (valueRange.endInclusive - valueRange.start) / 1000
-        val newOffset = scaleToOffset(value)
-        if (abs(newOffset - valueState.value) > error) {
-            if (valueState.value in trackRange) {
-                valueState.value = newOffset
-            }
-        }
-    }
-}
-
 private fun Modifier.sliderSemantics(
     value: Float,
     enabled: Boolean,
@@ -880,7 +825,7 @@
     maxPx: Float,
     isRtl: Boolean,
     rawOffset: State<Float>,
-    gestureEndAction: State<(Float) -> Unit>,
+    gestureEndAction: State<() -> Unit>,
     pressOffset: MutableState<Float>,
     enabled: Boolean
 ) = composed(
@@ -904,7 +849,7 @@
                                 // just trigger animation, press offset will be applied
                                 dragBy(0f)
                             }
-                            gestureEndAction.value.invoke(0f)
+                            gestureEndAction.value.invoke()
                         }
                     }
                 )
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index b405fc2..5654983 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -750,6 +750,18 @@
 
 }
 
+package androidx.compose.runtime.reflect {
+
+  public final class ComposableMethodInvokerKt {
+    method @kotlin.jvm.Throws(exceptionClasses=NoSuchMethodException::class) public static java.lang.reflect.Method getDeclaredComposableMethod(Class<?>, String methodName, Class<?>... args) throws java.lang.NoSuchMethodException;
+    method public static java.lang.reflect.Parameter![] getRealParameters(java.lang.reflect.Method);
+    method public static int getRealParametersCount(java.lang.reflect.Method);
+    method public static Object? invokeComposable(java.lang.reflect.Method, androidx.compose.runtime.Composer composer, Object? instance, java.lang.Object?... args);
+    method public static boolean isComposable(java.lang.reflect.Method);
+  }
+
+}
+
 package androidx.compose.runtime.snapshots {
 
   public final class ListUtilsKt {
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index e121f57..75793d0 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -815,6 +815,18 @@
 
 }
 
+package androidx.compose.runtime.reflect {
+
+  public final class ComposableMethodInvokerKt {
+    method @kotlin.jvm.Throws(exceptionClasses=NoSuchMethodException::class) public static java.lang.reflect.Method getDeclaredComposableMethod(Class<?>, String methodName, Class<?>... args) throws java.lang.NoSuchMethodException;
+    method public static java.lang.reflect.Parameter![] getRealParameters(java.lang.reflect.Method);
+    method public static int getRealParametersCount(java.lang.reflect.Method);
+    method public static Object? invokeComposable(java.lang.reflect.Method, androidx.compose.runtime.Composer composer, Object? instance, java.lang.Object?... args);
+    method public static boolean isComposable(java.lang.reflect.Method);
+  }
+
+}
+
 package androidx.compose.runtime.snapshots {
 
   public final class ListUtilsKt {
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 786f2b7..2643f45 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -778,6 +778,18 @@
 
 }
 
+package androidx.compose.runtime.reflect {
+
+  public final class ComposableMethodInvokerKt {
+    method @kotlin.jvm.Throws(exceptionClasses=NoSuchMethodException::class) public static java.lang.reflect.Method getDeclaredComposableMethod(Class<?>, String methodName, Class<?>... args) throws java.lang.NoSuchMethodException;
+    method public static java.lang.reflect.Parameter![] getRealParameters(java.lang.reflect.Method);
+    method public static int getRealParametersCount(java.lang.reflect.Method);
+    method public static Object? invokeComposable(java.lang.reflect.Method, androidx.compose.runtime.Composer composer, Object? instance, java.lang.Object?... args);
+    method public static boolean isComposable(java.lang.reflect.Method);
+  }
+
+}
+
 package androidx.compose.runtime.snapshots {
 
   public final class ListUtilsKt {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index e318d61..1dc664c 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -919,7 +919,10 @@
     }
 
     internal fun removeDerivedStateObservation(state: DerivedState<*>) {
-        derivedStates.removeScope(state)
+        // remove derived state if it is not observed in other scopes
+        if (state !in observations) {
+            derivedStates.removeScope(state)
+        }
     }
 
     /**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
index 368da08..3accee3 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
@@ -24,7 +24,7 @@
 import androidx.compose.runtime.RecomposeScopeImpl
 import androidx.compose.runtime.Stable
 
-private const val SLOTS_PER_INT = 10
+internal const val SLOTS_PER_INT = 10
 private const val BITS_PER_SLOT = 3
 
 internal fun bitsForSlot(bits: Int, slot: Int): Int {
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
index c73242e..125ac7d 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
@@ -387,6 +387,54 @@
     }
 
     @Test
+    fun observingDerivedStateInMultipleScopes() = compositionTest {
+        var observeInFirstScope by mutableStateOf(true)
+        var count by mutableStateOf(0)
+
+        compose {
+            val items by remember {
+                derivedStateOf {
+                    List(count) { it }
+                }
+            }
+
+            Linear {
+                if (observeInFirstScope) {
+                    Text("List of size ${items.size}")
+                }
+            }
+
+            Linear {
+                Text("List of size ${items.size}")
+            }
+        }
+
+        validate {
+            Linear {
+                Text("List of size 0")
+            }
+
+            Linear {
+                Text("List of size 0")
+            }
+        }
+
+        observeInFirstScope = false
+        advance()
+        count++
+        advance()
+
+        validate {
+            Linear {
+            }
+
+            Linear {
+                Text("List of size 1")
+            }
+        }
+    }
+
+    @Test
     fun changingTheDerivedStateInstanceShouldClearDependencies() = compositionTest {
         var reload by mutableStateOf(0)
 
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambdaN.jvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambdaN.jvm.kt
index 3193f395..121e1e1 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambdaN.jvm.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambdaN.jvm.kt
@@ -24,8 +24,6 @@
 import androidx.compose.runtime.Stable
 import kotlin.jvm.functions.FunctionN
 
-private const val SLOTS_PER_INT = 10
-
 @Stable
 internal class ComposableLambdaNImpl(
     val key: Int,
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethodInvoker.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethodInvoker.kt
new file mode 100644
index 0000000..3b7777a
--- /dev/null
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethodInvoker.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2022 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.compose.runtime.reflect
+
+import androidx.compose.runtime.Composer
+import androidx.compose.runtime.internal.SLOTS_PER_INT
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import java.lang.reflect.Parameter
+import kotlin.math.ceil
+
+private inline fun <reified T> T.dup(count: Int): Array<T> {
+    return (0 until count).map { this }.toTypedArray()
+}
+
+/**
+ * Find the given @Composable method by name.
+ */
+@Throws(NoSuchMethodException::class)
+fun Class<*>.getDeclaredComposableMethod(methodName: String, vararg args: Class<*>): Method {
+    val changedParams = changedParamCount(args.size, 0)
+    val method = try {
+        // without defaults
+        getDeclaredMethod(
+            methodName,
+            *args,
+            Composer::class.java, // composer param
+            *Int::class.java.dup(changedParams) // changed params
+        )
+    } catch (e: ReflectiveOperationException) {
+        val defaultParams = defaultParamCount(args.size)
+        try {
+            getDeclaredMethod(
+                methodName,
+                *args,
+                Composer::class.java, // composer param
+                *Int::class.java.dup(changedParams), // changed param
+                *Int::class.java.dup(defaultParams) // default param
+            )
+        } catch (e2: ReflectiveOperationException) {
+            null
+        }
+    } ?: throw NoSuchMethodException("$name.$methodName")
+
+    return method
+}
+
+/**
+ * Returns the default value for the [Class] type. This will be 0 for numeric types, false for
+ * boolean and null for object references.
+ */
+private fun Class<*>.getDefaultValue(): Any? = when (name) {
+    "int" -> 0.toInt()
+    "short" -> 0.toShort()
+    "byte" -> 0.toByte()
+    "long" -> 0.toLong()
+    "double" -> 0.toDouble()
+    "float" -> 0.toFloat()
+    "boolean" -> false
+    "char" -> 0.toChar()
+    else -> null
+}
+
+/**
+ * Structure intended to be used exclusively by [getComposableInfo].
+ */
+private data class ComposableInfo(
+    val isComposable: Boolean,
+    val realParamsCount: Int,
+    val changedParams: Int,
+    val defaultParams: Int
+)
+
+/**
+ * Checks whether the method is Composable function and returns result along with the real
+ * parameters count and changed parameter count (if composable) and packed in a structure.
+ */
+private fun Method.getComposableInfo(): ComposableInfo {
+    val realParamsCount = parameterTypes.indexOfLast { it == Composer::class.java }
+    if (realParamsCount == -1) {
+        return ComposableInfo(false, parameterTypes.size, 0, 0)
+    }
+    val thisParams = if (Modifier.isStatic(this.modifiers)) 0 else 1
+    val changedParams = changedParamCount(realParamsCount, thisParams)
+    val totalParamsWithoutDefaults = realParamsCount +
+        1 + // composer
+        changedParams
+    val totalParams = parameterTypes.size
+    val isDefault = totalParams != totalParamsWithoutDefaults
+    val defaultParams = if (isDefault)
+        defaultParamCount(realParamsCount)
+    else
+        0
+    return ComposableInfo(
+        totalParamsWithoutDefaults + defaultParams == totalParams,
+        realParamsCount,
+        changedParams,
+        defaultParams
+    )
+}
+
+/**
+ * Calls the Composable method on the given [instance]. If the method accepts default values, this
+ * function will call it with the correct options set.
+ */
+@Suppress("BanUncheckedReflection", "ListIterator")
+fun Method.invokeComposable(
+    composer: Composer,
+    instance: Any?,
+    vararg args: Any?
+): Any? {
+    val (isComposable, realParamsCount, changedParams, defaultParams) = getComposableInfo()
+
+    check(isComposable)
+
+    val totalParams = parameterTypes.size
+    val changedStartIndex = realParamsCount + 1
+    val defaultStartIndex = changedStartIndex + changedParams
+
+    val defaultsMasks = Array(defaultParams) { index ->
+        val start = index * BITS_PER_INT
+        val end = minOf(start + BITS_PER_INT, realParamsCount)
+        val useDefault =
+            (start until end).map { if (it >= args.size || args[it] == null) 1 else 0 }
+        val mask = useDefault.foldIndexed(0) { i, mask, default -> mask or (default shl i) }
+        mask
+    }
+
+    val arguments = Array(totalParams) { idx ->
+        when (idx) {
+            // pass in "empty" value for all real parameters since we will be using defaults.
+            in 0 until realParamsCount -> args.getOrElse(idx) {
+                parameterTypes[idx].getDefaultValue()
+            }
+            // the composer is the first synthetic parameter
+            realParamsCount -> composer
+            // since this is the root we don't need to be anything unique. 0 should suffice.
+            // changed parameters should be 0 to indicate "uncertain"
+            changedStartIndex -> 1
+            in changedStartIndex + 1 until defaultStartIndex -> 0
+            // Default values mask, all parameters set to use defaults
+            in defaultStartIndex until totalParams -> defaultsMasks[idx - defaultStartIndex]
+            else -> error("Unexpected index")
+        }
+    }
+    return invoke(instance, *arguments)
+}
+
+private const val BITS_PER_INT = 31
+
+private fun changedParamCount(realValueParams: Int, thisParams: Int): Int {
+    if (realValueParams == 0) return 1
+    val totalParams = realValueParams + thisParams
+    return ceil(
+        totalParams.toDouble() / SLOTS_PER_INT.toDouble()
+    ).toInt()
+}
+
+private fun defaultParamCount(realValueParams: Int): Int {
+    return ceil(
+        realValueParams.toDouble() / BITS_PER_INT.toDouble()
+    ).toInt()
+}
+
+/**
+ * Returns true if the method is a Composable function and false otherwise.
+ */
+val Method.isComposable: Boolean
+    get() = getComposableInfo().isComposable
+
+/**
+ * Returns real parameters count for the method, it returns the actual parameters count for the
+ * usual methods and for Composable functions it excludes the utility Compose-specific parameters
+ * from counting.
+ */
+val Method.realParametersCount: Int
+    get() {
+        val (isComposable, realParametersCount, _, _) = getComposableInfo()
+        if (isComposable) {
+            return realParametersCount
+        }
+        return parameterTypes.size
+    }
+
+/**
+ * Returns real parameters for the method, it returns the actual parameters for the usual methods
+ * and for Composable functions it excludes the utility Compose-specific parameters.
+ */
+val Method.realParameters: Array<out Parameter>
+    @Suppress("ClassVerificationFailure", "NewApi")
+    get() {
+        val (isComposable, realParametersCount, _, _) = getComposableInfo()
+        if (isComposable) {
+            return parameters.copyOfRange(0, realParametersCount)
+        }
+        return parameters
+    }
diff --git a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodInvokerTest.kt b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodInvokerTest.kt
new file mode 100644
index 0000000..a976fad
--- /dev/null
+++ b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodInvokerTest.kt
@@ -0,0 +1,581 @@
+/*
+ * Copyright 2022 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.compose.runtime.reflect
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composer
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.MonotonicFrameClock
+import androidx.compose.runtime.Recomposer
+import androidx.compose.runtime.currentComposer
+import androidx.compose.runtime.mock.EmptyApplier
+import androidx.compose.runtime.withRunningRecomposer
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+import kotlinx.coroutines.runBlocking
+
+@Composable
+private fun composableFunction() {
+}
+
+private fun nonComposableFunction() {
+}
+
+@Suppress("UNUSED_PARAMETER")
+private fun nonComposableFunctionWithComposerParam(unused: Composer) {
+}
+
+@Composable
+private fun composableFunctionWithDefaults(
+    s1: String,
+    s2: String,
+    s3: String = "a",
+    s4: String = "a",
+    s5: String = "a"
+): String { return s1 + s2 + s3 + s4 + s5 }
+
+@Composable
+private fun overloadedComposable() {
+}
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+private fun overloadedComposable(s: String) {
+}
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+private fun overloadedComposable(
+    v1: String,
+    v2: String,
+    v3: String,
+    v4: String,
+    v5: String,
+    v6: String,
+    v7: String,
+    v8: String,
+    v9: String,
+    v10: String
+) { }
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+private fun overloadedComposable(
+    v1: String,
+    v2: String,
+    v3: String,
+    v4: String,
+    v5: String,
+    v6: String,
+    v7: String,
+    v8: String,
+    v9: String,
+    v10: String,
+    v11: String
+) { }
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+private fun overloadedComposable(
+    v1: String,
+    v2: String,
+    v3: String,
+    v4: String,
+    v5: String,
+    v6: String,
+    v7: String,
+    v8: String,
+    v9: String,
+    v10: String,
+    v11: String,
+    v12: String
+) { }
+
+@Suppress("UNUSED_PARAMETER")
+@Composable
+private fun differentParametersTypes(
+    v1: String,
+    v2: Any,
+    v3: Int,
+    v4: Float,
+    v5: Double,
+    v6: Long
+) { }
+
+private class ComposablesWrapper {
+    @Composable
+    fun composableMethod() {
+    }
+
+    fun nonComposableMethod() {
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    fun nonComposableMethodWithComposerParam(unused: Composer) {
+    }
+
+    @Composable
+    fun composableMethodWithDefaults(
+        s1: String,
+        s2: String,
+        s3: String = "a",
+        s4: String = "a",
+        s5: String = "a"
+    ): String { return s1 + s2 + s3 + s4 + s5 }
+
+    @Composable
+    fun overloadedComposableMethod() {
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    @Composable
+    fun overloadedComposableMethod(s: String) {
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    @Composable
+    fun overloadedComposableMethod(
+        v1: String,
+        v2: String,
+        v3: String,
+        v4: String,
+        v5: String,
+        v6: String,
+        v7: String,
+        v8: String,
+        v9: String,
+        v10: String
+    ) { }
+
+    @Suppress("UNUSED_PARAMETER")
+    @Composable
+    fun overloadedComposableMethod(
+        v1: String,
+        v2: String,
+        v3: String,
+        v4: String,
+        v5: String,
+        v6: String,
+        v7: String,
+        v8: String,
+        v9: String,
+        v10: String,
+        v11: String
+    ) { }
+
+    @Suppress("UNUSED_PARAMETER")
+    @Composable
+    fun overloadedComposableMethod(
+        v1: String,
+        v2: String,
+        v3: String,
+        v4: String,
+        v5: String,
+        v6: String,
+        v7: String,
+        v8: String,
+        v9: String,
+        v10: String,
+        v11: String,
+        v12: String
+    ) { }
+
+    @Suppress("UNUSED_PARAMETER")
+    @Composable
+    fun differentParametersTypesMethod(
+        v1: String,
+        v2: Any,
+        v3: Int,
+        v4: Float,
+        v5: Double,
+        v6: Long
+    ) { }
+}
+
+class ComposableMethodInvokerTest {
+    private val clazz =
+        Class.forName("androidx.compose.runtime.reflect.ComposableMethodInvokerTestKt")
+    private val wrapperClazz =
+        Class.forName("androidx.compose.runtime.reflect.ComposablesWrapper")
+
+    private val composable = clazz.declaredMethods.find { it.name == "composableFunction" }!!
+    private val nonComposable = clazz.declaredMethods.find { it.name == "nonComposableFunction" }!!
+    private val nonComposableWithComposer =
+        clazz.declaredMethods.find { it.name == "nonComposableFunctionWithComposerParam" }!!
+    private val composableMethod =
+        wrapperClazz.declaredMethods.find { it.name == "composableMethod" }!!
+    private val nonComposableMethod =
+        wrapperClazz.declaredMethods.find { it.name == "nonComposableMethod" }!!
+    private val nonComposableMethodWithComposer =
+        wrapperClazz.declaredMethods.find { it.name == "nonComposableMethodWithComposerParam" }!!
+
+    @Test
+    fun test_isComposable_correctly_checks_functions() {
+        assertTrue(composable.isComposable)
+        assertFalse(nonComposable.isComposable)
+        assertFalse(nonComposableWithComposer.isComposable)
+        assertTrue(composableMethod.isComposable)
+        assertFalse(nonComposableMethod.isComposable)
+        assertFalse(nonComposableMethodWithComposer.isComposable)
+    }
+
+    @Throws(NoSuchMethodException::class)
+    @Test
+    fun test_getDeclaredComposableMethod_differentiates_overloaded_functions() {
+        val method0 = clazz.getDeclaredComposableMethod("overloadedComposable")
+        val method1 = clazz.getDeclaredComposableMethod("overloadedComposable", String::class.java)
+        val method10 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(10) { String::class.java }
+            )
+        val method11 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(11) { String::class.java }
+            )
+        val method12 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(12) { String::class.java }
+            )
+
+        assertNotEquals(method0, method1)
+        assertNotEquals(method0, method10)
+        assertNotEquals(method0, method11)
+        assertNotEquals(method0, method12)
+        assertNotEquals(method1, method10)
+        assertNotEquals(method1, method11)
+        assertNotEquals(method1, method12)
+        assertNotEquals(method10, method11)
+        assertNotEquals(method10, method12)
+        assertNotEquals(method11, method12)
+    }
+
+    @Throws(NoSuchMethodException::class)
+    @Test
+    fun test_getDeclaredComposableMethod_differentiates_overloaded_methods() {
+        val method0 = wrapperClazz.getDeclaredComposableMethod("overloadedComposableMethod")
+        val method1 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                String::class.java
+            )
+        val method10 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(10) { String::class.java }
+            )
+        val method11 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(11) { String::class.java }
+            )
+        val method12 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(12) { String::class.java }
+            )
+
+        assertNotEquals(method0, method1)
+        assertNotEquals(method0, method10)
+        assertNotEquals(method0, method11)
+        assertNotEquals(method0, method12)
+        assertNotEquals(method1, method10)
+        assertNotEquals(method1, method11)
+        assertNotEquals(method1, method12)
+        assertNotEquals(method10, method11)
+        assertNotEquals(method10, method12)
+        assertNotEquals(method11, method12)
+    }
+
+    @Throws(NoSuchMethodException::class)
+    @Test
+    fun test_getDeclaredComposableMethod_works_with_default_params() {
+        clazz.getDeclaredComposableMethod(
+            "composableFunctionWithDefaults",
+            *Array(5) { String::class.java }
+        )
+
+        wrapperClazz.getDeclaredComposableMethod(
+            "composableMethodWithDefaults",
+            *Array(5) { String::class.java }
+        )
+    }
+
+    @Throws(NoSuchMethodException::class)
+    @Test
+    fun test_realParametersCount_returns_correct_number_of_parameters() {
+        val function0 = clazz.getDeclaredComposableMethod("overloadedComposable")
+        val function1 =
+            clazz.getDeclaredComposableMethod("overloadedComposable", String::class.java)
+        val function10 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(10) { String::class.java }
+            )
+        val function11 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(11) { String::class.java }
+            )
+        val function12 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(12) { String::class.java }
+            )
+
+        val method0 = wrapperClazz.getDeclaredComposableMethod("overloadedComposableMethod")
+        val method1 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod", String::class.java
+            )
+        val method10 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(10) { String::class.java }
+            )
+        val method11 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(11) { String::class.java }
+            )
+        val method12 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(12) { String::class.java }
+            )
+
+        assertEquals(0, function0.realParametersCount)
+        assertEquals(1, function1.realParametersCount)
+        assertEquals(10, function10.realParametersCount)
+        assertEquals(11, function11.realParametersCount)
+        assertEquals(12, function12.realParametersCount)
+
+        assertEquals(0, method0.realParametersCount)
+        assertEquals(1, method1.realParametersCount)
+        assertEquals(10, method10.realParametersCount)
+        assertEquals(11, method11.realParametersCount)
+        assertEquals(12, method12.realParametersCount)
+
+        assertEquals(0, nonComposable.realParametersCount)
+        assertEquals(1, nonComposableWithComposer.realParametersCount)
+        assertEquals(0, composableMethod.realParametersCount)
+        assertEquals(0, nonComposableMethod.realParametersCount)
+        assertEquals(1, nonComposableMethodWithComposer.realParametersCount)
+    }
+
+    @Suppress("ClassVerificationFailure", "NewApi")
+    @Throws(NoSuchMethodException::class)
+    @Test
+    fun test_realParameters_returns_correct_parameters() {
+        val function0 = clazz.getDeclaredComposableMethod("overloadedComposable")
+        val function1 =
+            clazz.getDeclaredComposableMethod("overloadedComposable", String::class.java)
+        val function10 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(10) { String::class.java }
+            )
+        val function11 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(11) { String::class.java }
+            )
+        val function12 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(12) { String::class.java }
+            )
+
+        val method0 = wrapperClazz.getDeclaredComposableMethod("overloadedComposableMethod")
+        val method1 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                String::class.java
+            )
+        val method10 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(10) { String::class.java }
+            )
+        val method11 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(11) { String::class.java }
+            )
+        val method12 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(12) { String::class.java }
+            )
+
+        val diffParameters =
+            clazz.getDeclaredComposableMethod(
+                "differentParametersTypes",
+                String::class.java,
+                Any::class.java,
+                Int::class.java,
+                Float::class.java,
+                Double::class.java,
+                Long::class.java
+            )
+
+        val diffParametersMethod =
+            wrapperClazz.getDeclaredComposableMethod(
+                "differentParametersTypesMethod",
+                String::class.java,
+                Any::class.java,
+                Int::class.java,
+                Float::class.java,
+                Double::class.java,
+                Long::class.java
+            )
+
+        assertEquals(0, function0.realParameters.size)
+        assertEquals(1, function1.realParameters.size)
+        assertEquals(10, function10.realParameters.size)
+        assertEquals(11, function11.realParameters.size)
+        assertEquals(12, function12.realParameters.size)
+        assertEquals(12, function12.realParameters.size)
+
+        assertEquals(0, method0.realParameters.size)
+        assertEquals(1, method1.realParameters.size)
+        assertEquals(10, method10.realParameters.size)
+        assertEquals(11, method11.realParameters.size)
+        assertEquals(12, method12.realParameters.size)
+
+        assertEquals(0, nonComposable.realParameters.size)
+        assertEquals(1, nonComposableWithComposer.realParameters.size)
+        assertEquals(0, composableMethod.realParameters.size)
+        assertEquals(0, nonComposableMethod.realParameters.size)
+        assertEquals(1, nonComposableMethodWithComposer.realParameters.size)
+
+        assertEquals(6, diffParameters.realParameters.size)
+        assertEquals(
+            listOf(String::class.java, Any::class.java, Int::class.java, Float::class.java,
+                Double::class.java, Long::class.java),
+            diffParameters.realParameters.map { it.type })
+
+        assertEquals(6, diffParametersMethod.realParameters.size)
+        assertEquals(
+            listOf(String::class.java, Any::class.java, Int::class.java, Float::class.java,
+                Double::class.java, Long::class.java),
+            diffParametersMethod.realParameters.map { it.type })
+    }
+
+    private class TestFrameClock : MonotonicFrameClock {
+        override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R = onFrame(0L)
+    }
+
+    private fun <T> executeWithComposer(block: (composer: Composer) -> T): T =
+        runBlocking(TestFrameClock()) {
+            fun compose(
+                recomposer: Recomposer,
+                block: @Composable () -> Unit
+            ): Composition {
+                return Composition(
+                    EmptyApplier(),
+                    recomposer
+                ).apply {
+                    setContent(block)
+                }
+            }
+
+            var res: T? = null
+            withRunningRecomposer { r ->
+                compose(r) {
+                    res = block(currentComposer)
+                }
+            }
+            res!!
+        }
+
+    @Test
+    fun testInvokeComposableFunctions() {
+
+        val composableWithDefaults =
+            clazz.declaredMethods.find { it.name == "composableFunctionWithDefaults" }!!
+        composableWithDefaults.isAccessible = true
+
+        val resABAAA = executeWithComposer {
+            composableWithDefaults.invokeComposable(it, null, "a", "b") as String
+        }
+
+        val resABCAA = executeWithComposer {
+            composableWithDefaults.invokeComposable(it, null, "a", "b", "c") as String
+        }
+
+        val resABCDA = executeWithComposer {
+            composableWithDefaults.invokeComposable(it, null, "a", "b", "c", "d") as String
+        }
+
+        val resABCDE = executeWithComposer {
+            composableWithDefaults.invokeComposable(it, null, "a", "b", "c", "d", "e") as String
+        }
+
+        val resABADA = executeWithComposer {
+            composableWithDefaults.invokeComposable(it, null, "a", "b", null, "d") as String
+        }
+
+        assertEquals("abaaa", resABAAA)
+        assertEquals("abcaa", resABCAA)
+        assertEquals("abcda", resABCDA)
+        assertEquals("abcde", resABCDE)
+        assertEquals("abada", resABADA)
+    }
+
+    @Test
+    fun testInvokeComposableMethods() {
+
+        val composableWithDefaults =
+            wrapperClazz.declaredMethods.find { it.name == "composableMethodWithDefaults" }!!
+        composableWithDefaults.isAccessible = true
+
+        val instance = ComposablesWrapper()
+
+        val resABAAA = executeWithComposer {
+            composableWithDefaults.invokeComposable(it, instance, "a", "b") as String
+        }
+
+        val resABCAA = executeWithComposer {
+            composableWithDefaults.invokeComposable(it, instance, "a", "b", "c") as String
+        }
+
+        val resABCDA = executeWithComposer {
+            composableWithDefaults.invokeComposable(it, instance, "a", "b", "c", "d") as String
+        }
+
+        val resABCDE = executeWithComposer {
+            composableWithDefaults
+                .invokeComposable(it, instance, "a", "b", "c", "d", "e") as String
+        }
+
+        val resABADA = executeWithComposer {
+            composableWithDefaults.invokeComposable(it, instance, "a", "b", null, "d") as String
+        }
+
+        assertEquals("abaaa", resABAAA)
+        assertEquals("abcaa", resABCAA)
+        assertEquals("abcda", resABCDA)
+        assertEquals("abcde", resABCDE)
+        assertEquals("abada", resABADA)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/androidCommonTest/kotlin/androidx/compose/ui/test/util/Verifications.kt b/compose/ui/ui-test/src/androidCommonTest/kotlin/androidx/compose/ui/test/util/Verifications.kt
index 81418d0..489ecf0 100644
--- a/compose/ui/ui-test/src/androidCommonTest/kotlin/androidx/compose/ui/test/util/Verifications.kt
+++ b/compose/ui/ui-test/src/androidCommonTest/kotlin/androidx/compose/ui/test/util/Verifications.kt
@@ -144,9 +144,10 @@
 internal fun InputEvent.verifyKeyEvent(
     expectedAction: Int,
     expectedKeyCode: Int,
-    expectedEventTime: Int = 0,
-    expectedDownTime: Int = 0,
-    expectedMetaState: Int = 0
+    expectedEventTime: Long = 0,
+    expectedDownTime: Long = 0,
+    expectedMetaState: Int = 0,
+    expectedRepeat: Int = 0,
 ) {
     if (this is KeyEvent) {
         assertWithMessage("action").that(action).isEqualTo(expectedAction)
@@ -154,6 +155,7 @@
         assertWithMessage("eventTime").that(eventTime).isEqualTo(expectedEventTime)
         assertWithMessage("downTime").that(downTime).isEqualTo(expectedDownTime)
         assertWithMessage("metaState").that(metaState).isEqualTo(expectedMetaState)
+        assertWithMessage("repeat").that(repeatCount).isEqualTo(expectedRepeat)
     } else {
         throw AssertionError("A keyboard event must be of type KeyEvent, " +
             "not ${this::class.simpleName}")
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt
index 722d002..9994145 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt
@@ -198,20 +198,21 @@
 
     @OptIn(ExperimentalComposeUiApi::class)
     fun KeyInputState.constructMetaState(): Int {
-        fun Key.isDown() = isKeyDown(this)
+
+        fun genState(key: Key, mask: Int) = if (isKeyDown(key)) mask else 0
 
         return (if (capsLockOn) KeyEvent.META_CAPS_LOCK_ON else 0) or
             (if (numLockOn) KeyEvent.META_NUM_LOCK_ON else 0) or
             (if (scrollLockOn) KeyEvent.META_SCROLL_LOCK_ON else 0) or
-            (if (Key.Function.isDown()) KeyEvent.META_FUNCTION_ON else 0) or
-            (if (Key.CtrlLeft.isDown()) KeyEvent.META_CTRL_LEFT_ON else 0) or
-            (if (Key.CtrlRight.isDown()) KeyEvent.META_CTRL_RIGHT_ON else 0) or
-            (if (Key.AltLeft.isDown()) KeyEvent.META_ALT_LEFT_ON else 0) or
-            (if (Key.AltRight.isDown()) KeyEvent.META_ALT_RIGHT_ON else 0) or
-            (if (Key.MetaLeft.isDown()) KeyEvent.META_META_LEFT_ON else 0) or
-            (if (Key.MetaRight.isDown()) KeyEvent.META_META_RIGHT_ON else 0) or
-            (if (Key.ShiftLeft.isDown()) KeyEvent.META_SHIFT_LEFT_ON else 0) or
-            (if (Key.ShiftRight.isDown()) KeyEvent.META_SHIFT_RIGHT_ON else 0)
+            genState(Key.Function, KeyEvent.META_FUNCTION_ON) or
+            genState(Key.CtrlLeft, KeyEvent.META_CTRL_LEFT_ON or KeyEvent.META_CTRL_ON) or
+            genState(Key.CtrlRight, KeyEvent.META_CTRL_RIGHT_ON or KeyEvent.META_CTRL_ON) or
+            genState(Key.AltLeft, KeyEvent.META_ALT_LEFT_ON or KeyEvent.META_ALT_ON) or
+            genState(Key.AltRight, KeyEvent.META_ALT_RIGHT_ON or KeyEvent.META_ALT_ON) or
+            genState(Key.MetaLeft, KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON) or
+            genState(Key.MetaRight, KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON) or
+            genState(Key.ShiftLeft, KeyEvent.META_SHIFT_LEFT_ON or KeyEvent.META_SHIFT_ON) or
+            genState(Key.ShiftRight, KeyEvent.META_SHIFT_RIGHT_ON or KeyEvent.META_SHIFT_ON)
     }
 
     override fun KeyInputState.enqueueDown(key: Key) =
@@ -471,6 +472,7 @@
             eventTime = currentTime,
             action = action,
             code = keyCode,
+            repeat = repeatCount,
             metaState = metaState
         )
     }
@@ -483,6 +485,7 @@
         eventTime: Long,
         action: Int,
         code: Int,
+        repeat: Int,
         metaState: Int
     ) {
         synchronized(batchLock) {
@@ -492,6 +495,7 @@
                     "eventTime=$eventTime, " +
                     "action=$action, " +
                     "code=$code, " +
+                    "repeat=$repeat, " +
                     "metaState=$metaState)"
             }
 
@@ -500,7 +504,7 @@
                 /* eventTime = */ eventTime,
                 /* action = */ action,
                 /* code = */ code,
-                /* repeat = */ 0,
+                /* repeat = */ repeat,
                 /* metaState = */ metaState,
                 /* deviceId = */ KeyCharacterMap.VIRTUAL_KEYBOARD,
                 /* scancode = */ 0
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
index f13e7c8..9b17b00 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
@@ -76,6 +76,17 @@
          */
         var eventPeriodMillis = 16L
             internal set
+
+        /**
+         * The delay between a down event on a particular [Key] and the first repeat event on that
+         * same key.
+         */
+        const val InitialRepeatDelay = 500L
+
+        /**
+         * The interval between subsequent repeats (after the initial repeat) on a particular key.
+         */
+        const val SubsequentRepeatDelay = 50L
     }
 
     /**
@@ -160,13 +171,21 @@
     /**
      * Increases the current event time by [durationMillis].
      *
+     * Depending on the [keyInputState], there may be repeat key events that need to be sent within
+     * the given duration. If there are, the clock will be forwarded until it is time for the repeat
+     * key event, the key event will be sent, and then the clock will be forwarded by the remaining
+     * duration.
+     *
      * @param durationMillis The duration of the delay. Must be positive
      */
     fun advanceEventTime(durationMillis: Long = eventPeriodMillis) {
         require(durationMillis >= 0) {
             "duration of a delay can only be positive, not $durationMillis"
         }
-        currentTime += durationMillis
+
+        val endTime = currentTime + durationMillis
+        keyInputState.sendRepeatKeysIfNeeded(endTime)
+        currentTime = endTime
     }
 
     /**
@@ -602,6 +621,56 @@
     }
 
     /**
+     * Sends any and all repeat key events that are required between [currentTime] and [endTime].
+     *
+     * Mutates the value of [currentTime] in order to send each of the repeat events at exactly the
+     * time it should be sent.
+     *
+     * @param endTime All repeats set to occur before this time will be sent.
+     */
+    // TODO(b/236623354): Extend repeat key event support to [MainTestClock.advanceTimeBy].
+    private fun KeyInputState.sendRepeatKeysIfNeeded(endTime: Long) {
+
+        // Return if there is no key to repeat or if it is not yet time to repeat it.
+        if (repeatKey == null || endTime - downTime < InitialRepeatDelay) return
+
+        // Initial repeat
+        if (lastRepeatTime <= downTime) {
+            // Not yet had a repeat on this key, but it needs at least the initial one.
+            check(repeatCount == 0) {
+                "repeatCount should be reset to 0 when downTime updates"
+            }
+            repeatCount = 1
+
+            lastRepeatTime = downTime + InitialRepeatDelay
+            currentTime = lastRepeatTime
+
+            enqueueRepeat()
+        }
+
+        // Subsequent repeats
+        val numRepeats: Int = ((endTime - lastRepeatTime) / SubsequentRepeatDelay).toInt()
+
+        repeat(numRepeats) {
+            repeatCount += 1
+            lastRepeatTime += SubsequentRepeatDelay
+            currentTime = lastRepeatTime
+            enqueueRepeat()
+        }
+    }
+
+    /**
+     * Enqueues a key down event on the repeat key, if there is one. If the repeat key is null,
+     * an [IllegalStateException] is thrown.
+     */
+    private fun KeyInputState.enqueueRepeat() {
+        val repKey = checkNotNull(repeatKey) {
+            "A repeat key event cannot be sent if the repeat key is null."
+        }
+        keyInputState.enqueueDown(repKey)
+    }
+
+    /**
      * Sends all enqueued events and blocks while they are dispatched. If an exception is
      * thrown during the process, all events that haven't yet been dispatched will be dropped.
      */
@@ -716,18 +785,29 @@
 internal class KeyInputState {
     private val downKeys: HashSet<Key> = hashSetOf()
 
-    var downTime: Long = 0
-    var capsLockOn: Boolean = false
-    var numLockOn: Boolean = false
-    var scrollLockOn: Boolean = false
+    var downTime = 0L
+    var repeatKey: Key? = null
+    var repeatCount = 0
+    var lastRepeatTime = downTime
+    var capsLockOn = false
+    var numLockOn = false
+    var scrollLockOn = false
 
     fun isKeyDown(key: Key): Boolean = downKeys.contains(key)
 
-    fun setKeyUp(key: Key) = downKeys.remove(key)
+    fun setKeyUp(key: Key) {
+        downKeys.remove(key)
+        if (key == repeatKey) {
+            repeatKey = null
+            repeatCount = 0
+        }
+    }
 
     fun setKeyDown(key: Key) {
-        updateLockKeys(key)
         downKeys.add(key)
+        repeatKey = key
+        repeatCount = 0
+        updateLockKeys(key)
     }
 
     /**
@@ -740,7 +820,7 @@
      * lock will turn off immediately upon the next key down event (MacOS for example), whereas
      * other platforms (e.g. linux) wait for the next key up event before turning caps lock off.
      *
-     * This by calling this function whenever a lock key is pressed down, MacOS-like behaviour is
+     * By calling this function whenever a lock key is pressed down, MacOS-like behaviour is
      * achieved.
      */
     // TODO(Onadim): Investigate how lock key toggling is handled in Android, ChromeOS and Windows.
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInjectionScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInjectionScope.kt
index d18a314..f2fc7eb 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInjectionScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInjectionScope.kt
@@ -47,6 +47,15 @@
  * All events sent by these methods are batched together and sent as a whole after
  * [performKeyInput] has executed its code block.
  *
+ * When a key is held down - i.e. the virtual clock is forwarded whilst the key is pressed down,
+ * repeat key down events will be sent. In a fashion consistent with Android's implementation, the
+ * first repeat key event will be sent after a key has been held down for 500ms. Subsequent repeat
+ * events will be sent at 50ms intervals, until the key is released or another key is pressed down.
+ *
+ * The sending of repeat key events is handled as an implicit side-effect of [advanceEventTime],
+ * which is called within the injection scope. As such, no repeat key events will be sent if
+ * [MainTestClock.advanceTimeBy] is used to advance the time.
+ *
  * @see InjectionScope
  */
 @ExperimentalTestApi
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/KeyEventsTest.kt b/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/KeyEventsTest.kt
index 0eec57e..6e2fa87 100644
--- a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/KeyEventsTest.kt
+++ b/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/KeyEventsTest.kt
@@ -22,7 +22,9 @@
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.nativeKeyCode
 import androidx.compose.ui.test.AndroidInputDispatcher
+import androidx.compose.ui.test.InputDispatcher
 import androidx.compose.ui.test.RobolectricMinSdk
+import androidx.compose.ui.test.util.assertHasValidEventTimes
 import androidx.compose.ui.test.util.verifyKeyEvent
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.junit.Assert.assertEquals
@@ -54,39 +56,42 @@
         private const val functionKeyMetaMask = KeyEvent.META_FUNCTION_ON
 
         private val leftCtrl = Key.CtrlLeft
-        private const val leftCtrlMetaMask = KeyEvent.META_CTRL_LEFT_ON
+        private const val leftCtrlMask = KeyEvent.META_CTRL_LEFT_ON or KeyEvent.META_CTRL_ON
         private val rightCtrl = Key.CtrlRight
-        private const val rightCtrlMetaMask = KeyEvent.META_CTRL_RIGHT_ON
+        private const val rightCtrlMask = KeyEvent.META_CTRL_RIGHT_ON or KeyEvent.META_CTRL_ON
 
         private val leftAlt = Key.AltLeft
-        private const val leftAltMetaMask = KeyEvent.META_ALT_LEFT_ON
+        private const val leftAltMask = KeyEvent.META_ALT_LEFT_ON or KeyEvent.META_ALT_ON
         private val rightAlt = Key.AltRight
-        private const val rightAltMetaMask = KeyEvent.META_ALT_RIGHT_ON
+        private const val rightAltMask = KeyEvent.META_ALT_RIGHT_ON or KeyEvent.META_ALT_ON
 
         private val leftMeta = Key.MetaLeft
-        private const val leftMetaMetaMask = KeyEvent.META_META_LEFT_ON
+        private const val leftMetaMask = KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON
         private val rightMeta = Key.MetaRight
-        private const val rightMetaMetaMask = KeyEvent.META_META_RIGHT_ON
+        private const val rightMetaMask = KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON
 
         private val leftShift = Key.ShiftLeft
-        private const val leftShiftMetaMask = KeyEvent.META_SHIFT_LEFT_ON
+        private const val leftShiftMask = KeyEvent.META_SHIFT_LEFT_ON or KeyEvent.META_SHIFT_ON
         private val rightShift = Key.ShiftRight
-        private const val rightShiftMetaMask = KeyEvent.META_SHIFT_RIGHT_ON
+        private const val rightShiftMask = KeyEvent.META_SHIFT_RIGHT_ON or KeyEvent.META_SHIFT_ON
 
         private val capsLock = Key.CapsLock
-        private const val capsLockMetaMask = KeyEvent.META_CAPS_LOCK_ON
+        private const val capsLockMask = KeyEvent.META_CAPS_LOCK_ON
         private val numLock = Key.NumLock
-        private const val numLockMetaMask = KeyEvent.META_NUM_LOCK_ON
+        private const val numLockMask = KeyEvent.META_NUM_LOCK_ON
         private val scrollLock = Key.ScrollLock
-        private const val scrollLockMetaMask = KeyEvent.META_SCROLL_LOCK_ON
+        private const val scrollLockMask = KeyEvent.META_SCROLL_LOCK_ON
 
-        private const val allLockMasks = capsLockMetaMask or numLockMetaMask or scrollLockMetaMask
+        private const val allLockMasks = capsLockMask or numLockMask or scrollLockMask
 
-        private const val allMetaMasks = functionKeyMetaMask or leftCtrlMetaMask or
-            rightCtrlMetaMask or leftAltMetaMask or rightAltMetaMask or leftMetaMetaMask or
-            rightMetaMetaMask or leftShiftMetaMask or rightShiftMetaMask
+        private const val allMetaMasks = functionKeyMetaMask or leftCtrlMask or rightCtrlMask or
+            leftAltMask or rightAltMask or leftMetaMask or rightMetaMask or
+            leftShiftMask or rightShiftMask
 
         private const val allMasks = allLockMasks or allMetaMasks
+
+        private const val initialRepeatDelay = InputDispatcher.InitialRepeatDelay
+        private const val subsequentRepeatDelay = InputDispatcher.SubsequentRepeatDelay
     }
 
     @Test
@@ -135,7 +140,7 @@
 
         assertTrue(subject.isCapsLockOn)
         recorder.events.last().verifyKeyEvent(
-            keyDown, aKey.nativeKeyCode, 0, 0, capsLockMetaMask or leftShiftMetaMask
+            keyDown, aKey.nativeKeyCode, 0, 0, capsLockMask or leftShiftMask
         )
     }
 
@@ -215,47 +220,47 @@
 
     @Test
     fun leftCtrl_metaState_generatedCorrectly() =
-        verifyMetaKeyMetaState(leftCtrl, leftCtrlMetaMask)
+        verifyMetaKeyMetaState(leftCtrl, leftCtrlMask)
 
     @Test
     fun rightCtrl_metaState_generatedCorrectly() =
-        verifyMetaKeyMetaState(rightCtrl, rightCtrlMetaMask)
+        verifyMetaKeyMetaState(rightCtrl, rightCtrlMask)
 
     @Test
     fun leftAlt_metaState_generatedCorrectly() =
-        verifyMetaKeyMetaState(leftAlt, leftAltMetaMask)
+        verifyMetaKeyMetaState(leftAlt, leftAltMask)
 
     @Test
     fun rightAlt_metaState_generatedCorrectly() =
-        verifyMetaKeyMetaState(rightAlt, rightAltMetaMask)
+        verifyMetaKeyMetaState(rightAlt, rightAltMask)
 
     @Test
     fun leftMeta_metaState_generatedCorrectly() =
-        verifyMetaKeyMetaState(leftMeta, leftMetaMetaMask)
+        verifyMetaKeyMetaState(leftMeta, leftMetaMask)
 
     @Test
     fun rightMeta_metaState_generatedCorrectly() =
-        verifyMetaKeyMetaState(rightMeta, rightMetaMetaMask)
+        verifyMetaKeyMetaState(rightMeta, rightMetaMask)
 
     @Test
     fun leftShift_metaState_generatedCorrectly() =
-        verifyMetaKeyMetaState(leftShift, leftShiftMetaMask)
+        verifyMetaKeyMetaState(leftShift, leftShiftMask)
 
     @Test
     fun rightShift_metaState_generatedCorrectly() =
-        verifyMetaKeyMetaState(rightShift, rightShiftMetaMask)
+        verifyMetaKeyMetaState(rightShift, rightShiftMask)
 
     @Test
     fun capsLock_metaState_generatedCorrectly() =
-        verifyLockKeyMetaState(capsLock, capsLockMetaMask)
+        verifyLockKeyMetaState(capsLock, capsLockMask)
 
     @Test
     fun numLock_metaState_generatedCorrectly() =
-        verifyLockKeyMetaState(numLock, numLockMetaMask)
+        verifyLockKeyMetaState(numLock, numLockMask)
 
     @Test
     fun scrollLock_metaState_generatedCorrectly() =
-        verifyLockKeyMetaState(scrollLock, scrollLockMetaMask)
+        verifyLockKeyMetaState(scrollLock, scrollLockMask)
 
     @Test
     fun lockKeys_metaState_combinedCorrectly() {
@@ -264,6 +269,7 @@
         enqueueKeyPress(scrollLock)
         subject.flush()
 
+        recorder.assertHasValidEventTimes()
         assertEquals(6, recorder.events.size)
         recorder.events.last().verifyKeyEvent(
             keyUp,
@@ -285,6 +291,7 @@
         subject.enqueueKeyDown(rightShift)
         subject.flush()
 
+        recorder.assertHasValidEventTimes()
         assertEquals(9, recorder.events.size)
         recorder.events.last().verifyKeyEvent(
             keyDown, rightShift.nativeKeyCode, expectedMetaState = allMetaMasks
@@ -307,12 +314,156 @@
         subject.enqueueKeyDown(rightShift)
         subject.flush()
 
+        recorder.assertHasValidEventTimes()
         assertEquals(15, recorder.events.size)
         recorder.events.last().verifyKeyEvent(
             keyDown, rightShift.nativeKeyCode, expectedMetaState = allMasks
         )
     }
 
+    /* Repeat key tests */
+
+    @Test
+    fun noRepeat_before_repeatDelay() {
+        subject.enqueueKeyDown(aKey)
+        subject.advanceEventTime(initialRepeatDelay - 1)
+        subject.enqueueKeyUp(aKey)
+        subject.flush()
+
+        recorder.assertHasValidEventTimes()
+        assertEquals(2, recorder.events.size)
+        recorder.events.first().verifyKeyEvent(keyDown, aKey.nativeKeyCode)
+        recorder.events.last().verifyKeyEvent(
+            keyUp, aKey.nativeKeyCode,
+            expectedEventTime = initialRepeatDelay - 1
+        )
+    }
+
+    @Test
+    fun keyDownRepeats_exactlyAt_repeatDelay() {
+        subject.enqueueKeyDown(aKey)
+        subject.advanceEventTime(initialRepeatDelay)
+        subject.enqueueKeyUp(aKey)
+        subject.flush()
+
+        recorder.assertHasValidEventTimes()
+        assertEquals(3, recorder.events.size)
+        recorder.events[0].verifyKeyEvent(keyDown, aKey.nativeKeyCode)
+        recorder.events[1].verifyKeyEvent(keyDown, aKey.nativeKeyCode,
+            expectedEventTime = initialRepeatDelay, expectedRepeat = 1)
+        recorder.events[2].verifyKeyEvent(keyUp, aKey.nativeKeyCode,
+            expectedEventTime = initialRepeatDelay)
+    }
+
+    @Test
+    fun repeat_cancelledBy_newKeyDown() {
+        subject.enqueueKeyDown(aKey)
+        subject.advanceEventTime(initialRepeatDelay - 1)
+        subject.enqueueKeyDown(oneKey)
+        subject.advanceEventTime(initialRepeatDelay - 1)
+        subject.flush()
+
+        recorder.assertHasValidEventTimes()
+        assertEquals(2, recorder.events.size)
+        recorder.events[0].verifyKeyEvent(keyDown, aKey.nativeKeyCode)
+        recorder.events[1].verifyKeyEvent(keyDown, oneKey.nativeKeyCode,
+            expectedEventTime = initialRepeatDelay - 1, expectedDownTime = initialRepeatDelay - 1)
+    }
+
+    @Test
+    fun secondKeyDown_isRepeated() {
+        subject.enqueueKeyDown(aKey)
+        subject.advanceEventTime(initialRepeatDelay - 1)
+        subject.enqueueKeyDown(oneKey)
+        subject.advanceEventTime(initialRepeatDelay)
+        subject.flush()
+
+        recorder.assertHasValidEventTimes()
+        assertEquals(3, recorder.events.size)
+
+        recorder.events[0].verifyKeyEvent(keyDown, aKey.nativeKeyCode)
+        recorder.events[1].verifyKeyEvent(keyDown, oneKey.nativeKeyCode,
+            expectedEventTime = initialRepeatDelay - 1, expectedDownTime = initialRepeatDelay - 1)
+        recorder.events[2].verifyKeyEvent(
+            keyDown,
+            oneKey.nativeKeyCode,
+            expectedEventTime = 2 * initialRepeatDelay - 1,
+            expectedDownTime = initialRepeatDelay - 1,
+            expectedRepeat = 1
+        )
+    }
+
+    @Test
+    fun firstKeyDown_notRepeated_afterLatestKeyUp() {
+        subject.enqueueKeyDown(aKey)
+        subject.advanceEventTime(initialRepeatDelay - 1)
+        subject.enqueueKeyDown(oneKey)
+        subject.advanceEventTime(initialRepeatDelay)
+        subject.enqueueKeyUp(oneKey)
+        subject.advanceEventTime(initialRepeatDelay * 3) // The A key should not repeat.
+        subject.flush()
+
+        recorder.assertHasValidEventTimes()
+        assertEquals(4, recorder.events.size)
+
+        recorder.events[0].verifyKeyEvent(keyDown, aKey.nativeKeyCode)
+        recorder.events[1].verifyKeyEvent(keyDown, oneKey.nativeKeyCode,
+            expectedEventTime = initialRepeatDelay - 1, expectedDownTime = initialRepeatDelay - 1)
+        recorder.events[2].verifyKeyEvent(
+            keyDown,
+            oneKey.nativeKeyCode,
+            expectedEventTime = 2 * initialRepeatDelay - 1,
+            expectedDownTime = initialRepeatDelay - 1,
+            expectedRepeat = 1
+        )
+        recorder.events[3].verifyKeyEvent(
+            keyUp,
+            oneKey.nativeKeyCode,
+            expectedEventTime = 2 * initialRepeatDelay - 1,
+            expectedDownTime = initialRepeatDelay - 1
+        )
+    }
+
+    @Test
+    fun multipleRepeatKeyTest() {
+        subject.enqueueKeyDown(aKey) // t = 0
+        subject.advanceEventTime(initialRepeatDelay - 1) // t = 499
+        subject.flush()
+
+        assertEquals(1, recorder.events.size)
+        recorder.events.first().verifyKeyEvent(keyDown, aKey.nativeKeyCode)
+
+        subject.advanceEventTime(1) // t = 500
+        subject.flush()
+
+        assertEquals(2, recorder.events.size)
+        recorder.events.last().verifyKeyEvent(keyDown, aKey.nativeKeyCode,
+            expectedEventTime = initialRepeatDelay, expectedRepeat = 1)
+
+        subject.advanceEventTime(subsequentRepeatDelay - 1) // t = 549
+        subject.flush()
+
+        assertEquals(2, recorder.events.size)
+        subject.advanceEventTime(1) // t = 550
+        subject.flush()
+
+        assertEquals(3, recorder.events.size)
+        recorder.events.last().verifyKeyEvent(keyDown, aKey.nativeKeyCode,
+            expectedEventTime = initialRepeatDelay + subsequentRepeatDelay, expectedRepeat = 2)
+
+        subject.advanceEventTime(subsequentRepeatDelay * 10) // t = 1050
+        subject.flush()
+
+        recorder.assertHasValidEventTimes()
+        assertEquals(13, recorder.events.size)
+
+        recorder.events.drop(3).forEachIndexed { i, event ->
+            event.verifyKeyEvent(keyDown, aKey.nativeKeyCode,
+                expectedEventTime = (initialRepeatDelay + subsequentRepeatDelay * (i + 2)),
+                expectedRepeat = 3 + i)
+        }
+    }
+
     /* Negative testing. */
 
     @Test
@@ -344,6 +495,8 @@
         enqueueKeyPress(key)
         subject.flush()
 
+        recorder.assertHasValidEventTimes()
+        assertEquals(2, recorder.events.size)
         recorder.events[0].verifyKeyEvent(
             keyDown, key.nativeKeyCode, expectedMetaState = expectedMetaState
         )
@@ -355,6 +508,8 @@
         enqueueKeyPress(key)
         subject.flush()
 
+        recorder.assertHasValidEventTimes()
+        assertEquals(4, recorder.events.size)
         recorder.events[0].verifyKeyEvent(
             keyDown, key.nativeKeyCode, expectedMetaState = expectedMetaState
         )
diff --git a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/multimodal/KeyAndMouseEventsTest.kt b/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/multimodal/KeyAndMouseEventsTest.kt
index 58d6627..bf7111d 100644
--- a/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/multimodal/KeyAndMouseEventsTest.kt
+++ b/compose/ui/ui-test/src/test/kotlin/androidx/compose/ui/test/inputdispatcher/multimodal/KeyAndMouseEventsTest.kt
@@ -61,37 +61,37 @@
         private const val functionKeyMetaMask = KeyEvent.META_FUNCTION_ON
 
         private val leftCtrl = Key.CtrlLeft
-        private const val leftCtrlMetaMask = KeyEvent.META_CTRL_LEFT_ON
+        private const val leftCtrlMask = KeyEvent.META_CTRL_LEFT_ON or KeyEvent.META_CTRL_ON
         private val rightCtrl = Key.CtrlRight
-        private const val rightCtrlMetaMask = KeyEvent.META_CTRL_RIGHT_ON
+        private const val rightCtrlMask = KeyEvent.META_CTRL_RIGHT_ON or KeyEvent.META_CTRL_ON
 
         private val leftAlt = Key.AltLeft
-        private const val leftAltMetaMask = KeyEvent.META_ALT_LEFT_ON
+        private const val leftAltMask = KeyEvent.META_ALT_LEFT_ON or KeyEvent.META_ALT_ON
         private val rightAlt = Key.AltRight
-        private const val rightAltMetaMask = KeyEvent.META_ALT_RIGHT_ON
+        private const val rightAltMask = KeyEvent.META_ALT_RIGHT_ON or KeyEvent.META_ALT_ON
 
         private val leftMeta = Key.MetaLeft
-        private const val leftMetaMetaMask = KeyEvent.META_META_LEFT_ON
+        private const val leftMetaMask = KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON
         private val rightMeta = Key.MetaRight
-        private const val rightMetaMetaMask = KeyEvent.META_META_RIGHT_ON
+        private const val rightMetaMask = KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON
 
         private val leftShift = Key.ShiftLeft
-        private const val leftShiftMetaMask = KeyEvent.META_SHIFT_LEFT_ON
+        private const val leftShiftMask = KeyEvent.META_SHIFT_LEFT_ON or KeyEvent.META_SHIFT_ON
         private val rightShift = Key.ShiftRight
-        private const val rightShiftMetaMask = KeyEvent.META_SHIFT_RIGHT_ON
+        private const val rightShiftMask = KeyEvent.META_SHIFT_RIGHT_ON or KeyEvent.META_SHIFT_ON
 
         private val capsLock = Key.CapsLock
-        private const val capsLockMetaMask = KeyEvent.META_CAPS_LOCK_ON
+        private const val capsLockMask = KeyEvent.META_CAPS_LOCK_ON
         private val numLock = Key.NumLock
-        private const val numLockMetaMask = KeyEvent.META_NUM_LOCK_ON
+        private const val numLockMask = KeyEvent.META_NUM_LOCK_ON
         private val scrollLock = Key.ScrollLock
-        private const val scrollLockMetaMask = KeyEvent.META_SCROLL_LOCK_ON
+        private const val scrollLockMask = KeyEvent.META_SCROLL_LOCK_ON
 
-        private const val allLockMasks = capsLockMetaMask or numLockMetaMask or scrollLockMetaMask
+        private const val allLockMasks = capsLockMask or numLockMask or scrollLockMask
 
-        private const val allMetaMasks = functionKeyMetaMask or leftCtrlMetaMask or
-            rightCtrlMetaMask or leftAltMetaMask or rightAltMetaMask or leftMetaMetaMask or
-            rightMetaMetaMask or leftShiftMetaMask or rightShiftMetaMask
+        private const val allMetaMasks = functionKeyMetaMask or leftCtrlMask or rightCtrlMask or
+            leftAltMask or rightAltMask or leftMetaMask or rightMetaMask or
+            leftShiftMask or rightShiftMask
 
         private const val allMasks = allLockMasks or allMetaMasks
     }
@@ -102,47 +102,47 @@
 
     @Test
     fun leftCtrl_metaState_generatedCorrectly() =
-        verifyMetaKeyClickState(leftCtrl, leftCtrlMetaMask)
+        verifyMetaKeyClickState(leftCtrl, leftCtrlMask)
 
     @Test
     fun rightCtrl_metaState_generatedCorrectly() =
-        verifyMetaKeyClickState(rightCtrl, rightCtrlMetaMask)
+        verifyMetaKeyClickState(rightCtrl, rightCtrlMask)
 
     @Test
     fun leftAlt_metaState_generatedCorrectly() =
-        verifyMetaKeyClickState(leftAlt, leftAltMetaMask)
+        verifyMetaKeyClickState(leftAlt, leftAltMask)
 
     @Test
     fun rightAlt_metaState_generatedCorrectly() =
-        verifyMetaKeyClickState(rightAlt, rightAltMetaMask)
+        verifyMetaKeyClickState(rightAlt, rightAltMask)
 
     @Test
     fun leftMeta_metaState_generatedCorrectly() =
-        verifyMetaKeyClickState(leftMeta, leftMetaMetaMask)
+        verifyMetaKeyClickState(leftMeta, leftMetaMask)
 
     @Test
     fun rightMeta_metaState_generatedCorrectly() =
-        verifyMetaKeyClickState(rightMeta, rightMetaMetaMask)
+        verifyMetaKeyClickState(rightMeta, rightMetaMask)
 
     @Test
     fun leftShift_metaState_generatedCorrectly() =
-        verifyMetaKeyClickState(leftShift, leftShiftMetaMask)
+        verifyMetaKeyClickState(leftShift, leftShiftMask)
 
     @Test
     fun rightShift_metaState_generatedCorrectly() =
-        verifyMetaKeyClickState(rightShift, rightShiftMetaMask)
+        verifyMetaKeyClickState(rightShift, rightShiftMask)
 
     @Test
     fun capsLock_metaState_generatedCorrectly() =
-        verifyLockKeyClickState(capsLock, capsLockMetaMask)
+        verifyLockKeyClickState(capsLock, capsLockMask)
 
     @Test
     fun numLock_metaState_generatedCorrectly() =
-        verifyLockKeyClickState(numLock, numLockMetaMask)
+        verifyLockKeyClickState(numLock, numLockMask)
 
     @Test
     fun scrollLock_metaState_generatedCorrectly() =
-        verifyLockKeyClickState(scrollLock, scrollLockMetaMask)
+        verifyLockKeyClickState(scrollLock, scrollLockMask)
 
     @Test
     fun lockKeys_metaState_combinedCorrectly_inMousePress() {
diff --git a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
index cfbe91f..6428016 100644
--- a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
@@ -1,9 +1,9 @@
 // Signature format: 4.0
 package androidx.compose.ui.tooling {
 
-  @androidx.compose.ui.ExperimentalComposeUiApi public final class ComposableInvoker {
-    method @androidx.compose.ui.ExperimentalComposeUiApi public void invokeComposable(String className, String methodName, androidx.compose.runtime.Composer composer, java.lang.Object?... args);
-    field public static final androidx.compose.ui.tooling.ComposableInvoker INSTANCE;
+  @Deprecated @androidx.compose.ui.ExperimentalComposeUiApi public final class ComposableInvoker {
+    method @Deprecated @androidx.compose.ui.ExperimentalComposeUiApi public void invokeComposable(String className, String methodName, androidx.compose.runtime.Composer composer, java.lang.Object?... args);
+    field @Deprecated public static final androidx.compose.ui.tooling.ComposableInvoker INSTANCE;
   }
 
   public final class ComposeViewAdapterKt {
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
index 7c484d32..5d07d1a 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
@@ -49,7 +49,6 @@
 import androidx.compose.ui.platform.LocalFontLoader
 import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.text.font.createFontFamilyResolver
-import androidx.compose.ui.tooling.ComposableInvoker.invokeComposable
 import androidx.compose.ui.tooling.animation.PreviewAnimationClock
 import androidx.compose.ui.tooling.data.Group
 import androidx.compose.ui.tooling.data.SourceLocation
@@ -553,6 +552,7 @@
      * @param onCommit callback invoked after every commit of the preview composable.
      * @param onDraw callback invoked after every draw of the adapter. Only for test use.
      */
+    @Suppress("DEPRECATION")
     @OptIn(ExperimentalComposeUiApi::class)
     @VisibleForTesting
     internal fun init(
@@ -587,7 +587,7 @@
                 // class loads correctly.
                 val composable = {
                     try {
-                        invokeComposable(
+                        ComposableInvoker.invokeComposable(
                             className,
                             methodName,
                             composer,
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewActivity.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewActivity.kt
index 6e5cad2..ec5b8d9 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewActivity.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewActivity.kt
@@ -28,7 +28,6 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.tooling.ComposableInvoker.invokeComposable
 
 /**
  * Activity used to run `@Composable` previews from Android Studio.
@@ -59,6 +58,7 @@
         intent?.getStringExtra("composable")?.let { setComposableContent(it) }
     }
 
+    @Suppress("DEPRECATION")
     @OptIn(ExperimentalComposeUiApi::class)
     private fun setComposableContent(composableFqn: String) {
         Log.d(TAG, "PreviewActivity has composable $composableFqn")
@@ -71,7 +71,7 @@
         }
         Log.d(TAG, "Previewing '$methodName' without a parameter provider.")
         setContent {
-            invokeComposable(
+            ComposableInvoker.invokeComposable(
                 className,
                 methodName,
                 currentComposer
@@ -86,6 +86,7 @@
      * Otherwise, the content will display a FAB that changes the argument value on click, cycling
      * through all the values in the provider's sequence.
      */
+    @Suppress("DEPRECATION")
     @OptIn(ExperimentalComposeUiApi::class)
     private fun setParameterizedContent(
         className: String,
@@ -107,7 +108,7 @@
 
                 Scaffold(
                     content = {
-                        invokeComposable(
+                        ComposableInvoker.invokeComposable(
                             className,
                             methodName,
                             currentComposer,
@@ -124,7 +125,7 @@
             }
         } else {
             setContent {
-                invokeComposable(
+                ComposableInvoker.invokeComposable(
                     className,
                     methodName,
                     currentComposer,
diff --git a/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/runtime/NonInteractivePreviewFacade.kt b/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/runtime/NonInteractivePreviewFacade.kt
index e569bf9..5cec2cc 100644
--- a/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/runtime/NonInteractivePreviewFacade.kt
+++ b/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/runtime/NonInteractivePreviewFacade.kt
@@ -20,7 +20,6 @@
 import androidx.compose.runtime.currentComposer
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.platform.TestComposeWindow
-import androidx.compose.ui.tooling.ComposableInvoker
 import androidx.compose.ui.unit.Density
 
 /**
@@ -43,7 +42,7 @@
  *    preview classpath (a requested preview function or a requested frame size may differ).
  * 7. A rendered frame is sent back to the IDE plugin and is shown in the IDE as an image.
  */
-@Suppress("unused")
+@Suppress("DEPRECATION", "unused")
 internal class NonInteractivePreviewFacade {
     companion object {
         @JvmStatic
@@ -57,7 +56,7 @@
                 // We need to delay the reflection instantiation of the class until we are in the
                 // composable to ensure all the right initialization has happened and the Composable
                 // class loads correctly.
-                ComposableInvoker.invokeComposable(
+                androidx.compose.ui.tooling.ComposableInvoker.invokeComposable(
                     className,
                     methodName,
                     currentComposer
diff --git a/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/runtime/PreviewRunner.kt b/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/runtime/PreviewRunner.kt
index 89a9b0c..a64124c 100644
--- a/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/runtime/PreviewRunner.kt
+++ b/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/runtime/PreviewRunner.kt
@@ -19,7 +19,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.currentComposer
 import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.tooling.ComposableInvoker.invokeComposable
 import androidx.compose.ui.window.Window
 import androidx.compose.ui.window.launchApplication
 import kotlinx.coroutines.DelicateCoroutinesApi
@@ -29,6 +28,7 @@
     companion object {
         private var previewComposition: @Composable () -> Unit = {}
 
+        @Suppress("DEPRECATION")
         @JvmStatic
         @OptIn(ExperimentalComposeUiApi::class)
         fun main(args: Array<String>) {
@@ -40,7 +40,7 @@
                 // We need to delay the reflection instantiation of the class until we are in the
                 // composable to ensure all the right initialization has happened and the Composable
                 // class loads correctly.
-                invokeComposable(
+                androidx.compose.ui.tooling.ComposableInvoker.invokeComposable(
                     className,
                     methodName,
                     currentComposer
diff --git a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
index 20309f7..28d41ec 100644
--- a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
+++ b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
@@ -25,6 +25,7 @@
 /**
  * A utility object to invoke composable function by its name and containing class.
  */
+@Deprecated("Use androidx.compose.runtime.reflect.ComposableMethodInvoker instead")
 @ExperimentalComposeUiApi
 object ComposableInvoker {
 
@@ -56,7 +57,7 @@
     }
 
     private inline fun <reified T> T.dup(count: Int): Array<T> {
-        return (0..count).map { this }.toTypedArray()
+        return (0 until count).map { this }.toTypedArray()
     }
 
     /**
@@ -71,7 +72,6 @@
                 methodName,
                 *args.mapNotNull { it?.javaClass }.toTypedArray(),
                 Composer::class.java, // composer param
-                kotlin.Int::class.java, // key param
                 *kotlin.Int::class.java.dup(changedParams) // changed params
             )
         } catch (e: ReflectiveOperationException) {
@@ -97,7 +97,7 @@
         "double" -> 0.toDouble()
         "float" -> 0.toFloat()
         "boolean" -> false
-        "char" -> '0'
+        "char" -> 0.toChar()
         else -> null
     }
 
@@ -155,7 +155,7 @@
         return invoke(instance, *arguments)
     }
 
-    private const val SLOTS_PER_INT = 15
+    private const val SLOTS_PER_INT = 10
     private const val BITS_PER_INT = 31
 
     private fun changedParamCount(realValueParams: Int, thisParams: Int): Int {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 2e7abc3..a2f5105 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -60,7 +60,7 @@
 
         implementation("androidx.core:core:1.5.0")
         implementation('androidx.collection:collection:1.0.0')
-        implementation(project(":customview:customview-poolingcontainer"))
+        implementation("androidx.customview:customview-poolingcontainer:1.0.0-rc01")
         implementation("androidx.savedstate:savedstate-ktx:1.2.0-rc01")
         implementation("androidx.lifecycle:lifecycle-common-java8:2.3.0")
         implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
@@ -151,7 +151,7 @@
                 implementation("androidx.autofill:autofill:1.0.0")
                 implementation(libs.kotlinCoroutinesAndroid)
 
-                implementation(project(":customview:customview-poolingcontainer"))
+                implementation("androidx.customview:customview-poolingcontainer:1.0.0-rc01")
                 implementation("androidx.savedstate:savedstate-ktx:1.2.0-rc01")
                 implementation("androidx.lifecycle:lifecycle-common-java8:2.3.0")
                 implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
index b6add2f..2ea1689 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
@@ -241,7 +241,7 @@
         // assert
         rule.runOnIdle {
             assertThat(secondaryInspectableConnection.velocityOfferedFromChild).isEqualTo(
-                connection.velocityConsumedDownChain
+                connection.velocityConsumedDownChain + connection.velocityNotConsumedByChild
             )
         }
     }
diff --git a/core/uwb/uwb-rxjava3/api/current.txt b/core/uwb/uwb-rxjava3/api/current.txt
index e6f50d0..f5e20ab 100644
--- a/core/uwb/uwb-rxjava3/api/current.txt
+++ b/core/uwb/uwb-rxjava3/api/current.txt
@@ -1 +1,14 @@
 // Signature format: 4.0
+package androidx.core.uwb.rxjava3 {
+
+  public final class UwbClientSessionScopeRx {
+    method public static io.reactivex.rxjava3.core.Flowable<androidx.core.uwb.RangingResult> rangingResultsFlowable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters);
+    method public static io.reactivex.rxjava3.core.Observable<androidx.core.uwb.RangingResult> rangingResultsObservable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters);
+  }
+
+  public final class UwbManagerRx {
+    method public static io.reactivex.rxjava3.core.Single<androidx.core.uwb.UwbClientSessionScope> clientSessionScopeSingle(androidx.core.uwb.UwbManager);
+  }
+
+}
+
diff --git a/core/uwb/uwb-rxjava3/api/public_plus_experimental_current.txt b/core/uwb/uwb-rxjava3/api/public_plus_experimental_current.txt
index e6f50d0..f5e20ab 100644
--- a/core/uwb/uwb-rxjava3/api/public_plus_experimental_current.txt
+++ b/core/uwb/uwb-rxjava3/api/public_plus_experimental_current.txt
@@ -1 +1,14 @@
 // Signature format: 4.0
+package androidx.core.uwb.rxjava3 {
+
+  public final class UwbClientSessionScopeRx {
+    method public static io.reactivex.rxjava3.core.Flowable<androidx.core.uwb.RangingResult> rangingResultsFlowable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters);
+    method public static io.reactivex.rxjava3.core.Observable<androidx.core.uwb.RangingResult> rangingResultsObservable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters);
+  }
+
+  public final class UwbManagerRx {
+    method public static io.reactivex.rxjava3.core.Single<androidx.core.uwb.UwbClientSessionScope> clientSessionScopeSingle(androidx.core.uwb.UwbManager);
+  }
+
+}
+
diff --git a/core/uwb/uwb-rxjava3/api/restricted_current.txt b/core/uwb/uwb-rxjava3/api/restricted_current.txt
index e6f50d0..f5e20ab 100644
--- a/core/uwb/uwb-rxjava3/api/restricted_current.txt
+++ b/core/uwb/uwb-rxjava3/api/restricted_current.txt
@@ -1 +1,14 @@
 // Signature format: 4.0
+package androidx.core.uwb.rxjava3 {
+
+  public final class UwbClientSessionScopeRx {
+    method public static io.reactivex.rxjava3.core.Flowable<androidx.core.uwb.RangingResult> rangingResultsFlowable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters);
+    method public static io.reactivex.rxjava3.core.Observable<androidx.core.uwb.RangingResult> rangingResultsObservable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters);
+  }
+
+  public final class UwbManagerRx {
+    method public static io.reactivex.rxjava3.core.Single<androidx.core.uwb.UwbClientSessionScope> clientSessionScopeSingle(androidx.core.uwb.UwbManager);
+  }
+
+}
+
diff --git a/core/uwb/uwb-rxjava3/build.gradle b/core/uwb/uwb-rxjava3/build.gradle
index 24bbae8..eeb17b36 100644
--- a/core/uwb/uwb-rxjava3/build.gradle
+++ b/core/uwb/uwb-rxjava3/build.gradle
@@ -24,10 +24,29 @@
 
 dependencies {
     api(libs.kotlinStdlib)
-    // Add dependencies here
+    api(libs.rxjava3)
+    api(libs.kotlinCoroutinesRx3)
+    api(project(":core:uwb:uwb"))
+
+    androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(libs.espressoCore)
+    androidTestImplementation('com.google.android.gms:play-services-base:18.0.1')
+    androidTestImplementation('com.google.android.gms:play-services-nearby:18.2.0', {
+        exclude group: "androidx.core"
+    })
+    androidTestImplementation(libs.multidex)
 }
 
 android {
+    defaultConfig {
+        multiDexEnabled = true
+    }
+
     namespace "androidx.core.uwb.rxjava3"
 }
 
@@ -37,4 +56,4 @@
     mavenGroup = LibraryGroups.CORE_UWB
     inceptionYear = "2022"
     description = "RxJava3 integration for UWB module"
-}
+}
\ No newline at end of file
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/UwbClientSessionScopeRxTest.java b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/UwbClientSessionScopeRxTest.java
new file mode 100644
index 0000000..c2f8ad8
--- /dev/null
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/UwbClientSessionScopeRxTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022 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.core.uwb.rxjava3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.core.uwb.RangingParameters;
+import androidx.core.uwb.RangingResult;
+import androidx.core.uwb.RangingResult.RangingResultPosition;
+import androidx.core.uwb.UwbClientSessionScope;
+import androidx.core.uwb.UwbDevice;
+import androidx.core.uwb.UwbManager;
+import androidx.core.uwb.rxjava3.mock.TestUwbManager;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import io.reactivex.rxjava3.core.Flowable;
+import io.reactivex.rxjava3.core.Observable;
+import io.reactivex.rxjava3.core.Single;
+
+public class UwbClientSessionScopeRxTest {
+    private final Context context = ApplicationProvider.getApplicationContext();
+    private final UwbManager uwbManager = new TestUwbManager(context);
+    private final UwbDevice uwbDevice = UwbDevice.createForAddress(new byte[0]);
+    private final RangingParameters rangingParameters = new RangingParameters(
+            RangingParameters.UWB_CONFIG_ID_1,
+            0,
+            /*sessionKeyInfo=*/ null,
+            /*complexChannel=*/ null,
+            ImmutableList.of(uwbDevice),
+            RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC
+    );
+
+    @Test
+    public void testRangingResultObservable_returnsRangingResultObservable() {
+        Single<UwbClientSessionScope> clientSessionScopeSingle =
+                UwbManagerRx.clientSessionScopeSingle(uwbManager);
+        UwbClientSessionScope clientSessionScope = clientSessionScopeSingle.blockingGet();
+
+        Observable<RangingResult> rangingResultObservable =
+                UwbClientSessionScopeRx.rangingResultsObservable(clientSessionScope,
+                        rangingParameters);
+        RangingResult rangingResult = rangingResultObservable.blockingFirst();
+
+        assertThat(rangingResult instanceof RangingResult.RangingResultPosition).isTrue();
+        assertThat(
+                ((RangingResultPosition) rangingResult).getPosition().getDistance().getValue())
+                .isEqualTo(1.0f);
+    }
+
+    @Test
+    public void testRangingResultFlowable_returnsRangingResultFlowable() {
+        Single<UwbClientSessionScope> clientSessionScopeSingle =
+                UwbManagerRx.clientSessionScopeSingle(uwbManager);
+        UwbClientSessionScope clientSessionScope = clientSessionScopeSingle.blockingGet();
+
+        Flowable<RangingResult> rangingResultFlowable =
+                UwbClientSessionScopeRx.rangingResultsFlowable(clientSessionScope,
+                        rangingParameters);
+        RangingResult rangingResult = rangingResultFlowable.blockingFirst();
+
+        assertThat(rangingResult instanceof RangingResult.RangingResultPosition).isTrue();
+        assertThat(
+                ((RangingResultPosition) rangingResult).getPosition().getDistance().getValue())
+                .isEqualTo(1.0f);
+    }
+}
+
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/UwbManagerRxTest.java b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/UwbManagerRxTest.java
new file mode 100644
index 0000000..c510ea8
--- /dev/null
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/UwbManagerRxTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 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.core.uwb.rxjava3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.core.uwb.UwbAddress;
+import androidx.core.uwb.UwbClientSessionScope;
+import androidx.core.uwb.UwbManager;
+import androidx.core.uwb.rxjava3.mock.TestUwbManager;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Test;
+
+import io.reactivex.rxjava3.core.Single;
+
+public class UwbManagerRxTest {
+    private final Context context = ApplicationProvider.getApplicationContext();
+    private final UwbManager uwbManager = new TestUwbManager(context);
+    private final UwbAddress uwbAddress = new UwbAddress(new byte[0]);
+
+    @Test
+    public void testClientSessionScopeSingle_returnsClientSessionScope() {
+        Single<UwbClientSessionScope> clientSessionScopeSingle =
+                UwbManagerRx.clientSessionScopeSingle(uwbManager);
+
+        UwbClientSessionScope clientSessionScope = clientSessionScopeSingle.blockingGet();
+
+        assertThat(clientSessionScope).isNotNull();
+        assertThat(clientSessionScope.getLocalAddress()).isEqualTo(uwbAddress);
+    }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt
new file mode 100644
index 0000000..7bede3f
--- /dev/null
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2022 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.core.uwb.rxjava3.mock
+
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.common.api.Status
+import com.google.android.gms.common.api.internal.ApiKey
+import com.google.android.gms.nearby.uwb.RangingCapabilities
+import com.google.android.gms.nearby.uwb.RangingMeasurement
+import com.google.android.gms.nearby.uwb.RangingParameters
+import com.google.android.gms.nearby.uwb.RangingPosition
+import com.google.android.gms.nearby.uwb.RangingSessionCallback
+import com.google.android.gms.nearby.uwb.UwbAddress
+import com.google.android.gms.nearby.uwb.UwbClient
+import com.google.android.gms.nearby.uwb.UwbComplexChannel
+import com.google.android.gms.nearby.uwb.UwbDevice
+import com.google.android.gms.nearby.uwb.UwbStatusCodes
+import com.google.android.gms.nearby.uwb.zze
+import com.google.android.gms.tasks.Task
+import com.google.android.gms.tasks.Tasks
+
+/** A default implementation of [UwbClient] used in testing. */
+public class TestUwbClient(
+    val complexChannel: UwbComplexChannel,
+    val localAddress: UwbAddress,
+    val rangingCapabilities: RangingCapabilities,
+    val isAvailable: Boolean
+) : UwbClient {
+    var stopRangingCalled = false
+        private set
+    private lateinit var callback: RangingSessionCallback
+    private var startedRanging = false
+    companion object {
+        val rangingPosition = RangingPosition(
+            RangingMeasurement(1, 1.0F), null, null, 20)
+    }
+    override fun getApiKey(): ApiKey<zze> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getComplexChannel(): Task<UwbComplexChannel> {
+        return Tasks.forResult(complexChannel)
+    }
+
+    override fun getLocalAddress(): Task<UwbAddress> {
+        return Tasks.forResult(localAddress)
+    }
+
+    override fun getRangingCapabilities(): Task<RangingCapabilities> {
+        return Tasks.forResult(rangingCapabilities)
+    }
+
+    override fun isAvailable(): Task<Boolean> {
+        return Tasks.forResult(isAvailable)
+    }
+
+    override fun startRanging(
+        parameters: RangingParameters,
+        sessionCallback: RangingSessionCallback
+    ): Task<Void> {
+        if (startedRanging) {
+            throw ApiException(Status(UwbStatusCodes.RANGING_ALREADY_STARTED))
+        }
+        callback = sessionCallback
+        val peer = parameters.peerDevices.first()
+        callback.onRangingResult(peer, rangingPosition)
+        startedRanging = true
+        return Tasks.forResult(null)
+    }
+
+    override fun stopRanging(callback: RangingSessionCallback): Task<Void> {
+        if (stopRangingCalled) {
+            throw RuntimeException("Stop Ranging has already been called.")
+        }
+        stopRangingCalled = true
+        return Tasks.forResult(null)
+    }
+
+    fun disconnectPeer(device: UwbDevice) {
+        callback.onRangingSuspended(device, 0)
+    }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt
new file mode 100644
index 0000000..08c673f
--- /dev/null
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2022 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.core.uwb.rxjava3.mock
+
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingMeasurement
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbClientSessionScope
+import androidx.core.uwb.UwbDevice.Companion.createForAddress
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.nearby.uwb.RangingPosition
+import com.google.android.gms.nearby.uwb.RangingSessionCallback
+import com.google.android.gms.nearby.uwb.UwbDevice
+import com.google.common.collect.ImmutableList
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+
+/** A default implementation of [UwbClientSessionScope] used for testing. */
+class TestUwbClientSessionScope(
+    private val uwbClient: TestUwbClient,
+    override val rangingCapabilities: RangingCapabilities,
+    override val localAddress: UwbAddress
+) : UwbClientSessionScope {
+    private var sessionStarted = false
+    private val uwbDevice = createForAddress(ByteArray(0))
+    val defaultRangingParameters = RangingParameters(
+        RangingParameters.UWB_CONFIG_ID_1,
+        0,
+        null,
+        null,
+        ImmutableList.of(uwbDevice),
+        RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC
+    )
+
+    override fun prepareSession(parameters: RangingParameters) = callbackFlow {
+        if (sessionStarted) {
+            throw IllegalStateException(
+                "Ranging has already started. To initiate " +
+                    "a new ranging session, create a new client session scope."
+            )
+        }
+
+        val configId = com.google.android.gms.nearby.uwb.RangingParameters.UwbConfigId.CONFIG_ID_1
+        val updateRate =
+            com.google.android.gms.nearby.uwb.RangingParameters.RangingUpdateRate.AUTOMATIC
+        val parametersBuilder = com.google.android.gms.nearby.uwb.RangingParameters.Builder()
+            .setSessionId(defaultRangingParameters.sessionId)
+            .setUwbConfigId(configId)
+            .setRangingUpdateRate(updateRate)
+            .setSessionKeyInfo(defaultRangingParameters.sessionKeyInfo)
+        parametersBuilder.addPeerDevice(UwbDevice.createForAddress(uwbDevice.address.address))
+        val callback =
+            object : RangingSessionCallback {
+                var rangingInitialized = false
+                override fun onRangingInitialized(device: UwbDevice) {
+                    rangingInitialized = true
+                }
+
+                override fun onRangingResult(device: UwbDevice, position: RangingPosition) {
+                    trySend(
+                        RangingResult.RangingResultPosition(
+                            androidx.core.uwb.UwbDevice(UwbAddress(device.address.address)),
+                            androidx.core.uwb.RangingPosition(
+                                RangingMeasurement(position.distance.value),
+                                position.azimuth?.let {
+                                    RangingMeasurement(it.value)
+                                },
+                                position.elevation?.let {
+                                    RangingMeasurement(it.value)
+                                },
+                                position.elapsedRealtimeNanos
+                            )
+                        )
+                    )
+                }
+
+                override fun onRangingSuspended(device: UwbDevice, reason: Int) {
+                    trySend(
+                        RangingResult.RangingResultPeerDisconnected(
+                            androidx.core.uwb.UwbDevice(UwbAddress(device.address.address))
+                        )
+                    )
+                }
+            }
+
+        try {
+            uwbClient.startRanging(parametersBuilder.build(), callback)
+            sessionStarted = true
+        } catch (e: ApiException) {
+            // do nothing
+        }
+
+        awaitClose {
+            try {
+                uwbClient.stopRanging(callback)
+            } catch (e: ApiException) {
+                // do nothing
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt
new file mode 100644
index 0000000..8efce03
--- /dev/null
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 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.core.uwb.rxjava3.mock
+
+import android.content.Context
+import androidx.core.uwb.UwbClientSessionScope
+import androidx.core.uwb.UwbManager
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.RangingCapabilities
+import com.google.android.gms.nearby.uwb.UwbComplexChannel
+
+/** A default implementation of [UwbManager] used in testing. */
+class TestUwbManager(val context: Context) : UwbManager {
+    override suspend fun clientSessionScope(): UwbClientSessionScope {
+        val complexChannel = UwbComplexChannel.Builder()
+            .setPreambleIndex(0)
+            .setChannel(0)
+            .build()
+        val localAddress = com.google.android.gms.nearby.uwb.UwbAddress(ByteArray(0))
+        val rangingCapabilities =
+            com.google.android.gms.nearby.uwb.RangingCapabilities(true, false, false)
+        val uwbClient = TestUwbClient(complexChannel, localAddress, rangingCapabilities, true)
+        return TestUwbClientSessionScope(
+            uwbClient, RangingCapabilities(
+                rangingCapabilities.supportsDistance(),
+                rangingCapabilities.supportsAzimuthalAngle(),
+                rangingCapabilities.supportsElevationAngle()
+            ),
+            UwbAddress(localAddress.address)
+        )
+    }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb-rxjava3/src/main/java/androidx/core/uwb/rxjava3/UwbClientSessionScopeRx.kt b/core/uwb/uwb-rxjava3/src/main/java/androidx/core/uwb/rxjava3/UwbClientSessionScopeRx.kt
new file mode 100644
index 0000000..85c2cae
--- /dev/null
+++ b/core/uwb/uwb-rxjava3/src/main/java/androidx/core/uwb/rxjava3/UwbClientSessionScopeRx.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 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:JvmName("UwbClientSessionScopeRx")
+
+package androidx.core.uwb.rxjava3
+
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbClientSessionScope
+import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.core.Observable
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.rx3.asFlowable
+import kotlinx.coroutines.rx3.asObservable
+
+/**
+ * Returns an [Observable] stream of [RangingResult].
+ * @see UwbClientSessionScope.prepareSession
+ */
+public fun UwbClientSessionScope.rangingResultsObservable(parameters: RangingParameters):
+    Observable<RangingResult> {
+    return prepareSession(parameters).conflate().asObservable()
+}
+
+/**
+ * Returns a [Flowable] of [RangingResult].
+ * @see UwbClientSessionScope.prepareSession
+ */
+public fun UwbClientSessionScope.rangingResultsFlowable(parameters: RangingParameters):
+    Flowable<RangingResult> {
+    return prepareSession(parameters).conflate().asFlowable()
+}
\ No newline at end of file
diff --git a/core/uwb/uwb-rxjava3/src/main/java/androidx/core/uwb/rxjava3/UwbManagerRx.kt b/core/uwb/uwb-rxjava3/src/main/java/androidx/core/uwb/rxjava3/UwbManagerRx.kt
new file mode 100644
index 0000000..2a2f302
--- /dev/null
+++ b/core/uwb/uwb-rxjava3/src/main/java/androidx/core/uwb/rxjava3/UwbManagerRx.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 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:JvmName("UwbManagerRx")
+
+package androidx.core.uwb.rxjava3
+
+import androidx.core.uwb.UwbClientSessionScope
+import androidx.core.uwb.UwbManager
+import io.reactivex.rxjava3.core.Single
+import kotlinx.coroutines.rx3.rxSingle
+
+/**
+ * Returns a [Single] of [UwbClientSessionScope].
+ * @see UwbManager.clientSessionScope
+ */
+public fun UwbManager.clientSessionScopeSingle(): Single<UwbClientSessionScope> {
+    return rxSingle { clientSessionScope() }
+}
\ No newline at end of file
diff --git a/customview/customview/api/current.txt b/customview/customview/api/current.txt
index b7566cb..4850116 100644
--- a/customview/customview/api/current.txt
+++ b/customview/customview/api/current.txt
@@ -57,8 +57,8 @@
     method public boolean checkTouchSlop(int);
     method public boolean checkTouchSlop(int, int);
     method public boolean continueSettling(boolean);
-    method public static androidx.customview.widget.ViewDragHelper! create(android.view.ViewGroup, androidx.customview.widget.ViewDragHelper.Callback);
-    method public static androidx.customview.widget.ViewDragHelper! create(android.view.ViewGroup, float, androidx.customview.widget.ViewDragHelper.Callback);
+    method public static androidx.customview.widget.ViewDragHelper create(android.view.ViewGroup, androidx.customview.widget.ViewDragHelper.Callback);
+    method public static androidx.customview.widget.ViewDragHelper create(android.view.ViewGroup, float, androidx.customview.widget.ViewDragHelper.Callback);
     method public android.view.View? findTopChildUnder(int, int);
     method public void flingCapturedView(int, int, int, int);
     method public int getActivePointerId();
diff --git a/customview/customview/api/public_plus_experimental_current.txt b/customview/customview/api/public_plus_experimental_current.txt
index b7566cb..4850116 100644
--- a/customview/customview/api/public_plus_experimental_current.txt
+++ b/customview/customview/api/public_plus_experimental_current.txt
@@ -57,8 +57,8 @@
     method public boolean checkTouchSlop(int);
     method public boolean checkTouchSlop(int, int);
     method public boolean continueSettling(boolean);
-    method public static androidx.customview.widget.ViewDragHelper! create(android.view.ViewGroup, androidx.customview.widget.ViewDragHelper.Callback);
-    method public static androidx.customview.widget.ViewDragHelper! create(android.view.ViewGroup, float, androidx.customview.widget.ViewDragHelper.Callback);
+    method public static androidx.customview.widget.ViewDragHelper create(android.view.ViewGroup, androidx.customview.widget.ViewDragHelper.Callback);
+    method public static androidx.customview.widget.ViewDragHelper create(android.view.ViewGroup, float, androidx.customview.widget.ViewDragHelper.Callback);
     method public android.view.View? findTopChildUnder(int, int);
     method public void flingCapturedView(int, int, int, int);
     method public int getActivePointerId();
diff --git a/customview/customview/api/restricted_current.txt b/customview/customview/api/restricted_current.txt
index b7566cb..4850116 100644
--- a/customview/customview/api/restricted_current.txt
+++ b/customview/customview/api/restricted_current.txt
@@ -57,8 +57,8 @@
     method public boolean checkTouchSlop(int);
     method public boolean checkTouchSlop(int, int);
     method public boolean continueSettling(boolean);
-    method public static androidx.customview.widget.ViewDragHelper! create(android.view.ViewGroup, androidx.customview.widget.ViewDragHelper.Callback);
-    method public static androidx.customview.widget.ViewDragHelper! create(android.view.ViewGroup, float, androidx.customview.widget.ViewDragHelper.Callback);
+    method public static androidx.customview.widget.ViewDragHelper create(android.view.ViewGroup, androidx.customview.widget.ViewDragHelper.Callback);
+    method public static androidx.customview.widget.ViewDragHelper create(android.view.ViewGroup, float, androidx.customview.widget.ViewDragHelper.Callback);
     method public android.view.View? findTopChildUnder(int, int);
     method public void flingCapturedView(int, int, int, int);
     method public int getActivePointerId();
diff --git a/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java b/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
index adc7915..51e136d 100644
--- a/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
+++ b/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
@@ -335,7 +335,8 @@
     private static final FocusStrategy.BoundsAdapter<AccessibilityNodeInfoCompat> NODE_ADAPTER =
             new FocusStrategy.BoundsAdapter<AccessibilityNodeInfoCompat>() {
                 @Override
-                public void obtainBounds(AccessibilityNodeInfoCompat node, Rect outBounds) {
+                public void obtainBounds(@NonNull AccessibilityNodeInfoCompat node,
+                        @NonNull Rect outBounds) {
                     node.getBoundsInScreen(outBounds);
                 }
             };
diff --git a/customview/customview/src/main/java/androidx/customview/widget/FocusStrategy.java b/customview/customview/src/main/java/androidx/customview/widget/FocusStrategy.java
index 5c90d68..97b8006 100644
--- a/customview/customview/src/main/java/androidx/customview/widget/FocusStrategy.java
+++ b/customview/customview/src/main/java/androidx/customview/widget/FocusStrategy.java
@@ -440,7 +440,7 @@
      * Adapter used to obtain bounds from a generic data type.
      */
     public interface BoundsAdapter<T> {
-        void obtainBounds(T data, Rect outBounds);
+        void obtainBounds(T data, @NonNull Rect outBounds);
     }
 
     /**
diff --git a/customview/customview/src/main/java/androidx/customview/widget/ViewDragHelper.java b/customview/customview/src/main/java/androidx/customview/widget/ViewDragHelper.java
index dbaa3e0..f54eae5 100644
--- a/customview/customview/src/main/java/androidx/customview/widget/ViewDragHelper.java
+++ b/customview/customview/src/main/java/androidx/customview/widget/ViewDragHelper.java
@@ -360,6 +360,7 @@
      * @param cb Callback to provide information and receive events
      * @return a new ViewDragHelper instance
      */
+    @NonNull
     public static ViewDragHelper create(@NonNull ViewGroup forParent, @NonNull Callback cb) {
         return new ViewDragHelper(forParent.getContext(), forParent, cb);
     }
@@ -373,6 +374,7 @@
      * @param cb Callback to provide information and receive events
      * @return a new ViewDragHelper instance
      */
+    @NonNull
     public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity,
             @NonNull Callback cb) {
         final ViewDragHelper helper = create(forParent, cb);
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 0cb6521..891da00 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -1612,6 +1612,7 @@
 long androidx\.compose\.ui\.input\.pointer\.util\.VelocityTracker\.getImpulseVelocity.*
 java\.lang\.Object androidx\.glance\.state\.GlanceState\.getDataStore\(android\.content\.Context, androidx\.glance\.state\.GlanceStateDefinition, java\.lang\.String, kotlin\.coroutines\.Continuation\)
 Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint [\-A-Z]*\.
+java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\$detectDragGestures\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
 void androidx\.compose\.animation\.demos\.visualaid\.EasingFunctionDemoKt\.EasingGraph\(androidx\.compose\.animation\.core\.Easing, androidx\.compose\.ui\.Modifier, kotlinx\.coroutines\.CoroutineScope, androidx\.compose\.runtime\.Composer, int, int\)
 void androidx\.compose\.animation\.demos\.gesture\.FancyScrollingDemoKt\.FancyScrollingDemo\(androidx\.compose\.runtime\.Composer, int\)
 void androidx\.compose\.animation\.demos\.suspendfun\.SuspendDoubleTapToLikeDemoKt\.SuspendDoubleTapToLikeDemo\(androidx\.compose\.runtime\.Composer, int\)
@@ -1733,4 +1734,9 @@
 # see: https://github.com/JetBrains/kotlin/blob/master/native/commonizer/README.md
 # This warning is printed from: https://github.com/JetBrains/kotlin/blob/bc853e45e8982eff74e3263b0197c1af6086615d/native/commonizer/src/org/jetbrains/kotlin/commonizer/konan/LibraryCommonizer.kt#L41
 Warning\: No libraries found for target macos_arm[0-9]+\. This target will be excluded from commonization\.
-Warning\: No libraries found for target macos_x[0-9]+\. This target will be excluded from commonization\.
\ No newline at end of file
+Warning\: No libraries found for target macos_x[0-9]+\. This target will be excluded from commonization\.
+# > Task :tv:tv-foundation:processDebugManifest
+# > Task :tv:tv-material:processDebugManifest
+# > Task :tv:tv-material:processReleaseManifest
+# > Task :tv:tv-foundation:processReleaseManifest
+package="androidx\.tv\..*" found in source AndroidManifest\.xml: \$OUT_DIR/androidx/tv/tv\-[a-z]+/build/intermediates/tmp/ProcessLibraryManifest/[a-z]+/tempAndroidManifest[0-9]+\.xml\.
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 28d57bf..c3f41cf 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -271,6 +271,8 @@
     docs(project(":tracing:tracing-ktx"))
     docs(project(":transition:transition"))
     docs(project(":transition:transition-ktx"))
+    docs(project(":tv:tv-foundation"))
+    docs(project(":tv:tv-material"))
     docs(project(":tvprovider:tvprovider"))
     docs(project(":vectordrawable:vectordrawable"))
     docs(project(":vectordrawable:vectordrawable-animated"))
diff --git a/draganddrop/draganddrop/src/androidTest/java/androidx/draganddrop/DropHelperTest.java b/draganddrop/draganddrop/src/androidTest/java/androidx/draganddrop/DropHelperTest.java
index 8981297..d4ddce4 100644
--- a/draganddrop/draganddrop/src/androidTest/java/androidx/draganddrop/DropHelperTest.java
+++ b/draganddrop/draganddrop/src/androidTest/java/androidx/draganddrop/DropHelperTest.java
@@ -52,6 +52,7 @@
 import androidx.draganddrop.test.R;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
 
 import org.junit.Before;
@@ -64,6 +65,7 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /** Tests for {@link androidx.draganddrop.DropHelper}. */
+@LargeTest
 @RunWith(AndroidJUnit4.class)
 public class DropHelperTest {
 
diff --git a/drawerlayout/drawerlayout/lint-baseline.xml b/drawerlayout/drawerlayout/lint-baseline.xml
deleted file mode 100644
index 3d74932..0000000
--- a/drawerlayout/drawerlayout/lint-baseline.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.3.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (7.3.0-alpha07)" variant="all" version="7.3.0-alpha07">
-
-    <issue
-        id="ResourceType"
-        message="Expected resource of type styleable"
-        errorLine1="                final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);"
-        errorLine2="                                                                    ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/drawerlayout/widget/DrawerLayout.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected Parcelable onSaveInstanceState() {"
-        errorLine2="              ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/drawerlayout/widget/DrawerLayout.java"/>
-    </issue>
-
-</issues>
diff --git a/drawerlayout/drawerlayout/src/main/java/androidx/drawerlayout/widget/DrawerLayout.java b/drawerlayout/drawerlayout/src/main/java/androidx/drawerlayout/widget/DrawerLayout.java
index f577db8..c6f82fa 100644
--- a/drawerlayout/drawerlayout/src/main/java/androidx/drawerlayout/widget/DrawerLayout.java
+++ b/drawerlayout/drawerlayout/src/main/java/androidx/drawerlayout/widget/DrawerLayout.java
@@ -367,6 +367,7 @@
                         });
                 setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                         | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+                @SuppressLint("ResourceType")
                 final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);
                 try {
                     mStatusBarBackground = a.getDrawable(0);
@@ -2080,6 +2081,7 @@
         }
     }
 
+    @NonNull
     @Override
     protected Parcelable onSaveInstanceState() {
         final Parcelable superState = super.onSaveInstanceState();
diff --git a/emoji/emoji/api/restricted_current.txt b/emoji/emoji/api/restricted_current.txt
index 95072c9..dd1967a 100644
--- a/emoji/emoji/api/restricted_current.txt
+++ b/emoji/emoji/api/restricted_current.txt
@@ -85,7 +85,7 @@
     method public short getHeight();
     method public int getId();
     method public short getSdkAdded();
-    method public android.graphics.Typeface! getTypeface();
+    method public android.graphics.Typeface getTypeface();
     method public short getWidth();
     method public boolean isDefaultEmoji();
     method public void setHasGlyph(boolean);
diff --git a/emoji/emoji/src/main/java/androidx/emoji/text/EmojiMetadata.java b/emoji/emoji/src/main/java/androidx/emoji/text/EmojiMetadata.java
index ea0a187..5b61e35 100644
--- a/emoji/emoji/src/main/java/androidx/emoji/text/EmojiMetadata.java
+++ b/emoji/emoji/src/main/java/androidx/emoji/text/EmojiMetadata.java
@@ -117,7 +117,7 @@
     /**
      * @return return typeface to be used to render this metadata
      */
-    public Typeface getTypeface() {
+    public @NonNull Typeface getTypeface() {
         return mMetadataRepo.getTypeface();
     }
 
diff --git a/emoji/emoji/src/main/java/androidx/emoji/text/MetadataRepo.java b/emoji/emoji/src/main/java/androidx/emoji/text/MetadataRepo.java
index 0c5bb01..75df74a 100644
--- a/emoji/emoji/src/main/java/androidx/emoji/text/MetadataRepo.java
+++ b/emoji/emoji/src/main/java/androidx/emoji/text/MetadataRepo.java
@@ -63,7 +63,7 @@
     /**
      * Typeface to be used to render emojis.
      */
-    private final Typeface mTypeface;
+    private final @NonNull Typeface mTypeface;
 
     /**
      * Constructor used for tests.
@@ -72,7 +72,7 @@
      */
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     MetadataRepo() {
-        mTypeface = null;
+        mTypeface = Typeface.DEFAULT;
         mMetadataList = null;
         mRootNode = new Node(DEFAULT_ROOT_SIZE);
         mEmojiCharArray = new char[0];
@@ -149,7 +149,7 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP_PREFIX)
-    Typeface getTypeface() {
+    @NonNull Typeface getTypeface() {
         return mTypeface;
     }
 
diff --git a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiEditableFactoryTest.java b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiEditableFactoryTest.java
index e7862f4..a886719 100644
--- a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiEditableFactoryTest.java
+++ b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiEditableFactoryTest.java
@@ -24,10 +24,12 @@
 import static org.mockito.Mockito.mock;
 
 import android.annotation.SuppressLint;
+import android.os.Build;
 import android.text.Editable;
 import android.text.SpannableString;
 import android.text.Spanned;
 
+import androidx.annotation.RequiresApi;
 import androidx.emoji2.text.EmojiMetadata;
 import androidx.emoji2.text.EmojiSpan;
 import androidx.emoji2.text.SpannableBuilder;
@@ -58,6 +60,7 @@
         assertThat(editable, instanceOf(Editable.class));
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     @Test
     public void testNewEditable_preservesCharSequenceData() {
         final String string = "abc";
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SpannableBuilderTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SpannableBuilderTest.java
index 3e7d8ca..d0ba42d 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SpannableBuilderTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SpannableBuilderTest.java
@@ -35,6 +35,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.withSettings;
 
+import android.os.Build;
 import android.text.DynamicLayout;
 import android.text.Editable;
 import android.text.Layout;
@@ -45,6 +46,7 @@
 import android.text.style.QuoteSpan;
 import android.text.style.TypefaceSpan;
 
+import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SdkSuppress;
@@ -125,6 +127,7 @@
         assertEquals(1, start);
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     @Test
     public void testBlocksSpanCallbacks_forEmojiSpans() {
         final EmojiSpan span = mock(EmojiSpan.class);
@@ -186,6 +189,7 @@
         verify(mWatcher, times(1)).afterTextChanged(any(Editable.class));
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     @Test
     public void testDoesNotBlockSpanCallbacksForOtherWatchers() {
         final TextWatcher textWatcher = mock(TextWatcher.class);
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
index 4c241d2..ffeeb11 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
@@ -22,6 +22,7 @@
 import android.view.View
 import android.widget.TextView
 import androidx.annotation.LayoutRes
+import androidx.annotation.RequiresApi
 import androidx.core.app.SharedElementCallback
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.test.R
@@ -1679,6 +1680,7 @@
     }
 }
 
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
 class AddTransitionFragmentInActivity : FragmentActivity() {
     val fragment = TransitionFragment()
 
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java
index c5bd710..6d79fe1 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java
@@ -18,6 +18,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorInflater;
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -40,6 +41,7 @@
     private FragmentAnim() {
     }
 
+    @SuppressLint("ResourceType")
     static AnimationOrAnimator loadAnimation(@NonNull Context context,
             @NonNull Fragment fragment, boolean enter, boolean isPop) {
         int transit = fragment.getNextTransition();
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e5bafaa..3f27db1 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -121,7 +121,7 @@
 kotlinCoroutinesRx3 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx3", version.ref = "kotlinCoroutines" }
 kotlinDaemonEmbeddable = { module = "org.jetbrains.kotlin:kotlin-daemon-embeddable", version.ref = "kotlin" }
 kotlinKlibCommonizer = { module = "org.jetbrains.kotlin:kotlin-klib-commonizer", version.ref = "kotlin" }
-kotlinMetadataJvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.4.2" }
+kotlinMetadataJvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.5.0" }
 kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
 kotlinStdlibCommon = { module = "org.jetbrains.kotlin:kotlin-stdlib-common", version.ref = "kotlin" }
 kotlinStdlibJdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
diff --git a/health/health-connect-client/api/current.txt b/health/health-connect-client/api/current.txt
index 1840826..100f938 100644
--- a/health/health-connect-client/api/current.txt
+++ b/health/health-connect-client/api/current.txt
@@ -114,20 +114,20 @@
 package androidx.health.connect.client.records {
 
   public final class ActiveCaloriesBurnedRecord implements androidx.health.connect.client.records.Record {
-    ctor public ActiveCaloriesBurnedRecord(double energyKcal, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public ActiveCaloriesBurnedRecord(androidx.health.connect.client.units.Energy energy, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
-    method public double getEnergyKcal();
+    method public androidx.health.connect.client.units.Energy getEnergy();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
-    property public final double energyKcal;
+    property public final androidx.health.connect.client.units.Energy energy;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> ACTIVE_CALORIES_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ACTIVE_CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.ActiveCaloriesBurnedRecord.Companion Companion;
   }
 
@@ -149,16 +149,16 @@
   }
 
   public final class BasalMetabolicRateRecord implements androidx.health.connect.client.records.Record {
-    ctor public BasalMetabolicRateRecord(double kcalPerDay, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getKcalPerDay();
+    ctor public BasalMetabolicRateRecord(androidx.health.connect.client.units.Power basalMetabolicRate, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Power getBasalMetabolicRate();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final double kcalPerDay;
+    property public final androidx.health.connect.client.units.Power basalMetabolicRate;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> BASAL_CALORIES_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> BASAL_CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.BasalMetabolicRateRecord.Companion Companion;
   }
 
@@ -194,28 +194,28 @@
   }
 
   public final class BloodPressureRecord implements androidx.health.connect.client.records.Record {
-    ctor public BloodPressureRecord(double systolicMillimetersOfMercury, double diastolicMillimetersOfMercury, optional String? bodyPosition, optional String? measurementLocation, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public BloodPressureRecord(androidx.health.connect.client.units.Pressure systolic, androidx.health.connect.client.units.Pressure diastolic, optional String? bodyPosition, optional String? measurementLocation, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public String? getBodyPosition();
-    method public double getDiastolicMillimetersOfMercury();
+    method public androidx.health.connect.client.units.Pressure getDiastolic();
     method public String? getMeasurementLocation();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public double getSystolicMillimetersOfMercury();
+    method public androidx.health.connect.client.units.Pressure getSystolic();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
     property public final String? bodyPosition;
-    property public final double diastolicMillimetersOfMercury;
+    property public final androidx.health.connect.client.units.Pressure diastolic;
     property public final String? measurementLocation;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public final double systolicMillimetersOfMercury;
+    property public final androidx.health.connect.client.units.Pressure systolic;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
     field public static final androidx.health.connect.client.records.BloodPressureRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIASTOLIC_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIASTOLIC_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIASTOLIC_MIN;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SYSTOLIC_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SYSTOLIC_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SYSTOLIC_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MIN;
   }
 
   public static final class BloodPressureRecord.Companion {
@@ -278,12 +278,12 @@
   }
 
   public final class BoneMassRecord implements androidx.health.connect.client.records.Record {
-    ctor public BoneMassRecord(double massKg, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getMassKg();
+    ctor public BoneMassRecord(androidx.health.connect.client.units.Mass mass, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass getMass();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final double massKg;
+    property public final androidx.health.connect.client.units.Mass mass;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
@@ -680,33 +680,33 @@
   }
 
   public final class HydrationRecord implements androidx.health.connect.client.records.Record {
-    ctor public HydrationRecord(double volumeLiters, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public HydrationRecord(androidx.health.connect.client.units.Volume volume, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
-    method public double getVolumeLiters();
+    method public androidx.health.connect.client.units.Volume getVolume();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    property public final double volumeLiters;
+    property public final androidx.health.connect.client.units.Volume volume;
     field public static final androidx.health.connect.client.records.HydrationRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VOLUME_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Volume> VOLUME_TOTAL;
   }
 
   public static final class HydrationRecord.Companion {
   }
 
   public final class LeanBodyMassRecord implements androidx.health.connect.client.records.Record {
-    ctor public LeanBodyMassRecord(double massKg, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getMassKg();
+    ctor public LeanBodyMassRecord(androidx.health.connect.client.units.Mass mass, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass getMass();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final double massKg;
+    property public final androidx.health.connect.client.units.Mass mass;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
@@ -742,148 +742,148 @@
   }
 
   public final class NutritionRecord implements androidx.health.connect.client.records.Record {
-    ctor public NutritionRecord(optional double biotinGrams, optional double caffeineGrams, optional double calciumGrams, optional double kcal, optional double kcalFromFat, optional double chlorideGrams, optional double cholesterolGrams, optional double chromiumGrams, optional double copperGrams, optional double dietaryFiberGrams, optional double folateGrams, optional double folicAcidGrams, optional double iodineGrams, optional double ironGrams, optional double magnesiumGrams, optional double manganeseGrams, optional double molybdenumGrams, optional double monounsaturatedFatGrams, optional double niacinGrams, optional double pantothenicAcidGrams, optional double phosphorusGrams, optional double polyunsaturatedFatGrams, optional double potassiumGrams, optional double proteinGrams, optional double riboflavinGrams, optional double saturatedFatGrams, optional double seleniumGrams, optional double sodiumGrams, optional double sugarGrams, optional double thiaminGrams, optional double totalCarbohydrateGrams, optional double totalFatGrams, optional double transFatGrams, optional double unsaturatedFatGrams, optional double vitaminAGrams, optional double vitaminB12Grams, optional double vitaminB6Grams, optional double vitaminCGrams, optional double vitaminDGrams, optional double vitaminEGrams, optional double vitaminKGrams, optional double zincGrams, optional String? name, optional String? mealType, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getBiotinGrams();
-    method public double getCaffeineGrams();
-    method public double getCalciumGrams();
-    method public double getChlorideGrams();
-    method public double getCholesterolGrams();
-    method public double getChromiumGrams();
-    method public double getCopperGrams();
-    method public double getDietaryFiberGrams();
+    ctor public NutritionRecord(optional androidx.health.connect.client.units.Mass? biotin, optional androidx.health.connect.client.units.Mass? caffeine, optional androidx.health.connect.client.units.Mass? calcium, optional androidx.health.connect.client.units.Energy? energy, optional androidx.health.connect.client.units.Energy? energyFromFat, optional androidx.health.connect.client.units.Mass? chloride, optional androidx.health.connect.client.units.Mass? cholesterol, optional androidx.health.connect.client.units.Mass? chromium, optional androidx.health.connect.client.units.Mass? copper, optional androidx.health.connect.client.units.Mass? dietaryFiber, optional androidx.health.connect.client.units.Mass? folate, optional androidx.health.connect.client.units.Mass? folicAcid, optional androidx.health.connect.client.units.Mass? iodine, optional androidx.health.connect.client.units.Mass? iron, optional androidx.health.connect.client.units.Mass? magnesium, optional androidx.health.connect.client.units.Mass? manganese, optional androidx.health.connect.client.units.Mass? molybdenum, optional androidx.health.connect.client.units.Mass? monounsaturatedFat, optional androidx.health.connect.client.units.Mass? niacin, optional androidx.health.connect.client.units.Mass? pantothenicAcid, optional androidx.health.connect.client.units.Mass? phosphorus, optional androidx.health.connect.client.units.Mass? polyunsaturatedFat, optional androidx.health.connect.client.units.Mass? potassium, optional androidx.health.connect.client.units.Mass? protein, optional androidx.health.connect.client.units.Mass? riboflavin, optional androidx.health.connect.client.units.Mass? saturatedFat, optional androidx.health.connect.client.units.Mass? selenium, optional androidx.health.connect.client.units.Mass? sodium, optional androidx.health.connect.client.units.Mass? sugar, optional androidx.health.connect.client.units.Mass? thiamin, optional androidx.health.connect.client.units.Mass? totalCarbohydrate, optional androidx.health.connect.client.units.Mass? totalFat, optional androidx.health.connect.client.units.Mass? transFat, optional androidx.health.connect.client.units.Mass? unsaturatedFat, optional androidx.health.connect.client.units.Mass? vitaminA, optional androidx.health.connect.client.units.Mass? vitaminB12, optional androidx.health.connect.client.units.Mass? vitaminB6, optional androidx.health.connect.client.units.Mass? vitaminC, optional androidx.health.connect.client.units.Mass? vitaminD, optional androidx.health.connect.client.units.Mass? vitaminE, optional androidx.health.connect.client.units.Mass? vitaminK, optional androidx.health.connect.client.units.Mass? zinc, optional String? name, optional String? mealType, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass? getBiotin();
+    method public androidx.health.connect.client.units.Mass? getCaffeine();
+    method public androidx.health.connect.client.units.Mass? getCalcium();
+    method public androidx.health.connect.client.units.Mass? getChloride();
+    method public androidx.health.connect.client.units.Mass? getCholesterol();
+    method public androidx.health.connect.client.units.Mass? getChromium();
+    method public androidx.health.connect.client.units.Mass? getCopper();
+    method public androidx.health.connect.client.units.Mass? getDietaryFiber();
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
-    method public double getFolateGrams();
-    method public double getFolicAcidGrams();
-    method public double getIodineGrams();
-    method public double getIronGrams();
-    method public double getKcal();
-    method public double getKcalFromFat();
-    method public double getMagnesiumGrams();
-    method public double getManganeseGrams();
+    method public androidx.health.connect.client.units.Energy? getEnergy();
+    method public androidx.health.connect.client.units.Energy? getEnergyFromFat();
+    method public androidx.health.connect.client.units.Mass? getFolate();
+    method public androidx.health.connect.client.units.Mass? getFolicAcid();
+    method public androidx.health.connect.client.units.Mass? getIodine();
+    method public androidx.health.connect.client.units.Mass? getIron();
+    method public androidx.health.connect.client.units.Mass? getMagnesium();
+    method public androidx.health.connect.client.units.Mass? getManganese();
     method public String? getMealType();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public double getMolybdenumGrams();
-    method public double getMonounsaturatedFatGrams();
+    method public androidx.health.connect.client.units.Mass? getMolybdenum();
+    method public androidx.health.connect.client.units.Mass? getMonounsaturatedFat();
     method public String? getName();
-    method public double getNiacinGrams();
-    method public double getPantothenicAcidGrams();
-    method public double getPhosphorusGrams();
-    method public double getPolyunsaturatedFatGrams();
-    method public double getPotassiumGrams();
-    method public double getProteinGrams();
-    method public double getRiboflavinGrams();
-    method public double getSaturatedFatGrams();
-    method public double getSeleniumGrams();
-    method public double getSodiumGrams();
+    method public androidx.health.connect.client.units.Mass? getNiacin();
+    method public androidx.health.connect.client.units.Mass? getPantothenicAcid();
+    method public androidx.health.connect.client.units.Mass? getPhosphorus();
+    method public androidx.health.connect.client.units.Mass? getPolyunsaturatedFat();
+    method public androidx.health.connect.client.units.Mass? getPotassium();
+    method public androidx.health.connect.client.units.Mass? getProtein();
+    method public androidx.health.connect.client.units.Mass? getRiboflavin();
+    method public androidx.health.connect.client.units.Mass? getSaturatedFat();
+    method public androidx.health.connect.client.units.Mass? getSelenium();
+    method public androidx.health.connect.client.units.Mass? getSodium();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
-    method public double getSugarGrams();
-    method public double getThiaminGrams();
-    method public double getTotalCarbohydrateGrams();
-    method public double getTotalFatGrams();
-    method public double getTransFatGrams();
-    method public double getUnsaturatedFatGrams();
-    method public double getVitaminAGrams();
-    method public double getVitaminB12Grams();
-    method public double getVitaminB6Grams();
-    method public double getVitaminCGrams();
-    method public double getVitaminDGrams();
-    method public double getVitaminEGrams();
-    method public double getVitaminKGrams();
-    method public double getZincGrams();
-    property public final double biotinGrams;
-    property public final double caffeineGrams;
-    property public final double calciumGrams;
-    property public final double chlorideGrams;
-    property public final double cholesterolGrams;
-    property public final double chromiumGrams;
-    property public final double copperGrams;
-    property public final double dietaryFiberGrams;
+    method public androidx.health.connect.client.units.Mass? getSugar();
+    method public androidx.health.connect.client.units.Mass? getThiamin();
+    method public androidx.health.connect.client.units.Mass? getTotalCarbohydrate();
+    method public androidx.health.connect.client.units.Mass? getTotalFat();
+    method public androidx.health.connect.client.units.Mass? getTransFat();
+    method public androidx.health.connect.client.units.Mass? getUnsaturatedFat();
+    method public androidx.health.connect.client.units.Mass? getVitaminA();
+    method public androidx.health.connect.client.units.Mass? getVitaminB12();
+    method public androidx.health.connect.client.units.Mass? getVitaminB6();
+    method public androidx.health.connect.client.units.Mass? getVitaminC();
+    method public androidx.health.connect.client.units.Mass? getVitaminD();
+    method public androidx.health.connect.client.units.Mass? getVitaminE();
+    method public androidx.health.connect.client.units.Mass? getVitaminK();
+    method public androidx.health.connect.client.units.Mass? getZinc();
+    property public final androidx.health.connect.client.units.Mass? biotin;
+    property public final androidx.health.connect.client.units.Mass? caffeine;
+    property public final androidx.health.connect.client.units.Mass? calcium;
+    property public final androidx.health.connect.client.units.Mass? chloride;
+    property public final androidx.health.connect.client.units.Mass? cholesterol;
+    property public final androidx.health.connect.client.units.Mass? chromium;
+    property public final androidx.health.connect.client.units.Mass? copper;
+    property public final androidx.health.connect.client.units.Mass? dietaryFiber;
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
-    property public final double folateGrams;
-    property public final double folicAcidGrams;
-    property public final double iodineGrams;
-    property public final double ironGrams;
-    property public final double kcal;
-    property public final double kcalFromFat;
-    property public final double magnesiumGrams;
-    property public final double manganeseGrams;
+    property public final androidx.health.connect.client.units.Energy? energy;
+    property public final androidx.health.connect.client.units.Energy? energyFromFat;
+    property public final androidx.health.connect.client.units.Mass? folate;
+    property public final androidx.health.connect.client.units.Mass? folicAcid;
+    property public final androidx.health.connect.client.units.Mass? iodine;
+    property public final androidx.health.connect.client.units.Mass? iron;
+    property public final androidx.health.connect.client.units.Mass? magnesium;
+    property public final androidx.health.connect.client.units.Mass? manganese;
     property public final String? mealType;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public final double molybdenumGrams;
-    property public final double monounsaturatedFatGrams;
+    property public final androidx.health.connect.client.units.Mass? molybdenum;
+    property public final androidx.health.connect.client.units.Mass? monounsaturatedFat;
     property public final String? name;
-    property public final double niacinGrams;
-    property public final double pantothenicAcidGrams;
-    property public final double phosphorusGrams;
-    property public final double polyunsaturatedFatGrams;
-    property public final double potassiumGrams;
-    property public final double proteinGrams;
-    property public final double riboflavinGrams;
-    property public final double saturatedFatGrams;
-    property public final double seleniumGrams;
-    property public final double sodiumGrams;
+    property public final androidx.health.connect.client.units.Mass? niacin;
+    property public final androidx.health.connect.client.units.Mass? pantothenicAcid;
+    property public final androidx.health.connect.client.units.Mass? phosphorus;
+    property public final androidx.health.connect.client.units.Mass? polyunsaturatedFat;
+    property public final androidx.health.connect.client.units.Mass? potassium;
+    property public final androidx.health.connect.client.units.Mass? protein;
+    property public final androidx.health.connect.client.units.Mass? riboflavin;
+    property public final androidx.health.connect.client.units.Mass? saturatedFat;
+    property public final androidx.health.connect.client.units.Mass? selenium;
+    property public final androidx.health.connect.client.units.Mass? sodium;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    property public final double sugarGrams;
-    property public final double thiaminGrams;
-    property public final double totalCarbohydrateGrams;
-    property public final double totalFatGrams;
-    property public final double transFatGrams;
-    property public final double unsaturatedFatGrams;
-    property public final double vitaminAGrams;
-    property public final double vitaminB12Grams;
-    property public final double vitaminB6Grams;
-    property public final double vitaminCGrams;
-    property public final double vitaminDGrams;
-    property public final double vitaminEGrams;
-    property public final double vitaminKGrams;
-    property public final double zincGrams;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> BIOTIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CAFFEINE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALCIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALORIES_FROM_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALORIES_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CHLORIDE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CHOLESTEROL_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CHROMIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> COPPER_TOTAL;
+    property public final androidx.health.connect.client.units.Mass? sugar;
+    property public final androidx.health.connect.client.units.Mass? thiamin;
+    property public final androidx.health.connect.client.units.Mass? totalCarbohydrate;
+    property public final androidx.health.connect.client.units.Mass? totalFat;
+    property public final androidx.health.connect.client.units.Mass? transFat;
+    property public final androidx.health.connect.client.units.Mass? unsaturatedFat;
+    property public final androidx.health.connect.client.units.Mass? vitaminA;
+    property public final androidx.health.connect.client.units.Mass? vitaminB12;
+    property public final androidx.health.connect.client.units.Mass? vitaminB6;
+    property public final androidx.health.connect.client.units.Mass? vitaminC;
+    property public final androidx.health.connect.client.units.Mass? vitaminD;
+    property public final androidx.health.connect.client.units.Mass? vitaminE;
+    property public final androidx.health.connect.client.units.Mass? vitaminK;
+    property public final androidx.health.connect.client.units.Mass? zinc;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> BIOTIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CAFFEINE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CALCIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CHLORIDE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CHOLESTEROL_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CHROMIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> COPPER_TOTAL;
     field public static final androidx.health.connect.client.records.NutritionRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIETARY_FIBER_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> FOLATE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> FOLIC_ACID_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> IODINE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> IRON_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MAGNESIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MANGANESE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MOLYBDENUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MONOUNSATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> NIACIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> PANTOTHENIC_ACID_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> PHOSPHORUS_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> POLYUNSATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> POTASSIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> PROTEIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> RIBOFLAVIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SELENIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SODIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SUGAR_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> THIAMIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> TOTAL_CARBOHYDRATE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> TOTAL_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> TRANS_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> UNSATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_A_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_B12_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_B6_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_C_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_D_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_E_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_K_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> ZINC_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> DIETARY_FIBER_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ENERGY_FROM_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ENERGY_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> FOLATE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> FOLIC_ACID_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> IODINE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> IRON_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MAGNESIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MANGANESE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MOLYBDENUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MONOUNSATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> NIACIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> PANTOTHENIC_ACID_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> PHOSPHORUS_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> POLYUNSATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> POTASSIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> PROTEIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> RIBOFLAVIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SELENIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SODIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SUGAR_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> THIAMIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> TOTAL_CARBOHYDRATE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> TOTAL_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> TRANS_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> UNSATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_A_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_B12_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_B6_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_C_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_D_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_E_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_K_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> ZINC_TOTAL;
   }
 
   public static final class NutritionRecord.Companion {
@@ -921,37 +921,37 @@
     property public java.time.ZoneOffset? zoneOffset;
   }
 
-  public final class Power {
-    ctor public Power(java.time.Instant time, @FloatRange(from=0.0, to=100000.0) double watts);
-    method public java.time.Instant getTime();
-    method public double getWatts();
-    property public final java.time.Instant time;
-    property public final double watts;
-  }
-
   public final class PowerRecord implements androidx.health.connect.client.records.Record {
-    ctor public PowerRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.Power> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public PowerRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List<androidx.health.connect.client.records.Power> getSamples();
+    method public java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List<androidx.health.connect.client.records.Power> samples;
+    property public java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.PowerRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WATTS_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WATTS_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WATTS_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> POWER_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> POWER_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> POWER_MIN;
   }
 
   public static final class PowerRecord.Companion {
   }
 
+  public static final class PowerRecord.Sample {
+    ctor public PowerRecord.Sample(java.time.Instant time, androidx.health.connect.client.units.Power power);
+    method public androidx.health.connect.client.units.Power getPower();
+    method public java.time.Instant getTime();
+    property public final androidx.health.connect.client.units.Power power;
+    property public final java.time.Instant time;
+  }
+
   public interface Record {
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     property public abstract androidx.health.connect.client.records.metadata.Metadata metadata;
@@ -1176,21 +1176,21 @@
   }
 
   public final class TotalCaloriesBurnedRecord implements androidx.health.connect.client.records.Record {
-    ctor public TotalCaloriesBurnedRecord(double energyKcal, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public TotalCaloriesBurnedRecord(androidx.health.connect.client.units.Energy energy, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
-    method public double getEnergyKcal();
+    method public androidx.health.connect.client.units.Energy getEnergy();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
-    property public final double energyKcal;
+    property public final androidx.health.connect.client.units.Energy energy;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.TotalCaloriesBurnedRecord.Companion Companion;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ENERGY_TOTAL;
   }
 
   public static final class TotalCaloriesBurnedRecord.Companion {
@@ -1233,19 +1233,19 @@
   }
 
   public final class WeightRecord implements androidx.health.connect.client.records.Record {
-    ctor public WeightRecord(double weightKg, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public WeightRecord(androidx.health.connect.client.units.Mass weight, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
-    method public double getWeightKg();
+    method public androidx.health.connect.client.units.Mass getWeight();
     method public java.time.ZoneOffset? getZoneOffset();
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
-    property public final double weightKg;
+    property public final androidx.health.connect.client.units.Mass weight;
     property public java.time.ZoneOffset? zoneOffset;
     field public static final androidx.health.connect.client.records.WeightRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WEIGHT_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WEIGHT_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WEIGHT_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> WEIGHT_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> WEIGHT_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> WEIGHT_MIN;
   }
 
   public static final class WeightRecord.Companion {
@@ -1404,6 +1404,33 @@
 
 package androidx.health.connect.client.units {
 
+  public final class Energy implements java.lang.Comparable<androidx.health.connect.client.units.Energy> {
+    method public static androidx.health.connect.client.units.Energy calories(double value);
+    method public int compareTo(androidx.health.connect.client.units.Energy other);
+    method public double getCalories();
+    method public double getJoules();
+    method public double getKilocalories();
+    method public double getKilojoules();
+    method public static androidx.health.connect.client.units.Energy joules(double value);
+    method public static androidx.health.connect.client.units.Energy kilocalories(double value);
+    method public static androidx.health.connect.client.units.Energy kilojoules(double value);
+    property public final double inCalories;
+    property public final double inJoules;
+    property public final double inKilocalories;
+    property public final double inKilojoules;
+    field public static final androidx.health.connect.client.units.Energy.Companion Companion;
+  }
+
+  public static final class Energy.Companion {
+    method public androidx.health.connect.client.units.Energy calories(double value);
+    method public androidx.health.connect.client.units.Energy joules(double value);
+    method public androidx.health.connect.client.units.Energy kilocalories(double value);
+    method public androidx.health.connect.client.units.Energy kilojoules(double value);
+  }
+
+  public final class EnergyKt {
+  }
+
   public final class Length implements java.lang.Comparable<androidx.health.connect.client.units.Length> {
     method public int compareTo(androidx.health.connect.client.units.Length other);
     method public static androidx.health.connect.client.units.Length feet(double value);
@@ -1435,6 +1462,71 @@
   public final class LengthKt {
   }
 
+  public final class Mass implements java.lang.Comparable<androidx.health.connect.client.units.Mass> {
+    method public int compareTo(androidx.health.connect.client.units.Mass other);
+    method public double getGrams();
+    method public double getKilograms();
+    method public double getMicrograms();
+    method public double getMilligrams();
+    method public double getOunces();
+    method public static androidx.health.connect.client.units.Mass grams(double value);
+    method public static androidx.health.connect.client.units.Mass kilograms(double value);
+    method public static androidx.health.connect.client.units.Mass micrograms(double value);
+    method public static androidx.health.connect.client.units.Mass milligrams(double value);
+    method public static androidx.health.connect.client.units.Mass ounces(double value);
+    property public final double inGrams;
+    property public final double inKilograms;
+    property public final double inMicrograms;
+    property public final double inMilligrams;
+    property public final double inOunces;
+    field public static final androidx.health.connect.client.units.Mass.Companion Companion;
+  }
+
+  public static final class Mass.Companion {
+    method public androidx.health.connect.client.units.Mass grams(double value);
+    method public androidx.health.connect.client.units.Mass kilograms(double value);
+    method public androidx.health.connect.client.units.Mass micrograms(double value);
+    method public androidx.health.connect.client.units.Mass milligrams(double value);
+    method public androidx.health.connect.client.units.Mass ounces(double value);
+  }
+
+  public final class MassKt {
+  }
+
+  public final class Power implements java.lang.Comparable<androidx.health.connect.client.units.Power> {
+    method public int compareTo(androidx.health.connect.client.units.Power other);
+    method public double getKilocaloriesPerDay();
+    method public double getWatts();
+    method public static androidx.health.connect.client.units.Power kilocaloriesPerDay(double value);
+    method public static androidx.health.connect.client.units.Power watts(double value);
+    property public final double inKilocaloriesPerDay;
+    property public final double inWatts;
+    field public static final androidx.health.connect.client.units.Power.Companion Companion;
+  }
+
+  public static final class Power.Companion {
+    method public androidx.health.connect.client.units.Power kilocaloriesPerDay(double value);
+    method public androidx.health.connect.client.units.Power watts(double value);
+  }
+
+  public final class PowerKt {
+  }
+
+  public final class Pressure implements java.lang.Comparable<androidx.health.connect.client.units.Pressure> {
+    method public int compareTo(androidx.health.connect.client.units.Pressure other);
+    method public double getMillimetersOfMercury();
+    method public static androidx.health.connect.client.units.Pressure millimetersOfMercury(double value);
+    property public final double inMillimetersOfMercury;
+    field public static final androidx.health.connect.client.units.Pressure.Companion Companion;
+  }
+
+  public static final class Pressure.Companion {
+    method public androidx.health.connect.client.units.Pressure millimetersOfMercury(double value);
+  }
+
+  public final class PressureKt {
+  }
+
   public final class Temperature implements java.lang.Comparable<androidx.health.connect.client.units.Temperature> {
     method public static androidx.health.connect.client.units.Temperature celsius(double value);
     method public int compareTo(androidx.health.connect.client.units.Temperature other);
@@ -1454,5 +1546,24 @@
   public final class TemperatureKt {
   }
 
+  public final class Volume implements java.lang.Comparable<androidx.health.connect.client.units.Volume> {
+    method public int compareTo(androidx.health.connect.client.units.Volume other);
+    method public double getLiters();
+    method public double getMilliliters();
+    method public static androidx.health.connect.client.units.Volume liters(double value);
+    method public static androidx.health.connect.client.units.Volume milliliters(double value);
+    property public final double inLiters;
+    property public final double inMilliliters;
+    field public static final androidx.health.connect.client.units.Volume.Companion Companion;
+  }
+
+  public static final class Volume.Companion {
+    method public androidx.health.connect.client.units.Volume liters(double value);
+    method public androidx.health.connect.client.units.Volume milliliters(double value);
+  }
+
+  public final class VolumeKt {
+  }
+
 }
 
diff --git a/health/health-connect-client/api/public_plus_experimental_current.txt b/health/health-connect-client/api/public_plus_experimental_current.txt
index 1840826..100f938 100644
--- a/health/health-connect-client/api/public_plus_experimental_current.txt
+++ b/health/health-connect-client/api/public_plus_experimental_current.txt
@@ -114,20 +114,20 @@
 package androidx.health.connect.client.records {
 
   public final class ActiveCaloriesBurnedRecord implements androidx.health.connect.client.records.Record {
-    ctor public ActiveCaloriesBurnedRecord(double energyKcal, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public ActiveCaloriesBurnedRecord(androidx.health.connect.client.units.Energy energy, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
-    method public double getEnergyKcal();
+    method public androidx.health.connect.client.units.Energy getEnergy();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
-    property public final double energyKcal;
+    property public final androidx.health.connect.client.units.Energy energy;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> ACTIVE_CALORIES_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ACTIVE_CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.ActiveCaloriesBurnedRecord.Companion Companion;
   }
 
@@ -149,16 +149,16 @@
   }
 
   public final class BasalMetabolicRateRecord implements androidx.health.connect.client.records.Record {
-    ctor public BasalMetabolicRateRecord(double kcalPerDay, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getKcalPerDay();
+    ctor public BasalMetabolicRateRecord(androidx.health.connect.client.units.Power basalMetabolicRate, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Power getBasalMetabolicRate();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final double kcalPerDay;
+    property public final androidx.health.connect.client.units.Power basalMetabolicRate;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> BASAL_CALORIES_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> BASAL_CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.BasalMetabolicRateRecord.Companion Companion;
   }
 
@@ -194,28 +194,28 @@
   }
 
   public final class BloodPressureRecord implements androidx.health.connect.client.records.Record {
-    ctor public BloodPressureRecord(double systolicMillimetersOfMercury, double diastolicMillimetersOfMercury, optional String? bodyPosition, optional String? measurementLocation, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public BloodPressureRecord(androidx.health.connect.client.units.Pressure systolic, androidx.health.connect.client.units.Pressure diastolic, optional String? bodyPosition, optional String? measurementLocation, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public String? getBodyPosition();
-    method public double getDiastolicMillimetersOfMercury();
+    method public androidx.health.connect.client.units.Pressure getDiastolic();
     method public String? getMeasurementLocation();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public double getSystolicMillimetersOfMercury();
+    method public androidx.health.connect.client.units.Pressure getSystolic();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
     property public final String? bodyPosition;
-    property public final double diastolicMillimetersOfMercury;
+    property public final androidx.health.connect.client.units.Pressure diastolic;
     property public final String? measurementLocation;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public final double systolicMillimetersOfMercury;
+    property public final androidx.health.connect.client.units.Pressure systolic;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
     field public static final androidx.health.connect.client.records.BloodPressureRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIASTOLIC_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIASTOLIC_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIASTOLIC_MIN;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SYSTOLIC_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SYSTOLIC_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SYSTOLIC_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MIN;
   }
 
   public static final class BloodPressureRecord.Companion {
@@ -278,12 +278,12 @@
   }
 
   public final class BoneMassRecord implements androidx.health.connect.client.records.Record {
-    ctor public BoneMassRecord(double massKg, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getMassKg();
+    ctor public BoneMassRecord(androidx.health.connect.client.units.Mass mass, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass getMass();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final double massKg;
+    property public final androidx.health.connect.client.units.Mass mass;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
@@ -680,33 +680,33 @@
   }
 
   public final class HydrationRecord implements androidx.health.connect.client.records.Record {
-    ctor public HydrationRecord(double volumeLiters, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public HydrationRecord(androidx.health.connect.client.units.Volume volume, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
-    method public double getVolumeLiters();
+    method public androidx.health.connect.client.units.Volume getVolume();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    property public final double volumeLiters;
+    property public final androidx.health.connect.client.units.Volume volume;
     field public static final androidx.health.connect.client.records.HydrationRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VOLUME_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Volume> VOLUME_TOTAL;
   }
 
   public static final class HydrationRecord.Companion {
   }
 
   public final class LeanBodyMassRecord implements androidx.health.connect.client.records.Record {
-    ctor public LeanBodyMassRecord(double massKg, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getMassKg();
+    ctor public LeanBodyMassRecord(androidx.health.connect.client.units.Mass mass, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass getMass();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final double massKg;
+    property public final androidx.health.connect.client.units.Mass mass;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
@@ -742,148 +742,148 @@
   }
 
   public final class NutritionRecord implements androidx.health.connect.client.records.Record {
-    ctor public NutritionRecord(optional double biotinGrams, optional double caffeineGrams, optional double calciumGrams, optional double kcal, optional double kcalFromFat, optional double chlorideGrams, optional double cholesterolGrams, optional double chromiumGrams, optional double copperGrams, optional double dietaryFiberGrams, optional double folateGrams, optional double folicAcidGrams, optional double iodineGrams, optional double ironGrams, optional double magnesiumGrams, optional double manganeseGrams, optional double molybdenumGrams, optional double monounsaturatedFatGrams, optional double niacinGrams, optional double pantothenicAcidGrams, optional double phosphorusGrams, optional double polyunsaturatedFatGrams, optional double potassiumGrams, optional double proteinGrams, optional double riboflavinGrams, optional double saturatedFatGrams, optional double seleniumGrams, optional double sodiumGrams, optional double sugarGrams, optional double thiaminGrams, optional double totalCarbohydrateGrams, optional double totalFatGrams, optional double transFatGrams, optional double unsaturatedFatGrams, optional double vitaminAGrams, optional double vitaminB12Grams, optional double vitaminB6Grams, optional double vitaminCGrams, optional double vitaminDGrams, optional double vitaminEGrams, optional double vitaminKGrams, optional double zincGrams, optional String? name, optional String? mealType, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getBiotinGrams();
-    method public double getCaffeineGrams();
-    method public double getCalciumGrams();
-    method public double getChlorideGrams();
-    method public double getCholesterolGrams();
-    method public double getChromiumGrams();
-    method public double getCopperGrams();
-    method public double getDietaryFiberGrams();
+    ctor public NutritionRecord(optional androidx.health.connect.client.units.Mass? biotin, optional androidx.health.connect.client.units.Mass? caffeine, optional androidx.health.connect.client.units.Mass? calcium, optional androidx.health.connect.client.units.Energy? energy, optional androidx.health.connect.client.units.Energy? energyFromFat, optional androidx.health.connect.client.units.Mass? chloride, optional androidx.health.connect.client.units.Mass? cholesterol, optional androidx.health.connect.client.units.Mass? chromium, optional androidx.health.connect.client.units.Mass? copper, optional androidx.health.connect.client.units.Mass? dietaryFiber, optional androidx.health.connect.client.units.Mass? folate, optional androidx.health.connect.client.units.Mass? folicAcid, optional androidx.health.connect.client.units.Mass? iodine, optional androidx.health.connect.client.units.Mass? iron, optional androidx.health.connect.client.units.Mass? magnesium, optional androidx.health.connect.client.units.Mass? manganese, optional androidx.health.connect.client.units.Mass? molybdenum, optional androidx.health.connect.client.units.Mass? monounsaturatedFat, optional androidx.health.connect.client.units.Mass? niacin, optional androidx.health.connect.client.units.Mass? pantothenicAcid, optional androidx.health.connect.client.units.Mass? phosphorus, optional androidx.health.connect.client.units.Mass? polyunsaturatedFat, optional androidx.health.connect.client.units.Mass? potassium, optional androidx.health.connect.client.units.Mass? protein, optional androidx.health.connect.client.units.Mass? riboflavin, optional androidx.health.connect.client.units.Mass? saturatedFat, optional androidx.health.connect.client.units.Mass? selenium, optional androidx.health.connect.client.units.Mass? sodium, optional androidx.health.connect.client.units.Mass? sugar, optional androidx.health.connect.client.units.Mass? thiamin, optional androidx.health.connect.client.units.Mass? totalCarbohydrate, optional androidx.health.connect.client.units.Mass? totalFat, optional androidx.health.connect.client.units.Mass? transFat, optional androidx.health.connect.client.units.Mass? unsaturatedFat, optional androidx.health.connect.client.units.Mass? vitaminA, optional androidx.health.connect.client.units.Mass? vitaminB12, optional androidx.health.connect.client.units.Mass? vitaminB6, optional androidx.health.connect.client.units.Mass? vitaminC, optional androidx.health.connect.client.units.Mass? vitaminD, optional androidx.health.connect.client.units.Mass? vitaminE, optional androidx.health.connect.client.units.Mass? vitaminK, optional androidx.health.connect.client.units.Mass? zinc, optional String? name, optional String? mealType, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass? getBiotin();
+    method public androidx.health.connect.client.units.Mass? getCaffeine();
+    method public androidx.health.connect.client.units.Mass? getCalcium();
+    method public androidx.health.connect.client.units.Mass? getChloride();
+    method public androidx.health.connect.client.units.Mass? getCholesterol();
+    method public androidx.health.connect.client.units.Mass? getChromium();
+    method public androidx.health.connect.client.units.Mass? getCopper();
+    method public androidx.health.connect.client.units.Mass? getDietaryFiber();
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
-    method public double getFolateGrams();
-    method public double getFolicAcidGrams();
-    method public double getIodineGrams();
-    method public double getIronGrams();
-    method public double getKcal();
-    method public double getKcalFromFat();
-    method public double getMagnesiumGrams();
-    method public double getManganeseGrams();
+    method public androidx.health.connect.client.units.Energy? getEnergy();
+    method public androidx.health.connect.client.units.Energy? getEnergyFromFat();
+    method public androidx.health.connect.client.units.Mass? getFolate();
+    method public androidx.health.connect.client.units.Mass? getFolicAcid();
+    method public androidx.health.connect.client.units.Mass? getIodine();
+    method public androidx.health.connect.client.units.Mass? getIron();
+    method public androidx.health.connect.client.units.Mass? getMagnesium();
+    method public androidx.health.connect.client.units.Mass? getManganese();
     method public String? getMealType();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public double getMolybdenumGrams();
-    method public double getMonounsaturatedFatGrams();
+    method public androidx.health.connect.client.units.Mass? getMolybdenum();
+    method public androidx.health.connect.client.units.Mass? getMonounsaturatedFat();
     method public String? getName();
-    method public double getNiacinGrams();
-    method public double getPantothenicAcidGrams();
-    method public double getPhosphorusGrams();
-    method public double getPolyunsaturatedFatGrams();
-    method public double getPotassiumGrams();
-    method public double getProteinGrams();
-    method public double getRiboflavinGrams();
-    method public double getSaturatedFatGrams();
-    method public double getSeleniumGrams();
-    method public double getSodiumGrams();
+    method public androidx.health.connect.client.units.Mass? getNiacin();
+    method public androidx.health.connect.client.units.Mass? getPantothenicAcid();
+    method public androidx.health.connect.client.units.Mass? getPhosphorus();
+    method public androidx.health.connect.client.units.Mass? getPolyunsaturatedFat();
+    method public androidx.health.connect.client.units.Mass? getPotassium();
+    method public androidx.health.connect.client.units.Mass? getProtein();
+    method public androidx.health.connect.client.units.Mass? getRiboflavin();
+    method public androidx.health.connect.client.units.Mass? getSaturatedFat();
+    method public androidx.health.connect.client.units.Mass? getSelenium();
+    method public androidx.health.connect.client.units.Mass? getSodium();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
-    method public double getSugarGrams();
-    method public double getThiaminGrams();
-    method public double getTotalCarbohydrateGrams();
-    method public double getTotalFatGrams();
-    method public double getTransFatGrams();
-    method public double getUnsaturatedFatGrams();
-    method public double getVitaminAGrams();
-    method public double getVitaminB12Grams();
-    method public double getVitaminB6Grams();
-    method public double getVitaminCGrams();
-    method public double getVitaminDGrams();
-    method public double getVitaminEGrams();
-    method public double getVitaminKGrams();
-    method public double getZincGrams();
-    property public final double biotinGrams;
-    property public final double caffeineGrams;
-    property public final double calciumGrams;
-    property public final double chlorideGrams;
-    property public final double cholesterolGrams;
-    property public final double chromiumGrams;
-    property public final double copperGrams;
-    property public final double dietaryFiberGrams;
+    method public androidx.health.connect.client.units.Mass? getSugar();
+    method public androidx.health.connect.client.units.Mass? getThiamin();
+    method public androidx.health.connect.client.units.Mass? getTotalCarbohydrate();
+    method public androidx.health.connect.client.units.Mass? getTotalFat();
+    method public androidx.health.connect.client.units.Mass? getTransFat();
+    method public androidx.health.connect.client.units.Mass? getUnsaturatedFat();
+    method public androidx.health.connect.client.units.Mass? getVitaminA();
+    method public androidx.health.connect.client.units.Mass? getVitaminB12();
+    method public androidx.health.connect.client.units.Mass? getVitaminB6();
+    method public androidx.health.connect.client.units.Mass? getVitaminC();
+    method public androidx.health.connect.client.units.Mass? getVitaminD();
+    method public androidx.health.connect.client.units.Mass? getVitaminE();
+    method public androidx.health.connect.client.units.Mass? getVitaminK();
+    method public androidx.health.connect.client.units.Mass? getZinc();
+    property public final androidx.health.connect.client.units.Mass? biotin;
+    property public final androidx.health.connect.client.units.Mass? caffeine;
+    property public final androidx.health.connect.client.units.Mass? calcium;
+    property public final androidx.health.connect.client.units.Mass? chloride;
+    property public final androidx.health.connect.client.units.Mass? cholesterol;
+    property public final androidx.health.connect.client.units.Mass? chromium;
+    property public final androidx.health.connect.client.units.Mass? copper;
+    property public final androidx.health.connect.client.units.Mass? dietaryFiber;
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
-    property public final double folateGrams;
-    property public final double folicAcidGrams;
-    property public final double iodineGrams;
-    property public final double ironGrams;
-    property public final double kcal;
-    property public final double kcalFromFat;
-    property public final double magnesiumGrams;
-    property public final double manganeseGrams;
+    property public final androidx.health.connect.client.units.Energy? energy;
+    property public final androidx.health.connect.client.units.Energy? energyFromFat;
+    property public final androidx.health.connect.client.units.Mass? folate;
+    property public final androidx.health.connect.client.units.Mass? folicAcid;
+    property public final androidx.health.connect.client.units.Mass? iodine;
+    property public final androidx.health.connect.client.units.Mass? iron;
+    property public final androidx.health.connect.client.units.Mass? magnesium;
+    property public final androidx.health.connect.client.units.Mass? manganese;
     property public final String? mealType;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public final double molybdenumGrams;
-    property public final double monounsaturatedFatGrams;
+    property public final androidx.health.connect.client.units.Mass? molybdenum;
+    property public final androidx.health.connect.client.units.Mass? monounsaturatedFat;
     property public final String? name;
-    property public final double niacinGrams;
-    property public final double pantothenicAcidGrams;
-    property public final double phosphorusGrams;
-    property public final double polyunsaturatedFatGrams;
-    property public final double potassiumGrams;
-    property public final double proteinGrams;
-    property public final double riboflavinGrams;
-    property public final double saturatedFatGrams;
-    property public final double seleniumGrams;
-    property public final double sodiumGrams;
+    property public final androidx.health.connect.client.units.Mass? niacin;
+    property public final androidx.health.connect.client.units.Mass? pantothenicAcid;
+    property public final androidx.health.connect.client.units.Mass? phosphorus;
+    property public final androidx.health.connect.client.units.Mass? polyunsaturatedFat;
+    property public final androidx.health.connect.client.units.Mass? potassium;
+    property public final androidx.health.connect.client.units.Mass? protein;
+    property public final androidx.health.connect.client.units.Mass? riboflavin;
+    property public final androidx.health.connect.client.units.Mass? saturatedFat;
+    property public final androidx.health.connect.client.units.Mass? selenium;
+    property public final androidx.health.connect.client.units.Mass? sodium;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    property public final double sugarGrams;
-    property public final double thiaminGrams;
-    property public final double totalCarbohydrateGrams;
-    property public final double totalFatGrams;
-    property public final double transFatGrams;
-    property public final double unsaturatedFatGrams;
-    property public final double vitaminAGrams;
-    property public final double vitaminB12Grams;
-    property public final double vitaminB6Grams;
-    property public final double vitaminCGrams;
-    property public final double vitaminDGrams;
-    property public final double vitaminEGrams;
-    property public final double vitaminKGrams;
-    property public final double zincGrams;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> BIOTIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CAFFEINE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALCIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALORIES_FROM_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALORIES_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CHLORIDE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CHOLESTEROL_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CHROMIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> COPPER_TOTAL;
+    property public final androidx.health.connect.client.units.Mass? sugar;
+    property public final androidx.health.connect.client.units.Mass? thiamin;
+    property public final androidx.health.connect.client.units.Mass? totalCarbohydrate;
+    property public final androidx.health.connect.client.units.Mass? totalFat;
+    property public final androidx.health.connect.client.units.Mass? transFat;
+    property public final androidx.health.connect.client.units.Mass? unsaturatedFat;
+    property public final androidx.health.connect.client.units.Mass? vitaminA;
+    property public final androidx.health.connect.client.units.Mass? vitaminB12;
+    property public final androidx.health.connect.client.units.Mass? vitaminB6;
+    property public final androidx.health.connect.client.units.Mass? vitaminC;
+    property public final androidx.health.connect.client.units.Mass? vitaminD;
+    property public final androidx.health.connect.client.units.Mass? vitaminE;
+    property public final androidx.health.connect.client.units.Mass? vitaminK;
+    property public final androidx.health.connect.client.units.Mass? zinc;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> BIOTIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CAFFEINE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CALCIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CHLORIDE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CHOLESTEROL_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CHROMIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> COPPER_TOTAL;
     field public static final androidx.health.connect.client.records.NutritionRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIETARY_FIBER_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> FOLATE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> FOLIC_ACID_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> IODINE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> IRON_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MAGNESIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MANGANESE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MOLYBDENUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MONOUNSATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> NIACIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> PANTOTHENIC_ACID_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> PHOSPHORUS_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> POLYUNSATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> POTASSIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> PROTEIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> RIBOFLAVIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SELENIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SODIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SUGAR_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> THIAMIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> TOTAL_CARBOHYDRATE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> TOTAL_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> TRANS_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> UNSATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_A_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_B12_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_B6_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_C_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_D_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_E_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_K_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> ZINC_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> DIETARY_FIBER_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ENERGY_FROM_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ENERGY_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> FOLATE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> FOLIC_ACID_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> IODINE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> IRON_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MAGNESIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MANGANESE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MOLYBDENUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MONOUNSATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> NIACIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> PANTOTHENIC_ACID_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> PHOSPHORUS_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> POLYUNSATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> POTASSIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> PROTEIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> RIBOFLAVIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SELENIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SODIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SUGAR_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> THIAMIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> TOTAL_CARBOHYDRATE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> TOTAL_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> TRANS_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> UNSATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_A_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_B12_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_B6_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_C_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_D_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_E_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_K_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> ZINC_TOTAL;
   }
 
   public static final class NutritionRecord.Companion {
@@ -921,37 +921,37 @@
     property public java.time.ZoneOffset? zoneOffset;
   }
 
-  public final class Power {
-    ctor public Power(java.time.Instant time, @FloatRange(from=0.0, to=100000.0) double watts);
-    method public java.time.Instant getTime();
-    method public double getWatts();
-    property public final java.time.Instant time;
-    property public final double watts;
-  }
-
   public final class PowerRecord implements androidx.health.connect.client.records.Record {
-    ctor public PowerRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.Power> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public PowerRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List<androidx.health.connect.client.records.Power> getSamples();
+    method public java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List<androidx.health.connect.client.records.Power> samples;
+    property public java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.PowerRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WATTS_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WATTS_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WATTS_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> POWER_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> POWER_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> POWER_MIN;
   }
 
   public static final class PowerRecord.Companion {
   }
 
+  public static final class PowerRecord.Sample {
+    ctor public PowerRecord.Sample(java.time.Instant time, androidx.health.connect.client.units.Power power);
+    method public androidx.health.connect.client.units.Power getPower();
+    method public java.time.Instant getTime();
+    property public final androidx.health.connect.client.units.Power power;
+    property public final java.time.Instant time;
+  }
+
   public interface Record {
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     property public abstract androidx.health.connect.client.records.metadata.Metadata metadata;
@@ -1176,21 +1176,21 @@
   }
 
   public final class TotalCaloriesBurnedRecord implements androidx.health.connect.client.records.Record {
-    ctor public TotalCaloriesBurnedRecord(double energyKcal, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public TotalCaloriesBurnedRecord(androidx.health.connect.client.units.Energy energy, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
-    method public double getEnergyKcal();
+    method public androidx.health.connect.client.units.Energy getEnergy();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
-    property public final double energyKcal;
+    property public final androidx.health.connect.client.units.Energy energy;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.TotalCaloriesBurnedRecord.Companion Companion;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ENERGY_TOTAL;
   }
 
   public static final class TotalCaloriesBurnedRecord.Companion {
@@ -1233,19 +1233,19 @@
   }
 
   public final class WeightRecord implements androidx.health.connect.client.records.Record {
-    ctor public WeightRecord(double weightKg, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public WeightRecord(androidx.health.connect.client.units.Mass weight, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
-    method public double getWeightKg();
+    method public androidx.health.connect.client.units.Mass getWeight();
     method public java.time.ZoneOffset? getZoneOffset();
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
-    property public final double weightKg;
+    property public final androidx.health.connect.client.units.Mass weight;
     property public java.time.ZoneOffset? zoneOffset;
     field public static final androidx.health.connect.client.records.WeightRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WEIGHT_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WEIGHT_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WEIGHT_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> WEIGHT_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> WEIGHT_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> WEIGHT_MIN;
   }
 
   public static final class WeightRecord.Companion {
@@ -1404,6 +1404,33 @@
 
 package androidx.health.connect.client.units {
 
+  public final class Energy implements java.lang.Comparable<androidx.health.connect.client.units.Energy> {
+    method public static androidx.health.connect.client.units.Energy calories(double value);
+    method public int compareTo(androidx.health.connect.client.units.Energy other);
+    method public double getCalories();
+    method public double getJoules();
+    method public double getKilocalories();
+    method public double getKilojoules();
+    method public static androidx.health.connect.client.units.Energy joules(double value);
+    method public static androidx.health.connect.client.units.Energy kilocalories(double value);
+    method public static androidx.health.connect.client.units.Energy kilojoules(double value);
+    property public final double inCalories;
+    property public final double inJoules;
+    property public final double inKilocalories;
+    property public final double inKilojoules;
+    field public static final androidx.health.connect.client.units.Energy.Companion Companion;
+  }
+
+  public static final class Energy.Companion {
+    method public androidx.health.connect.client.units.Energy calories(double value);
+    method public androidx.health.connect.client.units.Energy joules(double value);
+    method public androidx.health.connect.client.units.Energy kilocalories(double value);
+    method public androidx.health.connect.client.units.Energy kilojoules(double value);
+  }
+
+  public final class EnergyKt {
+  }
+
   public final class Length implements java.lang.Comparable<androidx.health.connect.client.units.Length> {
     method public int compareTo(androidx.health.connect.client.units.Length other);
     method public static androidx.health.connect.client.units.Length feet(double value);
@@ -1435,6 +1462,71 @@
   public final class LengthKt {
   }
 
+  public final class Mass implements java.lang.Comparable<androidx.health.connect.client.units.Mass> {
+    method public int compareTo(androidx.health.connect.client.units.Mass other);
+    method public double getGrams();
+    method public double getKilograms();
+    method public double getMicrograms();
+    method public double getMilligrams();
+    method public double getOunces();
+    method public static androidx.health.connect.client.units.Mass grams(double value);
+    method public static androidx.health.connect.client.units.Mass kilograms(double value);
+    method public static androidx.health.connect.client.units.Mass micrograms(double value);
+    method public static androidx.health.connect.client.units.Mass milligrams(double value);
+    method public static androidx.health.connect.client.units.Mass ounces(double value);
+    property public final double inGrams;
+    property public final double inKilograms;
+    property public final double inMicrograms;
+    property public final double inMilligrams;
+    property public final double inOunces;
+    field public static final androidx.health.connect.client.units.Mass.Companion Companion;
+  }
+
+  public static final class Mass.Companion {
+    method public androidx.health.connect.client.units.Mass grams(double value);
+    method public androidx.health.connect.client.units.Mass kilograms(double value);
+    method public androidx.health.connect.client.units.Mass micrograms(double value);
+    method public androidx.health.connect.client.units.Mass milligrams(double value);
+    method public androidx.health.connect.client.units.Mass ounces(double value);
+  }
+
+  public final class MassKt {
+  }
+
+  public final class Power implements java.lang.Comparable<androidx.health.connect.client.units.Power> {
+    method public int compareTo(androidx.health.connect.client.units.Power other);
+    method public double getKilocaloriesPerDay();
+    method public double getWatts();
+    method public static androidx.health.connect.client.units.Power kilocaloriesPerDay(double value);
+    method public static androidx.health.connect.client.units.Power watts(double value);
+    property public final double inKilocaloriesPerDay;
+    property public final double inWatts;
+    field public static final androidx.health.connect.client.units.Power.Companion Companion;
+  }
+
+  public static final class Power.Companion {
+    method public androidx.health.connect.client.units.Power kilocaloriesPerDay(double value);
+    method public androidx.health.connect.client.units.Power watts(double value);
+  }
+
+  public final class PowerKt {
+  }
+
+  public final class Pressure implements java.lang.Comparable<androidx.health.connect.client.units.Pressure> {
+    method public int compareTo(androidx.health.connect.client.units.Pressure other);
+    method public double getMillimetersOfMercury();
+    method public static androidx.health.connect.client.units.Pressure millimetersOfMercury(double value);
+    property public final double inMillimetersOfMercury;
+    field public static final androidx.health.connect.client.units.Pressure.Companion Companion;
+  }
+
+  public static final class Pressure.Companion {
+    method public androidx.health.connect.client.units.Pressure millimetersOfMercury(double value);
+  }
+
+  public final class PressureKt {
+  }
+
   public final class Temperature implements java.lang.Comparable<androidx.health.connect.client.units.Temperature> {
     method public static androidx.health.connect.client.units.Temperature celsius(double value);
     method public int compareTo(androidx.health.connect.client.units.Temperature other);
@@ -1454,5 +1546,24 @@
   public final class TemperatureKt {
   }
 
+  public final class Volume implements java.lang.Comparable<androidx.health.connect.client.units.Volume> {
+    method public int compareTo(androidx.health.connect.client.units.Volume other);
+    method public double getLiters();
+    method public double getMilliliters();
+    method public static androidx.health.connect.client.units.Volume liters(double value);
+    method public static androidx.health.connect.client.units.Volume milliliters(double value);
+    property public final double inLiters;
+    property public final double inMilliliters;
+    field public static final androidx.health.connect.client.units.Volume.Companion Companion;
+  }
+
+  public static final class Volume.Companion {
+    method public androidx.health.connect.client.units.Volume liters(double value);
+    method public androidx.health.connect.client.units.Volume milliliters(double value);
+  }
+
+  public final class VolumeKt {
+  }
+
 }
 
diff --git a/health/health-connect-client/api/restricted_current.txt b/health/health-connect-client/api/restricted_current.txt
index 6bd0c2c..d597095 100644
--- a/health/health-connect-client/api/restricted_current.txt
+++ b/health/health-connect-client/api/restricted_current.txt
@@ -114,20 +114,20 @@
 package androidx.health.connect.client.records {
 
   public final class ActiveCaloriesBurnedRecord implements androidx.health.connect.client.records.IntervalRecord {
-    ctor public ActiveCaloriesBurnedRecord(double energyKcal, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public ActiveCaloriesBurnedRecord(androidx.health.connect.client.units.Energy energy, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
-    method public double getEnergyKcal();
+    method public androidx.health.connect.client.units.Energy getEnergy();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
-    property public final double energyKcal;
+    property public final androidx.health.connect.client.units.Energy energy;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> ACTIVE_CALORIES_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ACTIVE_CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.ActiveCaloriesBurnedRecord.Companion Companion;
   }
 
@@ -149,16 +149,16 @@
   }
 
   public final class BasalMetabolicRateRecord implements androidx.health.connect.client.records.InstantaneousRecord {
-    ctor public BasalMetabolicRateRecord(double kcalPerDay, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getKcalPerDay();
+    ctor public BasalMetabolicRateRecord(androidx.health.connect.client.units.Power basalMetabolicRate, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Power getBasalMetabolicRate();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final double kcalPerDay;
+    property public final androidx.health.connect.client.units.Power basalMetabolicRate;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> BASAL_CALORIES_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> BASAL_CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.BasalMetabolicRateRecord.Companion Companion;
   }
 
@@ -194,28 +194,28 @@
   }
 
   public final class BloodPressureRecord implements androidx.health.connect.client.records.InstantaneousRecord {
-    ctor public BloodPressureRecord(double systolicMillimetersOfMercury, double diastolicMillimetersOfMercury, optional String? bodyPosition, optional String? measurementLocation, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public BloodPressureRecord(androidx.health.connect.client.units.Pressure systolic, androidx.health.connect.client.units.Pressure diastolic, optional String? bodyPosition, optional String? measurementLocation, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public String? getBodyPosition();
-    method public double getDiastolicMillimetersOfMercury();
+    method public androidx.health.connect.client.units.Pressure getDiastolic();
     method public String? getMeasurementLocation();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public double getSystolicMillimetersOfMercury();
+    method public androidx.health.connect.client.units.Pressure getSystolic();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
     property public final String? bodyPosition;
-    property public final double diastolicMillimetersOfMercury;
+    property public final androidx.health.connect.client.units.Pressure diastolic;
     property public final String? measurementLocation;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public final double systolicMillimetersOfMercury;
+    property public final androidx.health.connect.client.units.Pressure systolic;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
     field public static final androidx.health.connect.client.records.BloodPressureRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIASTOLIC_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIASTOLIC_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIASTOLIC_MIN;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SYSTOLIC_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SYSTOLIC_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SYSTOLIC_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MIN;
   }
 
   public static final class BloodPressureRecord.Companion {
@@ -278,12 +278,12 @@
   }
 
   public final class BoneMassRecord implements androidx.health.connect.client.records.InstantaneousRecord {
-    ctor public BoneMassRecord(double massKg, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getMassKg();
+    ctor public BoneMassRecord(androidx.health.connect.client.units.Mass mass, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass getMass();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final double massKg;
+    property public final androidx.health.connect.client.units.Mass mass;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
@@ -680,21 +680,21 @@
   }
 
   public final class HydrationRecord implements androidx.health.connect.client.records.IntervalRecord {
-    ctor public HydrationRecord(double volumeLiters, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public HydrationRecord(androidx.health.connect.client.units.Volume volume, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
-    method public double getVolumeLiters();
+    method public androidx.health.connect.client.units.Volume getVolume();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    property public final double volumeLiters;
+    property public final androidx.health.connect.client.units.Volume volume;
     field public static final androidx.health.connect.client.records.HydrationRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VOLUME_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Volume> VOLUME_TOTAL;
   }
 
   public static final class HydrationRecord.Companion {
@@ -719,12 +719,12 @@
   }
 
   public final class LeanBodyMassRecord implements androidx.health.connect.client.records.InstantaneousRecord {
-    ctor public LeanBodyMassRecord(double massKg, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getMassKg();
+    ctor public LeanBodyMassRecord(androidx.health.connect.client.units.Mass mass, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass getMass();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final double massKg;
+    property public final androidx.health.connect.client.units.Mass mass;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
@@ -760,148 +760,148 @@
   }
 
   public final class NutritionRecord implements androidx.health.connect.client.records.IntervalRecord {
-    ctor public NutritionRecord(optional double biotinGrams, optional double caffeineGrams, optional double calciumGrams, optional double kcal, optional double kcalFromFat, optional double chlorideGrams, optional double cholesterolGrams, optional double chromiumGrams, optional double copperGrams, optional double dietaryFiberGrams, optional double folateGrams, optional double folicAcidGrams, optional double iodineGrams, optional double ironGrams, optional double magnesiumGrams, optional double manganeseGrams, optional double molybdenumGrams, optional double monounsaturatedFatGrams, optional double niacinGrams, optional double pantothenicAcidGrams, optional double phosphorusGrams, optional double polyunsaturatedFatGrams, optional double potassiumGrams, optional double proteinGrams, optional double riboflavinGrams, optional double saturatedFatGrams, optional double seleniumGrams, optional double sodiumGrams, optional double sugarGrams, optional double thiaminGrams, optional double totalCarbohydrateGrams, optional double totalFatGrams, optional double transFatGrams, optional double unsaturatedFatGrams, optional double vitaminAGrams, optional double vitaminB12Grams, optional double vitaminB6Grams, optional double vitaminCGrams, optional double vitaminDGrams, optional double vitaminEGrams, optional double vitaminKGrams, optional double zincGrams, optional String? name, optional String? mealType, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public double getBiotinGrams();
-    method public double getCaffeineGrams();
-    method public double getCalciumGrams();
-    method public double getChlorideGrams();
-    method public double getCholesterolGrams();
-    method public double getChromiumGrams();
-    method public double getCopperGrams();
-    method public double getDietaryFiberGrams();
+    ctor public NutritionRecord(optional androidx.health.connect.client.units.Mass? biotin, optional androidx.health.connect.client.units.Mass? caffeine, optional androidx.health.connect.client.units.Mass? calcium, optional androidx.health.connect.client.units.Energy? energy, optional androidx.health.connect.client.units.Energy? energyFromFat, optional androidx.health.connect.client.units.Mass? chloride, optional androidx.health.connect.client.units.Mass? cholesterol, optional androidx.health.connect.client.units.Mass? chromium, optional androidx.health.connect.client.units.Mass? copper, optional androidx.health.connect.client.units.Mass? dietaryFiber, optional androidx.health.connect.client.units.Mass? folate, optional androidx.health.connect.client.units.Mass? folicAcid, optional androidx.health.connect.client.units.Mass? iodine, optional androidx.health.connect.client.units.Mass? iron, optional androidx.health.connect.client.units.Mass? magnesium, optional androidx.health.connect.client.units.Mass? manganese, optional androidx.health.connect.client.units.Mass? molybdenum, optional androidx.health.connect.client.units.Mass? monounsaturatedFat, optional androidx.health.connect.client.units.Mass? niacin, optional androidx.health.connect.client.units.Mass? pantothenicAcid, optional androidx.health.connect.client.units.Mass? phosphorus, optional androidx.health.connect.client.units.Mass? polyunsaturatedFat, optional androidx.health.connect.client.units.Mass? potassium, optional androidx.health.connect.client.units.Mass? protein, optional androidx.health.connect.client.units.Mass? riboflavin, optional androidx.health.connect.client.units.Mass? saturatedFat, optional androidx.health.connect.client.units.Mass? selenium, optional androidx.health.connect.client.units.Mass? sodium, optional androidx.health.connect.client.units.Mass? sugar, optional androidx.health.connect.client.units.Mass? thiamin, optional androidx.health.connect.client.units.Mass? totalCarbohydrate, optional androidx.health.connect.client.units.Mass? totalFat, optional androidx.health.connect.client.units.Mass? transFat, optional androidx.health.connect.client.units.Mass? unsaturatedFat, optional androidx.health.connect.client.units.Mass? vitaminA, optional androidx.health.connect.client.units.Mass? vitaminB12, optional androidx.health.connect.client.units.Mass? vitaminB6, optional androidx.health.connect.client.units.Mass? vitaminC, optional androidx.health.connect.client.units.Mass? vitaminD, optional androidx.health.connect.client.units.Mass? vitaminE, optional androidx.health.connect.client.units.Mass? vitaminK, optional androidx.health.connect.client.units.Mass? zinc, optional String? name, optional String? mealType, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass? getBiotin();
+    method public androidx.health.connect.client.units.Mass? getCaffeine();
+    method public androidx.health.connect.client.units.Mass? getCalcium();
+    method public androidx.health.connect.client.units.Mass? getChloride();
+    method public androidx.health.connect.client.units.Mass? getCholesterol();
+    method public androidx.health.connect.client.units.Mass? getChromium();
+    method public androidx.health.connect.client.units.Mass? getCopper();
+    method public androidx.health.connect.client.units.Mass? getDietaryFiber();
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
-    method public double getFolateGrams();
-    method public double getFolicAcidGrams();
-    method public double getIodineGrams();
-    method public double getIronGrams();
-    method public double getKcal();
-    method public double getKcalFromFat();
-    method public double getMagnesiumGrams();
-    method public double getManganeseGrams();
+    method public androidx.health.connect.client.units.Energy? getEnergy();
+    method public androidx.health.connect.client.units.Energy? getEnergyFromFat();
+    method public androidx.health.connect.client.units.Mass? getFolate();
+    method public androidx.health.connect.client.units.Mass? getFolicAcid();
+    method public androidx.health.connect.client.units.Mass? getIodine();
+    method public androidx.health.connect.client.units.Mass? getIron();
+    method public androidx.health.connect.client.units.Mass? getMagnesium();
+    method public androidx.health.connect.client.units.Mass? getManganese();
     method public String? getMealType();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public double getMolybdenumGrams();
-    method public double getMonounsaturatedFatGrams();
+    method public androidx.health.connect.client.units.Mass? getMolybdenum();
+    method public androidx.health.connect.client.units.Mass? getMonounsaturatedFat();
     method public String? getName();
-    method public double getNiacinGrams();
-    method public double getPantothenicAcidGrams();
-    method public double getPhosphorusGrams();
-    method public double getPolyunsaturatedFatGrams();
-    method public double getPotassiumGrams();
-    method public double getProteinGrams();
-    method public double getRiboflavinGrams();
-    method public double getSaturatedFatGrams();
-    method public double getSeleniumGrams();
-    method public double getSodiumGrams();
+    method public androidx.health.connect.client.units.Mass? getNiacin();
+    method public androidx.health.connect.client.units.Mass? getPantothenicAcid();
+    method public androidx.health.connect.client.units.Mass? getPhosphorus();
+    method public androidx.health.connect.client.units.Mass? getPolyunsaturatedFat();
+    method public androidx.health.connect.client.units.Mass? getPotassium();
+    method public androidx.health.connect.client.units.Mass? getProtein();
+    method public androidx.health.connect.client.units.Mass? getRiboflavin();
+    method public androidx.health.connect.client.units.Mass? getSaturatedFat();
+    method public androidx.health.connect.client.units.Mass? getSelenium();
+    method public androidx.health.connect.client.units.Mass? getSodium();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
-    method public double getSugarGrams();
-    method public double getThiaminGrams();
-    method public double getTotalCarbohydrateGrams();
-    method public double getTotalFatGrams();
-    method public double getTransFatGrams();
-    method public double getUnsaturatedFatGrams();
-    method public double getVitaminAGrams();
-    method public double getVitaminB12Grams();
-    method public double getVitaminB6Grams();
-    method public double getVitaminCGrams();
-    method public double getVitaminDGrams();
-    method public double getVitaminEGrams();
-    method public double getVitaminKGrams();
-    method public double getZincGrams();
-    property public final double biotinGrams;
-    property public final double caffeineGrams;
-    property public final double calciumGrams;
-    property public final double chlorideGrams;
-    property public final double cholesterolGrams;
-    property public final double chromiumGrams;
-    property public final double copperGrams;
-    property public final double dietaryFiberGrams;
+    method public androidx.health.connect.client.units.Mass? getSugar();
+    method public androidx.health.connect.client.units.Mass? getThiamin();
+    method public androidx.health.connect.client.units.Mass? getTotalCarbohydrate();
+    method public androidx.health.connect.client.units.Mass? getTotalFat();
+    method public androidx.health.connect.client.units.Mass? getTransFat();
+    method public androidx.health.connect.client.units.Mass? getUnsaturatedFat();
+    method public androidx.health.connect.client.units.Mass? getVitaminA();
+    method public androidx.health.connect.client.units.Mass? getVitaminB12();
+    method public androidx.health.connect.client.units.Mass? getVitaminB6();
+    method public androidx.health.connect.client.units.Mass? getVitaminC();
+    method public androidx.health.connect.client.units.Mass? getVitaminD();
+    method public androidx.health.connect.client.units.Mass? getVitaminE();
+    method public androidx.health.connect.client.units.Mass? getVitaminK();
+    method public androidx.health.connect.client.units.Mass? getZinc();
+    property public final androidx.health.connect.client.units.Mass? biotin;
+    property public final androidx.health.connect.client.units.Mass? caffeine;
+    property public final androidx.health.connect.client.units.Mass? calcium;
+    property public final androidx.health.connect.client.units.Mass? chloride;
+    property public final androidx.health.connect.client.units.Mass? cholesterol;
+    property public final androidx.health.connect.client.units.Mass? chromium;
+    property public final androidx.health.connect.client.units.Mass? copper;
+    property public final androidx.health.connect.client.units.Mass? dietaryFiber;
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
-    property public final double folateGrams;
-    property public final double folicAcidGrams;
-    property public final double iodineGrams;
-    property public final double ironGrams;
-    property public final double kcal;
-    property public final double kcalFromFat;
-    property public final double magnesiumGrams;
-    property public final double manganeseGrams;
+    property public final androidx.health.connect.client.units.Energy? energy;
+    property public final androidx.health.connect.client.units.Energy? energyFromFat;
+    property public final androidx.health.connect.client.units.Mass? folate;
+    property public final androidx.health.connect.client.units.Mass? folicAcid;
+    property public final androidx.health.connect.client.units.Mass? iodine;
+    property public final androidx.health.connect.client.units.Mass? iron;
+    property public final androidx.health.connect.client.units.Mass? magnesium;
+    property public final androidx.health.connect.client.units.Mass? manganese;
     property public final String? mealType;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public final double molybdenumGrams;
-    property public final double monounsaturatedFatGrams;
+    property public final androidx.health.connect.client.units.Mass? molybdenum;
+    property public final androidx.health.connect.client.units.Mass? monounsaturatedFat;
     property public final String? name;
-    property public final double niacinGrams;
-    property public final double pantothenicAcidGrams;
-    property public final double phosphorusGrams;
-    property public final double polyunsaturatedFatGrams;
-    property public final double potassiumGrams;
-    property public final double proteinGrams;
-    property public final double riboflavinGrams;
-    property public final double saturatedFatGrams;
-    property public final double seleniumGrams;
-    property public final double sodiumGrams;
+    property public final androidx.health.connect.client.units.Mass? niacin;
+    property public final androidx.health.connect.client.units.Mass? pantothenicAcid;
+    property public final androidx.health.connect.client.units.Mass? phosphorus;
+    property public final androidx.health.connect.client.units.Mass? polyunsaturatedFat;
+    property public final androidx.health.connect.client.units.Mass? potassium;
+    property public final androidx.health.connect.client.units.Mass? protein;
+    property public final androidx.health.connect.client.units.Mass? riboflavin;
+    property public final androidx.health.connect.client.units.Mass? saturatedFat;
+    property public final androidx.health.connect.client.units.Mass? selenium;
+    property public final androidx.health.connect.client.units.Mass? sodium;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    property public final double sugarGrams;
-    property public final double thiaminGrams;
-    property public final double totalCarbohydrateGrams;
-    property public final double totalFatGrams;
-    property public final double transFatGrams;
-    property public final double unsaturatedFatGrams;
-    property public final double vitaminAGrams;
-    property public final double vitaminB12Grams;
-    property public final double vitaminB6Grams;
-    property public final double vitaminCGrams;
-    property public final double vitaminDGrams;
-    property public final double vitaminEGrams;
-    property public final double vitaminKGrams;
-    property public final double zincGrams;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> BIOTIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CAFFEINE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALCIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALORIES_FROM_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALORIES_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CHLORIDE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CHOLESTEROL_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CHROMIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> COPPER_TOTAL;
+    property public final androidx.health.connect.client.units.Mass? sugar;
+    property public final androidx.health.connect.client.units.Mass? thiamin;
+    property public final androidx.health.connect.client.units.Mass? totalCarbohydrate;
+    property public final androidx.health.connect.client.units.Mass? totalFat;
+    property public final androidx.health.connect.client.units.Mass? transFat;
+    property public final androidx.health.connect.client.units.Mass? unsaturatedFat;
+    property public final androidx.health.connect.client.units.Mass? vitaminA;
+    property public final androidx.health.connect.client.units.Mass? vitaminB12;
+    property public final androidx.health.connect.client.units.Mass? vitaminB6;
+    property public final androidx.health.connect.client.units.Mass? vitaminC;
+    property public final androidx.health.connect.client.units.Mass? vitaminD;
+    property public final androidx.health.connect.client.units.Mass? vitaminE;
+    property public final androidx.health.connect.client.units.Mass? vitaminK;
+    property public final androidx.health.connect.client.units.Mass? zinc;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> BIOTIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CAFFEINE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CALCIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CHLORIDE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CHOLESTEROL_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> CHROMIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> COPPER_TOTAL;
     field public static final androidx.health.connect.client.records.NutritionRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> DIETARY_FIBER_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> FOLATE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> FOLIC_ACID_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> IODINE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> IRON_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MAGNESIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MANGANESE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MOLYBDENUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> MONOUNSATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> NIACIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> PANTOTHENIC_ACID_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> PHOSPHORUS_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> POLYUNSATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> POTASSIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> PROTEIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> RIBOFLAVIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SELENIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SODIUM_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> SUGAR_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> THIAMIN_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> TOTAL_CARBOHYDRATE_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> TOTAL_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> TRANS_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> UNSATURATED_FAT_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_A_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_B12_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_B6_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_C_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_D_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_E_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> VITAMIN_K_TOTAL;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> ZINC_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> DIETARY_FIBER_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ENERGY_FROM_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ENERGY_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> FOLATE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> FOLIC_ACID_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> IODINE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> IRON_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MAGNESIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MANGANESE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MOLYBDENUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> MONOUNSATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> NIACIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> PANTOTHENIC_ACID_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> PHOSPHORUS_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> POLYUNSATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> POTASSIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> PROTEIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> RIBOFLAVIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SELENIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SODIUM_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> SUGAR_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> THIAMIN_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> TOTAL_CARBOHYDRATE_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> TOTAL_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> TRANS_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> UNSATURATED_FAT_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_A_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_B12_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_B6_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_C_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_D_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_E_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> VITAMIN_K_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> ZINC_TOTAL;
   }
 
   public static final class NutritionRecord.Companion {
@@ -939,37 +939,37 @@
     property public java.time.ZoneOffset? zoneOffset;
   }
 
-  public final class Power {
-    ctor public Power(java.time.Instant time, @FloatRange(from=0.0, to=100000.0) double watts);
-    method public java.time.Instant getTime();
-    method public double getWatts();
-    property public final java.time.Instant time;
-    property public final double watts;
-  }
-
-  public final class PowerRecord implements androidx.health.connect.client.records.SeriesRecord<androidx.health.connect.client.records.Power> {
-    ctor public PowerRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.Power> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+  public final class PowerRecord implements androidx.health.connect.client.records.SeriesRecord<androidx.health.connect.client.records.PowerRecord.Sample> {
+    ctor public PowerRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List<androidx.health.connect.client.records.Power> getSamples();
+    method public java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List<androidx.health.connect.client.records.Power> samples;
+    property public java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.PowerRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WATTS_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WATTS_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WATTS_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> POWER_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> POWER_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Power> POWER_MIN;
   }
 
   public static final class PowerRecord.Companion {
   }
 
+  public static final class PowerRecord.Sample {
+    ctor public PowerRecord.Sample(java.time.Instant time, androidx.health.connect.client.units.Power power);
+    method public androidx.health.connect.client.units.Power getPower();
+    method public java.time.Instant getTime();
+    property public final androidx.health.connect.client.units.Power power;
+    property public final java.time.Instant time;
+  }
+
   public interface Record {
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     property public abstract androidx.health.connect.client.records.metadata.Metadata metadata;
@@ -1199,21 +1199,21 @@
   }
 
   public final class TotalCaloriesBurnedRecord implements androidx.health.connect.client.records.IntervalRecord {
-    ctor public TotalCaloriesBurnedRecord(double energyKcal, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public TotalCaloriesBurnedRecord(androidx.health.connect.client.units.Energy energy, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
-    method public double getEnergyKcal();
+    method public androidx.health.connect.client.units.Energy getEnergy();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
-    property public final double energyKcal;
+    property public final androidx.health.connect.client.units.Energy energy;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.TotalCaloriesBurnedRecord.Companion Companion;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Energy> ENERGY_TOTAL;
   }
 
   public static final class TotalCaloriesBurnedRecord.Companion {
@@ -1256,19 +1256,19 @@
   }
 
   public final class WeightRecord implements androidx.health.connect.client.records.InstantaneousRecord {
-    ctor public WeightRecord(double weightKg, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public WeightRecord(androidx.health.connect.client.units.Mass weight, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
-    method public double getWeightKg();
+    method public androidx.health.connect.client.units.Mass getWeight();
     method public java.time.ZoneOffset? getZoneOffset();
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
-    property public final double weightKg;
+    property public final androidx.health.connect.client.units.Mass weight;
     property public java.time.ZoneOffset? zoneOffset;
     field public static final androidx.health.connect.client.records.WeightRecord.Companion Companion;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WEIGHT_AVG;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WEIGHT_MAX;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.lang.Double> WEIGHT_MIN;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> WEIGHT_AVG;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> WEIGHT_MAX;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Mass> WEIGHT_MIN;
   }
 
   public static final class WeightRecord.Companion {
@@ -1427,6 +1427,33 @@
 
 package androidx.health.connect.client.units {
 
+  public final class Energy implements java.lang.Comparable<androidx.health.connect.client.units.Energy> {
+    method public static androidx.health.connect.client.units.Energy calories(double value);
+    method public int compareTo(androidx.health.connect.client.units.Energy other);
+    method public double getCalories();
+    method public double getJoules();
+    method public double getKilocalories();
+    method public double getKilojoules();
+    method public static androidx.health.connect.client.units.Energy joules(double value);
+    method public static androidx.health.connect.client.units.Energy kilocalories(double value);
+    method public static androidx.health.connect.client.units.Energy kilojoules(double value);
+    property public final double inCalories;
+    property public final double inJoules;
+    property public final double inKilocalories;
+    property public final double inKilojoules;
+    field public static final androidx.health.connect.client.units.Energy.Companion Companion;
+  }
+
+  public static final class Energy.Companion {
+    method public androidx.health.connect.client.units.Energy calories(double value);
+    method public androidx.health.connect.client.units.Energy joules(double value);
+    method public androidx.health.connect.client.units.Energy kilocalories(double value);
+    method public androidx.health.connect.client.units.Energy kilojoules(double value);
+  }
+
+  public final class EnergyKt {
+  }
+
   public final class Length implements java.lang.Comparable<androidx.health.connect.client.units.Length> {
     method public int compareTo(androidx.health.connect.client.units.Length other);
     method public static androidx.health.connect.client.units.Length feet(double value);
@@ -1458,6 +1485,71 @@
   public final class LengthKt {
   }
 
+  public final class Mass implements java.lang.Comparable<androidx.health.connect.client.units.Mass> {
+    method public int compareTo(androidx.health.connect.client.units.Mass other);
+    method public double getGrams();
+    method public double getKilograms();
+    method public double getMicrograms();
+    method public double getMilligrams();
+    method public double getOunces();
+    method public static androidx.health.connect.client.units.Mass grams(double value);
+    method public static androidx.health.connect.client.units.Mass kilograms(double value);
+    method public static androidx.health.connect.client.units.Mass micrograms(double value);
+    method public static androidx.health.connect.client.units.Mass milligrams(double value);
+    method public static androidx.health.connect.client.units.Mass ounces(double value);
+    property public final double inGrams;
+    property public final double inKilograms;
+    property public final double inMicrograms;
+    property public final double inMilligrams;
+    property public final double inOunces;
+    field public static final androidx.health.connect.client.units.Mass.Companion Companion;
+  }
+
+  public static final class Mass.Companion {
+    method public androidx.health.connect.client.units.Mass grams(double value);
+    method public androidx.health.connect.client.units.Mass kilograms(double value);
+    method public androidx.health.connect.client.units.Mass micrograms(double value);
+    method public androidx.health.connect.client.units.Mass milligrams(double value);
+    method public androidx.health.connect.client.units.Mass ounces(double value);
+  }
+
+  public final class MassKt {
+  }
+
+  public final class Power implements java.lang.Comparable<androidx.health.connect.client.units.Power> {
+    method public int compareTo(androidx.health.connect.client.units.Power other);
+    method public double getKilocaloriesPerDay();
+    method public double getWatts();
+    method public static androidx.health.connect.client.units.Power kilocaloriesPerDay(double value);
+    method public static androidx.health.connect.client.units.Power watts(double value);
+    property public final double inKilocaloriesPerDay;
+    property public final double inWatts;
+    field public static final androidx.health.connect.client.units.Power.Companion Companion;
+  }
+
+  public static final class Power.Companion {
+    method public androidx.health.connect.client.units.Power kilocaloriesPerDay(double value);
+    method public androidx.health.connect.client.units.Power watts(double value);
+  }
+
+  public final class PowerKt {
+  }
+
+  public final class Pressure implements java.lang.Comparable<androidx.health.connect.client.units.Pressure> {
+    method public int compareTo(androidx.health.connect.client.units.Pressure other);
+    method public double getMillimetersOfMercury();
+    method public static androidx.health.connect.client.units.Pressure millimetersOfMercury(double value);
+    property public final double inMillimetersOfMercury;
+    field public static final androidx.health.connect.client.units.Pressure.Companion Companion;
+  }
+
+  public static final class Pressure.Companion {
+    method public androidx.health.connect.client.units.Pressure millimetersOfMercury(double value);
+  }
+
+  public final class PressureKt {
+  }
+
   public final class Temperature implements java.lang.Comparable<androidx.health.connect.client.units.Temperature> {
     method public static androidx.health.connect.client.units.Temperature celsius(double value);
     method public int compareTo(androidx.health.connect.client.units.Temperature other);
@@ -1477,5 +1569,24 @@
   public final class TemperatureKt {
   }
 
+  public final class Volume implements java.lang.Comparable<androidx.health.connect.client.units.Volume> {
+    method public int compareTo(androidx.health.connect.client.units.Volume other);
+    method public double getLiters();
+    method public double getMilliliters();
+    method public static androidx.health.connect.client.units.Volume liters(double value);
+    method public static androidx.health.connect.client.units.Volume milliliters(double value);
+    property public final double inLiters;
+    property public final double inMilliliters;
+    field public static final androidx.health.connect.client.units.Volume.Companion Companion;
+  }
+
+  public static final class Volume.Companion {
+    method public androidx.health.connect.client.units.Volume liters(double value);
+    method public androidx.health.connect.client.units.Volume milliliters(double value);
+  }
+
+  public final class VolumeKt {
+  }
+
 }
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
index 317f450..bf6ae6c 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -56,7 +56,6 @@
 import androidx.health.connect.client.records.NutritionRecord
 import androidx.health.connect.client.records.OvulationTestRecord
 import androidx.health.connect.client.records.OxygenSaturationRecord
-import androidx.health.connect.client.records.Power
 import androidx.health.connect.client.records.PowerRecord
 import androidx.health.connect.client.records.Record
 import androidx.health.connect.client.records.RespiratoryRateRecord
@@ -76,7 +75,14 @@
 import androidx.health.connect.client.records.WeightRecord
 import androidx.health.connect.client.records.WheelchairPushesRecord
 import androidx.health.connect.client.units.celsius
+import androidx.health.connect.client.units.grams
+import androidx.health.connect.client.units.kilocalories
+import androidx.health.connect.client.units.kilocaloriesPerDay
+import androidx.health.connect.client.units.kilograms
+import androidx.health.connect.client.units.liters
 import androidx.health.connect.client.units.meters
+import androidx.health.connect.client.units.millimetersOfMercury
+import androidx.health.connect.client.units.watts
 import androidx.health.platform.client.proto.DataProto
 import java.time.Instant
 
@@ -94,7 +100,7 @@
                 )
             "BasalMetabolicRate" ->
                 BasalMetabolicRateRecord(
-                    kcalPerDay = getDouble("bmr"),
+                    basalMetabolicRate = getDouble("bmr").kilocaloriesPerDay,
                     time = time,
                     zoneOffset = zoneOffset,
                     metadata = metadata
@@ -111,8 +117,8 @@
                 )
             "BloodPressure" ->
                 BloodPressureRecord(
-                    systolicMillimetersOfMercury = getDouble("systolic"),
-                    diastolicMillimetersOfMercury = getDouble("diastolic"),
+                    systolic = getDouble("systolic").millimetersOfMercury,
+                    diastolic = getDouble("diastolic").millimetersOfMercury,
                     bodyPosition = getEnum("bodyPosition"),
                     measurementLocation = getEnum("measurementLocation"),
                     time = time,
@@ -136,14 +142,14 @@
                 )
             "BodyWaterMass" ->
                 BodyWaterMassRecord(
-                    massKg = getDouble("mass"),
+                    mass = getDouble("mass").kilograms,
                     time = time,
                     zoneOffset = zoneOffset,
                     metadata = metadata
                 )
             "BoneMass" ->
                 BoneMassRecord(
-                    massKg = getDouble("mass"),
+                    mass = getDouble("mass").kilograms,
                     time = time,
                     zoneOffset = zoneOffset,
                     metadata = metadata
@@ -265,7 +271,7 @@
                 )
             "LeanBodyMass" ->
                 LeanBodyMassRecord(
-                    massKg = getDouble("mass"),
+                    mass = getDouble("mass").kilograms,
                     time = time,
                     zoneOffset = zoneOffset,
                     metadata = metadata
@@ -299,9 +305,9 @@
                     endZoneOffset = endZoneOffset,
                     samples =
                         seriesValuesList.map { value ->
-                            Power(
+                            PowerRecord.Sample(
                                 time = Instant.ofEpochMilli(value.instantTimeMillis),
-                                watts = value.getDouble("power"),
+                                power = value.getDouble("power").watts,
                             )
                         },
                     metadata = metadata,
@@ -374,14 +380,14 @@
                 )
             "Weight" ->
                 WeightRecord(
-                    weightKg = getDouble("weight"),
+                    weight = getDouble("weight").kilograms,
                     time = time,
                     zoneOffset = zoneOffset,
                     metadata = metadata
                 )
             "ActiveCaloriesBurned" ->
                 ActiveCaloriesBurnedRecord(
-                    energyKcal = getDouble("energy"),
+                    energy = getDouble("energy").kilocalories,
                     startTime = startTime,
                     startZoneOffset = startZoneOffset,
                     endTime = endTime,
@@ -446,7 +452,7 @@
                 )
             "Hydration" ->
                 HydrationRecord(
-                    volumeLiters = getDouble("volume"),
+                    volume = getDouble("volume").liters,
                     startTime = startTime,
                     startZoneOffset = startZoneOffset,
                     endTime = endTime,
@@ -455,48 +461,48 @@
                 )
             "Nutrition" ->
                 NutritionRecord(
-                    biotinGrams = getDouble("biotin"),
-                    caffeineGrams = getDouble("caffeine"),
-                    calciumGrams = getDouble("calcium"),
-                    kcal = getDouble("calories"),
-                    kcalFromFat = getDouble("caloriesFromFat"),
-                    chlorideGrams = getDouble("chloride"),
-                    cholesterolGrams = getDouble("cholesterol"),
-                    chromiumGrams = getDouble("chromium"),
-                    copperGrams = getDouble("copper"),
-                    dietaryFiberGrams = getDouble("dietaryFiber"),
-                    folateGrams = getDouble("folate"),
-                    folicAcidGrams = getDouble("folicAcid"),
-                    iodineGrams = getDouble("iodine"),
-                    ironGrams = getDouble("iron"),
-                    magnesiumGrams = getDouble("magnesium"),
-                    manganeseGrams = getDouble("manganese"),
-                    molybdenumGrams = getDouble("molybdenum"),
-                    monounsaturatedFatGrams = getDouble("monounsaturatedFat"),
-                    niacinGrams = getDouble("niacin"),
-                    pantothenicAcidGrams = getDouble("pantothenicAcid"),
-                    phosphorusGrams = getDouble("phosphorus"),
-                    polyunsaturatedFatGrams = getDouble("polyunsaturatedFat"),
-                    potassiumGrams = getDouble("potassium"),
-                    proteinGrams = getDouble("protein"),
-                    riboflavinGrams = getDouble("riboflavin"),
-                    saturatedFatGrams = getDouble("saturatedFat"),
-                    seleniumGrams = getDouble("selenium"),
-                    sodiumGrams = getDouble("sodium"),
-                    sugarGrams = getDouble("sugar"),
-                    thiaminGrams = getDouble("thiamin"),
-                    totalCarbohydrateGrams = getDouble("totalCarbohydrate"),
-                    totalFatGrams = getDouble("totalFat"),
-                    transFatGrams = getDouble("transFat"),
-                    unsaturatedFatGrams = getDouble("unsaturatedFat"),
-                    vitaminAGrams = getDouble("vitaminA"),
-                    vitaminB12Grams = getDouble("vitaminB12"),
-                    vitaminB6Grams = getDouble("vitaminB6"),
-                    vitaminCGrams = getDouble("vitaminC"),
-                    vitaminDGrams = getDouble("vitaminD"),
-                    vitaminEGrams = getDouble("vitaminE"),
-                    vitaminKGrams = getDouble("vitaminK"),
-                    zincGrams = getDouble("zinc"),
+                    biotin = valuesMap["biotin"]?.doubleVal?.grams,
+                    caffeine = valuesMap["caffeine"]?.doubleVal?.grams,
+                    calcium = valuesMap["calcium"]?.doubleVal?.grams,
+                    energy = valuesMap["calories"]?.doubleVal?.kilocalories,
+                    energyFromFat = valuesMap["caloriesFromFat"]?.doubleVal?.kilocalories,
+                    chloride = valuesMap["chloride"]?.doubleVal?.grams,
+                    cholesterol = valuesMap["cholesterol"]?.doubleVal?.grams,
+                    chromium = valuesMap["chromium"]?.doubleVal?.grams,
+                    copper = valuesMap["copper"]?.doubleVal?.grams,
+                    dietaryFiber = valuesMap["dietaryFiber"]?.doubleVal?.grams,
+                    folate = valuesMap["folate"]?.doubleVal?.grams,
+                    folicAcid = valuesMap["folicAcid"]?.doubleVal?.grams,
+                    iodine = valuesMap["iodine"]?.doubleVal?.grams,
+                    iron = valuesMap["iron"]?.doubleVal?.grams,
+                    magnesium = valuesMap["magnesium"]?.doubleVal?.grams,
+                    manganese = valuesMap["manganese"]?.doubleVal?.grams,
+                    molybdenum = valuesMap["molybdenum"]?.doubleVal?.grams,
+                    monounsaturatedFat = valuesMap["monounsaturatedFat"]?.doubleVal?.grams,
+                    niacin = valuesMap["niacin"]?.doubleVal?.grams,
+                    pantothenicAcid = valuesMap["pantothenicAcid"]?.doubleVal?.grams,
+                    phosphorus = valuesMap["phosphorus"]?.doubleVal?.grams,
+                    polyunsaturatedFat = valuesMap["polyunsaturatedFat"]?.doubleVal?.grams,
+                    potassium = valuesMap["potassium"]?.doubleVal?.grams,
+                    protein = valuesMap["protein"]?.doubleVal?.grams,
+                    riboflavin = valuesMap["riboflavin"]?.doubleVal?.grams,
+                    saturatedFat = valuesMap["saturatedFat"]?.doubleVal?.grams,
+                    selenium = valuesMap["selenium"]?.doubleVal?.grams,
+                    sodium = valuesMap["sodium"]?.doubleVal?.grams,
+                    sugar = valuesMap["sugar"]?.doubleVal?.grams,
+                    thiamin = valuesMap["thiamin"]?.doubleVal?.grams,
+                    totalCarbohydrate = valuesMap["totalCarbohydrate"]?.doubleVal?.grams,
+                    totalFat = valuesMap["totalFat"]?.doubleVal?.grams,
+                    transFat = valuesMap["transFat"]?.doubleVal?.grams,
+                    unsaturatedFat = valuesMap["unsaturatedFat"]?.doubleVal?.grams,
+                    vitaminA = valuesMap["vitaminA"]?.doubleVal?.grams,
+                    vitaminB12 = valuesMap["vitaminB12"]?.doubleVal?.grams,
+                    vitaminB6 = valuesMap["vitaminB6"]?.doubleVal?.grams,
+                    vitaminC = valuesMap["vitaminC"]?.doubleVal?.grams,
+                    vitaminD = valuesMap["vitaminD"]?.doubleVal?.grams,
+                    vitaminE = valuesMap["vitaminE"]?.doubleVal?.grams,
+                    vitaminK = valuesMap["vitaminK"]?.doubleVal?.grams,
+                    zinc = valuesMap["zinc"]?.doubleVal?.grams,
                     mealType = getEnum("mealType"),
                     name = getString("name"),
                     startTime = startTime,
@@ -555,7 +561,7 @@
                 )
             "TotalCaloriesBurned" ->
                 TotalCaloriesBurnedRecord(
-                    energyKcal = getDouble("energy"),
+                    energy = getDouble("energy").kilocalories,
                     startTime = startTime,
                     startZoneOffset = startZoneOffset,
                     endTime = endTime,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
index 86e7d00..fbe8142 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
@@ -87,7 +87,7 @@
         is BasalMetabolicRateRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BasalMetabolicRate"))
-                .apply { putValues("bmr", doubleVal(kcalPerDay)) }
+                .apply { putValues("bmr", doubleVal(basalMetabolicRate.inKilocaloriesPerDay)) }
                 .build()
         is BloodGlucoseRecord ->
             instantaneousProto()
@@ -103,8 +103,8 @@
             instantaneousProto()
                 .setDataType(protoDataType("BloodPressure"))
                 .apply {
-                    putValues("systolic", doubleVal(systolicMillimetersOfMercury))
-                    putValues("diastolic", doubleVal(diastolicMillimetersOfMercury))
+                    putValues("systolic", doubleVal(systolic.inMillimetersOfMercury))
+                    putValues("diastolic", doubleVal(diastolic.inMillimetersOfMercury))
                     bodyPosition?.let { putValues("bodyPosition", enumVal(it)) }
                     measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
                 }
@@ -125,12 +125,12 @@
         is BodyWaterMassRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BodyWaterMass"))
-                .apply { putValues("mass", doubleVal(massKg)) }
+                .apply { putValues("mass", doubleVal(mass.inKilograms)) }
                 .build()
         is BoneMassRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("BoneMass"))
-                .apply { putValues("mass", doubleVal(massKg)) }
+                .apply { putValues("mass", doubleVal(mass.inKilograms)) }
                 .build()
         is CervicalMucusRecord ->
             instantaneousProto()
@@ -212,7 +212,7 @@
         is LeanBodyMassRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("LeanBodyMass"))
-                .apply { putValues("mass", doubleVal(massKg)) }
+                .apply { putValues("mass", doubleVal(mass.inKilograms)) }
                 .build()
         is MenstruationRecord ->
             instantaneousProto()
@@ -232,7 +232,7 @@
         is PowerRecord ->
             toProto(dataTypeName = "PowerSeries") { sample ->
                 DataProto.SeriesValue.newBuilder()
-                    .putValues("power", doubleVal(sample.watts))
+                    .putValues("power", doubleVal(sample.power.inWatts))
                     .setInstantTimeMillis(sample.time.toEpochMilli())
                     .build()
             }
@@ -281,12 +281,12 @@
         is WeightRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("Weight"))
-                .apply { putValues("weight", doubleVal(weightKg)) }
+                .apply { putValues("weight", doubleVal(weight.inKilograms)) }
                 .build()
         is ActiveCaloriesBurnedRecord ->
             intervalProto()
                 .setDataType(protoDataType("ActiveCaloriesBurned"))
-                .apply { putValues("energy", doubleVal(energyKcal)) }
+                .apply { putValues("energy", doubleVal(energy.inKilocalories)) }
                 .build()
         is ExerciseEventRecord ->
             intervalProto()
@@ -329,137 +329,137 @@
         is HydrationRecord ->
             intervalProto()
                 .setDataType(protoDataType("Hydration"))
-                .apply { putValues("volume", doubleVal(volumeLiters)) }
+                .apply { putValues("volume", doubleVal(volume.inLiters)) }
                 .build()
         is NutritionRecord ->
             intervalProto()
                 .setDataType(protoDataType("Nutrition"))
                 .apply {
-                    if (biotinGrams > 0) {
-                        putValues("biotin", doubleVal(biotinGrams))
+                    if (biotin != null) {
+                        putValues("biotin", doubleVal(biotin.inGrams))
                     }
-                    if (caffeineGrams > 0) {
-                        putValues("caffeine", doubleVal(caffeineGrams))
+                    if (caffeine != null) {
+                        putValues("caffeine", doubleVal(caffeine.inGrams))
                     }
-                    if (calciumGrams > 0) {
-                        putValues("calcium", doubleVal(calciumGrams))
+                    if (calcium != null) {
+                        putValues("calcium", doubleVal(calcium.inGrams))
                     }
-                    if (kcal > 0) {
-                        putValues("calories", doubleVal(kcal))
+                    if (energy != null) {
+                        putValues("calories", doubleVal(energy.inKilocalories))
                     }
-                    if (kcalFromFat > 0) {
-                        putValues("caloriesFromFat", doubleVal(kcalFromFat))
+                    if (energyFromFat != null) {
+                        putValues("caloriesFromFat", doubleVal(energyFromFat.inKilocalories))
                     }
-                    if (chlorideGrams > 0) {
-                        putValues("chloride", doubleVal(chlorideGrams))
+                    if (chloride != null) {
+                        putValues("chloride", doubleVal(chloride.inGrams))
                     }
-                    if (cholesterolGrams > 0) {
-                        putValues("cholesterol", doubleVal(cholesterolGrams))
+                    if (cholesterol != null) {
+                        putValues("cholesterol", doubleVal(cholesterol.inGrams))
                     }
-                    if (chromiumGrams > 0) {
-                        putValues("chromium", doubleVal(chromiumGrams))
+                    if (chromium != null) {
+                        putValues("chromium", doubleVal(chromium.inGrams))
                     }
-                    if (copperGrams > 0) {
-                        putValues("copper", doubleVal(copperGrams))
+                    if (copper != null) {
+                        putValues("copper", doubleVal(copper.inGrams))
                     }
-                    if (dietaryFiberGrams > 0) {
-                        putValues("dietaryFiber", doubleVal(dietaryFiberGrams))
+                    if (dietaryFiber != null) {
+                        putValues("dietaryFiber", doubleVal(dietaryFiber.inGrams))
                     }
-                    if (folateGrams > 0) {
-                        putValues("folate", doubleVal(folateGrams))
+                    if (folate != null) {
+                        putValues("folate", doubleVal(folate.inGrams))
                     }
-                    if (folicAcidGrams > 0) {
-                        putValues("folicAcid", doubleVal(folicAcidGrams))
+                    if (folicAcid != null) {
+                        putValues("folicAcid", doubleVal(folicAcid.inGrams))
                     }
-                    if (iodineGrams > 0) {
-                        putValues("iodine", doubleVal(iodineGrams))
+                    if (iodine != null) {
+                        putValues("iodine", doubleVal(iodine.inGrams))
                     }
-                    if (ironGrams > 0) {
-                        putValues("iron", doubleVal(ironGrams))
+                    if (iron != null) {
+                        putValues("iron", doubleVal(iron.inGrams))
                     }
-                    if (magnesiumGrams > 0) {
-                        putValues("magnesium", doubleVal(magnesiumGrams))
+                    if (magnesium != null) {
+                        putValues("magnesium", doubleVal(magnesium.inGrams))
                     }
-                    if (manganeseGrams > 0) {
-                        putValues("manganese", doubleVal(manganeseGrams))
+                    if (manganese != null) {
+                        putValues("manganese", doubleVal(manganese.inGrams))
                     }
-                    if (molybdenumGrams > 0) {
-                        putValues("molybdenum", doubleVal(molybdenumGrams))
+                    if (molybdenum != null) {
+                        putValues("molybdenum", doubleVal(molybdenum.inGrams))
                     }
-                    if (monounsaturatedFatGrams > 0) {
-                        putValues("monounsaturatedFat", doubleVal(monounsaturatedFatGrams))
+                    if (monounsaturatedFat != null) {
+                        putValues("monounsaturatedFat", doubleVal(monounsaturatedFat.inGrams))
                     }
-                    if (niacinGrams > 0) {
-                        putValues("niacin", doubleVal(niacinGrams))
+                    if (niacin != null) {
+                        putValues("niacin", doubleVal(niacin.inGrams))
                     }
-                    if (pantothenicAcidGrams > 0) {
-                        putValues("pantothenicAcid", doubleVal(pantothenicAcidGrams))
+                    if (pantothenicAcid != null) {
+                        putValues("pantothenicAcid", doubleVal(pantothenicAcid.inGrams))
                     }
-                    if (phosphorusGrams > 0) {
-                        putValues("phosphorus", doubleVal(phosphorusGrams))
+                    if (phosphorus != null) {
+                        putValues("phosphorus", doubleVal(phosphorus.inGrams))
                     }
-                    if (polyunsaturatedFatGrams > 0) {
-                        putValues("polyunsaturatedFat", doubleVal(polyunsaturatedFatGrams))
+                    if (polyunsaturatedFat != null) {
+                        putValues("polyunsaturatedFat", doubleVal(polyunsaturatedFat.inGrams))
                     }
-                    if (potassiumGrams > 0) {
-                        putValues("potassium", doubleVal(potassiumGrams))
+                    if (potassium != null) {
+                        putValues("potassium", doubleVal(potassium.inGrams))
                     }
-                    if (proteinGrams > 0) {
-                        putValues("protein", doubleVal(proteinGrams))
+                    if (protein != null) {
+                        putValues("protein", doubleVal(protein.inGrams))
                     }
-                    if (riboflavinGrams > 0) {
-                        putValues("riboflavin", doubleVal(riboflavinGrams))
+                    if (riboflavin != null) {
+                        putValues("riboflavin", doubleVal(riboflavin.inGrams))
                     }
-                    if (saturatedFatGrams > 0) {
-                        putValues("saturatedFat", doubleVal(saturatedFatGrams))
+                    if (saturatedFat != null) {
+                        putValues("saturatedFat", doubleVal(saturatedFat.inGrams))
                     }
-                    if (seleniumGrams > 0) {
-                        putValues("selenium", doubleVal(seleniumGrams))
+                    if (selenium != null) {
+                        putValues("selenium", doubleVal(selenium.inGrams))
                     }
-                    if (sodiumGrams > 0) {
-                        putValues("sodium", doubleVal(sodiumGrams))
+                    if (sodium != null) {
+                        putValues("sodium", doubleVal(sodium.inGrams))
                     }
-                    if (sugarGrams > 0) {
-                        putValues("sugar", doubleVal(sugarGrams))
+                    if (sugar != null) {
+                        putValues("sugar", doubleVal(sugar.inGrams))
                     }
-                    if (thiaminGrams > 0) {
-                        putValues("thiamin", doubleVal(thiaminGrams))
+                    if (thiamin != null) {
+                        putValues("thiamin", doubleVal(thiamin.inGrams))
                     }
-                    if (totalCarbohydrateGrams > 0) {
-                        putValues("totalCarbohydrate", doubleVal(totalCarbohydrateGrams))
+                    if (totalCarbohydrate != null) {
+                        putValues("totalCarbohydrate", doubleVal(totalCarbohydrate.inGrams))
                     }
-                    if (totalFatGrams > 0) {
-                        putValues("totalFat", doubleVal(totalFatGrams))
+                    if (totalFat != null) {
+                        putValues("totalFat", doubleVal(totalFat.inGrams))
                     }
-                    if (transFatGrams > 0) {
-                        putValues("transFat", doubleVal(transFatGrams))
+                    if (transFat != null) {
+                        putValues("transFat", doubleVal(transFat.inGrams))
                     }
-                    if (unsaturatedFatGrams > 0) {
-                        putValues("unsaturatedFat", doubleVal(unsaturatedFatGrams))
+                    if (unsaturatedFat != null) {
+                        putValues("unsaturatedFat", doubleVal(unsaturatedFat.inGrams))
                     }
-                    if (vitaminAGrams > 0) {
-                        putValues("vitaminA", doubleVal(vitaminAGrams))
+                    if (vitaminA != null) {
+                        putValues("vitaminA", doubleVal(vitaminA.inGrams))
                     }
-                    if (vitaminB12Grams > 0) {
-                        putValues("vitaminB12", doubleVal(vitaminB12Grams))
+                    if (vitaminB12 != null) {
+                        putValues("vitaminB12", doubleVal(vitaminB12.inGrams))
                     }
-                    if (vitaminB6Grams > 0) {
-                        putValues("vitaminB6", doubleVal(vitaminB6Grams))
+                    if (vitaminB6 != null) {
+                        putValues("vitaminB6", doubleVal(vitaminB6.inGrams))
                     }
-                    if (vitaminCGrams > 0) {
-                        putValues("vitaminC", doubleVal(vitaminCGrams))
+                    if (vitaminC != null) {
+                        putValues("vitaminC", doubleVal(vitaminC.inGrams))
                     }
-                    if (vitaminDGrams > 0) {
-                        putValues("vitaminD", doubleVal(vitaminDGrams))
+                    if (vitaminD != null) {
+                        putValues("vitaminD", doubleVal(vitaminD.inGrams))
                     }
-                    if (vitaminEGrams > 0) {
-                        putValues("vitaminE", doubleVal(vitaminEGrams))
+                    if (vitaminE != null) {
+                        putValues("vitaminE", doubleVal(vitaminE.inGrams))
                     }
-                    if (vitaminKGrams > 0) {
-                        putValues("vitaminK", doubleVal(vitaminKGrams))
+                    if (vitaminK != null) {
+                        putValues("vitaminK", doubleVal(vitaminK.inGrams))
                     }
-                    if (zincGrams > 0) {
-                        putValues("zinc", doubleVal(zincGrams))
+                    if (zinc != null) {
+                        putValues("zinc", doubleVal(zinc.inGrams))
                     }
                     mealType?.let { putValues("mealType", enumVal(it)) }
                     name?.let { putValues("name", stringVal(it)) }
@@ -504,7 +504,7 @@
         is TotalCaloriesBurnedRecord ->
             intervalProto()
                 .setDataType(protoDataType("TotalCaloriesBurned"))
-                .apply { putValues("energy", doubleVal(energyKcal)) }
+                .apply { putValues("energy", doubleVal(energy.inKilocalories)) }
                 .build()
         is WheelchairPushesRecord ->
             intervalProto()
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ActiveCaloriesBurnedRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ActiveCaloriesBurnedRecord.kt
index d8c196b..fcab192 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ActiveCaloriesBurnedRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ActiveCaloriesBurnedRecord.kt
@@ -17,6 +17,7 @@
 
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Energy
 import java.time.Instant
 import java.time.ZoneOffset
 
@@ -26,19 +27,27 @@
  * so both the start and end times should be set.
  */
 public class ActiveCaloriesBurnedRecord(
-    /** Energy in kilocalories. Required field. Valid range: 0-1000000. */
-    public val energyKcal: Double,
+    /** Energy in [Energy] unit. Required field. Valid range: 0-1000000 kcal. */
+    public val energy: Energy,
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : IntervalRecord {
+
+    init {
+        energy.requireNotLess(other = energy.zero(), "energy")
+    }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is ActiveCaloriesBurnedRecord) return false
 
-        if (energyKcal != other.energyKcal) return false
+        if (energy != other.energy) return false
         if (startTime != other.startTime) return false
         if (startZoneOffset != other.startZoneOffset) return false
         if (endTime != other.endTime) return false
@@ -48,9 +57,12 @@
         return true
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + energyKcal.hashCode()
+        var result = energy.hashCode()
+        result = 31 * result + startTime.hashCode()
         result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
         result = 31 * result + endTime.hashCode()
         result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
@@ -67,11 +79,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val ACTIVE_CALORIES_TOTAL: AggregateMetric<Double> =
+        val ACTIVE_CALORIES_TOTAL: AggregateMetric<Energy> =
             AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                ENERGY_FIELD_NAME
+                dataTypeName = TYPE_NAME,
+                aggregationType = AggregateMetric.AggregationType.TOTAL,
+                fieldName = ENERGY_FIELD_NAME,
+                mapper = Energy::calories,
             )
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalBodyTemperatureRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalBodyTemperatureRecord.kt
index 725dd3c..90da526 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalBodyTemperatureRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalBodyTemperatureRecord.kt
@@ -30,7 +30,9 @@
     public val temperature: Temperature,
     /**
      * Where on the user's basal body the temperature measurement was taken from. Optional field.
-     * Allowed values: [BodyTemperatureMeasurementLocations].
+     * Allowed values: [BodyTemperatureMeasurementLocation].
+     *
+     * @see BodyTemperatureMeasurementLocation
      */
     @property:BodyTemperatureMeasurementLocations public val measurementLocation: String? = null,
     override val time: Instant,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalMetabolicRateRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalMetabolicRateRecord.kt
index 3b59c97..156351e 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalMetabolicRateRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalMetabolicRateRecord.kt
@@ -17,25 +17,31 @@
 
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Power
 import java.time.Instant
 import java.time.ZoneOffset
 
 /**
- * Captures the BMR of a user, in kilocalories. Each record represents the number of kilocalories a
- * user would burn if at rest all day, based on their height and weight.
+ * Captures the BMR of a user. Each record represents the energy a user would burn if at rest all
+ * day, based on their height and weight.
  */
 public class BasalMetabolicRateRecord(
-    /** Basal metabolic rate, in kilocalories. Required field. Valid range: 0-10000. */
-    public val kcalPerDay: Double,
+    /** Basal metabolic rate, in [Power] unit. Required field. Valid range: 0-10000 kcal/day. */
+    public val basalMetabolicRate: Power,
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : InstantaneousRecord {
+
+    init {
+        basalMetabolicRate.requireNotLess(other = basalMetabolicRate.zero(), name = "bmr")
+    }
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is BasalMetabolicRateRecord) return false
 
-        if (kcalPerDay != other.kcalPerDay) return false
+        if (basalMetabolicRate != other.basalMetabolicRate) return false
         if (time != other.time) return false
         if (zoneOffset != other.zoneOffset) return false
         if (metadata != other.metadata) return false
@@ -44,8 +50,7 @@
     }
 
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + kcalPerDay.hashCode()
+        var result = basalMetabolicRate.hashCode()
         result = 31 * result + time.hashCode()
         result = 31 * result + (zoneOffset?.hashCode() ?: 0)
         result = 31 * result + metadata.hashCode()
@@ -61,11 +66,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val BASAL_CALORIES_TOTAL: AggregateMetric<Double> =
+        val BASAL_CALORIES_TOTAL: AggregateMetric<Power> =
             AggregateMetric.doubleMetric(
-                BASAL_CALORIES_TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                ENERGY_FIELD_NAME
+                dataTypeName = BASAL_CALORIES_TYPE_NAME,
+                aggregationType = AggregateMetric.AggregationType.TOTAL,
+                fieldName = ENERGY_FIELD_NAME,
+                mapper = Power::kilocaloriesPerDay,
             )
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt
index fd38aac..18ff122 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt
@@ -34,16 +34,22 @@
     /**
      * Type of body fluid used to measure the blood glucose. Optional, enum field. Allowed values:
      * [SpecimenSource].
+     *
+     * @see SpecimenSource
      */
     @property:SpecimenSources public val specimenSource: String? = null,
     /**
      * Type of meal related to the blood glucose measurement. Optional, enum field. Allowed values:
-     * [MealTypes].
+     * [MealType].
+     *
+     * @see MealType
      */
     @property:MealTypes public val mealType: String? = null,
     /**
      * Relationship of the meal to the blood glucose measurement. Optional, enum field. Allowed
-     * values: [RelationToMeals].
+     * values: [RelationToMeal].
+     *
+     * @see RelationToMeal
      */
     @property:RelationToMeals public val relationToMeal: String? = null,
     override val time: Instant,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BloodPressureRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BloodPressureRecord.kt
index 2318fac..c5f62db 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BloodPressureRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BloodPressureRecord.kt
@@ -18,6 +18,8 @@
 import androidx.annotation.StringDef
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.records.BloodPressureRecord.MeasurementLocation
+import androidx.health.connect.client.units.Pressure
 import java.time.Instant
 import java.time.ZoneOffset
 
@@ -27,23 +29,27 @@
  */
 public class BloodPressureRecord(
     /**
-     * Systolic blood pressure measurement, in millimetres of mercury (mmHg). Required field. Valid
-     * range: 20-200.
+     * Systolic blood pressure measurement, in [Pressure] unit. Required field. Valid range: 20-200
+     * mmHg.
      */
-    public val systolicMillimetersOfMercury: Double,
+    public val systolic: Pressure,
     /**
-     * Diastolic blood pressure measurement, in millimetres of mercury (mmHg). Required field. Valid
-     * range: 10-180.
+     * Diastolic blood pressure measurement, in [Pressure] unit. Required field. Valid range:
+     * 10-180 mmHg.
      */
-    public val diastolicMillimetersOfMercury: Double,
+    public val diastolic: Pressure,
     /**
      * The user's body position when the measurement was taken. Optional field. Allowed values:
-     * [BodyPositions].
+     * [BodyPosition].
+     *
+     * @see BodyPosition
      */
     @property:BodyPositions public val bodyPosition: String? = null,
     /**
      * The arm and part of the arm where the measurement was taken. Optional field. Allowed values:
      * [MeasurementLocation].
+     *
+     * @see MeasurementLocation
      */
     @property:MeasurementLocations public val measurementLocation: String? = null,
     override val time: Instant,
@@ -52,22 +58,19 @@
 ) : InstantaneousRecord {
 
     init {
-        requireNonNegative(
-            value = systolicMillimetersOfMercury,
-            name = "systolicMillimetersOfMercury"
-        )
-        requireNonNegative(
-            value = diastolicMillimetersOfMercury,
-            name = "diastolicMillimetersOfMercury"
-        )
+        systolic.requireNotLess(other = systolic.zero(), name = "systolic")
+        diastolic.requireNotLess(other = diastolic.zero(), name = "diastolic")
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is BloodPressureRecord) return false
 
-        if (systolicMillimetersOfMercury != other.systolicMillimetersOfMercury) return false
-        if (diastolicMillimetersOfMercury != other.diastolicMillimetersOfMercury) return false
+        if (systolic != other.systolic) return false
+        if (diastolic != other.diastolic) return false
         if (bodyPosition != other.bodyPosition) return false
         if (measurementLocation != other.measurementLocation) return false
         if (time != other.time) return false
@@ -77,12 +80,14 @@
         return true
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + systolicMillimetersOfMercury.hashCode()
-        result = 31 * result + diastolicMillimetersOfMercury.hashCode()
-        result = 31 * result + bodyPosition.hashCode()
-        result = 31 * result + measurementLocation.hashCode()
+        var result = systolic.hashCode()
+        result = 31 * result + diastolic.hashCode()
+        result = 31 * result + (bodyPosition?.hashCode() ?: 0)
+        result = 31 * result + (measurementLocation?.hashCode() ?: 0)
         result = 31 * result + time.hashCode()
         result = 31 * result + (zoneOffset?.hashCode() ?: 0)
         result = 31 * result + metadata.hashCode()
@@ -123,11 +128,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SYSTOLIC_AVG: AggregateMetric<Double> =
+        val SYSTOLIC_AVG: AggregateMetric<Pressure> =
             AggregateMetric.doubleMetric(
-                BLOOD_PRESSURE_NAME,
-                AggregateMetric.AggregationType.AVERAGE,
-                SYSTOLIC_FIELD_NAME
+                dataTypeName = BLOOD_PRESSURE_NAME,
+                aggregationType = AggregateMetric.AggregationType.AVERAGE,
+                fieldName = SYSTOLIC_FIELD_NAME,
+                mapper = Pressure::millimetersOfMercury,
             )
 
         /**
@@ -135,11 +141,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SYSTOLIC_MIN: AggregateMetric<Double> =
+        val SYSTOLIC_MIN: AggregateMetric<Pressure> =
             AggregateMetric.doubleMetric(
-                BLOOD_PRESSURE_NAME,
-                AggregateMetric.AggregationType.MINIMUM,
-                SYSTOLIC_FIELD_NAME
+                dataTypeName = BLOOD_PRESSURE_NAME,
+                aggregationType = AggregateMetric.AggregationType.MINIMUM,
+                fieldName = SYSTOLIC_FIELD_NAME,
+                mapper = Pressure::millimetersOfMercury,
             )
 
         /**
@@ -147,11 +154,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SYSTOLIC_MAX: AggregateMetric<Double> =
+        val SYSTOLIC_MAX: AggregateMetric<Pressure> =
             AggregateMetric.doubleMetric(
-                BLOOD_PRESSURE_NAME,
-                AggregateMetric.AggregationType.MAXIMUM,
-                SYSTOLIC_FIELD_NAME
+                dataTypeName = BLOOD_PRESSURE_NAME,
+                aggregationType = AggregateMetric.AggregationType.MAXIMUM,
+                fieldName = SYSTOLIC_FIELD_NAME,
+                mapper = Pressure::millimetersOfMercury,
             )
 
         /**
@@ -159,11 +167,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val DIASTOLIC_AVG: AggregateMetric<Double> =
+        val DIASTOLIC_AVG: AggregateMetric<Pressure> =
             AggregateMetric.doubleMetric(
-                BLOOD_PRESSURE_NAME,
-                AggregateMetric.AggregationType.AVERAGE,
-                DIASTOLIC_FIELD_NAME
+                dataTypeName = BLOOD_PRESSURE_NAME,
+                aggregationType = AggregateMetric.AggregationType.AVERAGE,
+                fieldName = DIASTOLIC_FIELD_NAME,
+                mapper = Pressure::millimetersOfMercury,
             )
 
         /**
@@ -171,11 +180,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val DIASTOLIC_MIN: AggregateMetric<Double> =
+        val DIASTOLIC_MIN: AggregateMetric<Pressure> =
             AggregateMetric.doubleMetric(
-                BLOOD_PRESSURE_NAME,
-                AggregateMetric.AggregationType.MINIMUM,
-                DIASTOLIC_FIELD_NAME
+                dataTypeName = BLOOD_PRESSURE_NAME,
+                aggregationType = AggregateMetric.AggregationType.MINIMUM,
+                fieldName = DIASTOLIC_FIELD_NAME,
+                mapper = Pressure::millimetersOfMercury,
             )
 
         /**
@@ -183,11 +193,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val DIASTOLIC_MAX: AggregateMetric<Double> =
+        val DIASTOLIC_MAX: AggregateMetric<Pressure> =
             AggregateMetric.doubleMetric(
-                BLOOD_PRESSURE_NAME,
-                AggregateMetric.AggregationType.MAXIMUM,
-                DIASTOLIC_FIELD_NAME
+                dataTypeName = BLOOD_PRESSURE_NAME,
+                aggregationType = AggregateMetric.AggregationType.MAXIMUM,
+                fieldName = DIASTOLIC_FIELD_NAME,
+                mapper = Pressure::millimetersOfMercury,
             )
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureRecord.kt
index d2ca611..75c7fdc 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureRecord.kt
@@ -29,7 +29,9 @@
     public val temperature: Temperature,
     /**
      * Where on the user's body the temperature measurement was taken from. Optional field. Allowed
-     * values: [BodyTemperatureMeasurementLocations].
+     * values: [BodyTemperatureMeasurementLocation].
+     *
+     * @see BodyTemperatureMeasurementLocation
      */
     @property:BodyTemperatureMeasurementLocations public val measurementLocation: String? = null,
     override val time: Instant,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt
index 7e8958d..9099d8f 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt
@@ -17,6 +17,7 @@
 
 import androidx.annotation.RestrictTo
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Mass
 import java.time.Instant
 import java.time.ZoneOffset
 
@@ -25,22 +26,25 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 public class BodyWaterMassRecord(
-    /** Mass in kilograms. Required field. Valid range: 0-1000. */
-    public val massKg: Double,
+    /** Mass in [Mass] unit. Required field. Valid range: 0-1000 kilograms. */
+    public val mass: Mass,
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : InstantaneousRecord {
 
     init {
-        requireNonNegative(value = massKg, name = "massKg")
+        mass.requireNotLess(other = mass.zero(), name = "mass")
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is BodyWaterMassRecord) return false
 
-        if (massKg != other.massKg) return false
+        if (mass != other.mass) return false
         if (time != other.time) return false
         if (zoneOffset != other.zoneOffset) return false
         if (metadata != other.metadata) return false
@@ -48,9 +52,11 @@
         return true
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + massKg.hashCode()
+        var result = mass.hashCode()
         result = 31 * result + time.hashCode()
         result = 31 * result + (zoneOffset?.hashCode() ?: 0)
         result = 31 * result + metadata.hashCode()
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BoneMassRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BoneMassRecord.kt
index ed5b095..f0e5a65 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BoneMassRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BoneMassRecord.kt
@@ -16,26 +16,31 @@
 package androidx.health.connect.client.records
 
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Mass
 import java.time.Instant
 import java.time.ZoneOffset
 
 /** Captures the user's bone mass. Each record represents a single instantaneous measurement. */
 public class BoneMassRecord(
-    /** Mass in kilograms. Required field. Valid range: 0-1000. */
-    public val massKg: Double,
+    /** Mass in [Mass] unit. Required field. Valid range: 0-1000 kilograms. */
+    public val mass: Mass,
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : InstantaneousRecord {
+
     init {
-        requireNonNegative(value = massKg, name = "massKg")
+        mass.requireNotLess(other = mass.zero(), name = "mass")
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is BoneMassRecord) return false
 
-        if (massKg != other.massKg) return false
+        if (mass != other.mass) return false
         if (time != other.time) return false
         if (zoneOffset != other.zoneOffset) return false
         if (metadata != other.metadata) return false
@@ -43,9 +48,11 @@
         return true
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + massKg.hashCode()
+        var result = mass.hashCode()
         result = 31 * result + time.hashCode()
         result = 31 * result + (zoneOffset?.hashCode() ?: 0)
         result = 31 * result + metadata.hashCode()
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt
index 42176b7..fa8f627 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt
@@ -27,15 +27,21 @@
  * Captures the description of cervical mucus. Each record represents a self-assessed description of
  * cervical mucus for a user. All fields are optional and can be used to describe the look and feel
  * of cervical mucus.
- *
- * @param appearance The consistency of the user's cervical mucus. Optional field. Allowed values:
- * [Appearances].
- *
- * @param sensation The feel of the user's cervical mucus. Optional field. Allowed values:
- * [Sensations].
  */
 public class CervicalMucusRecord(
+    /**
+     * The consistency of the user's cervical mucus. Optional field. Allowed values:
+     * [Appearance].
+     *
+     * @see Appearance
+     */
     @property:Appearances public val appearance: String? = null,
+    /**
+     * The feel of the user's cervical mucus. Optional field. Allowed values:
+     * [Sensation].
+     *
+     * @see Sensation
+     */
     @property:Sensations public val sensation: String? = null,
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
index b912037..3ed5f05 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
@@ -28,7 +28,11 @@
  * For pause events, resume state can be assumed from the end time of the pause or rest event.
  */
 public class ExerciseEventRecord(
-    /** Type of event. Required field. Allowed values: [EventType]. */
+    /**
+     * Type of event. Required field. Allowed values: [EventType].
+     *
+     * @see EventType
+     */
     @property:EventTypes public val eventType: String,
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRepetitionsRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRepetitionsRecord.kt
index caf4227..0f1c681 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRepetitionsRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRepetitionsRecord.kt
@@ -25,7 +25,11 @@
 public class ExerciseRepetitionsRecord(
     /** Count. Required field. Valid range: 1-1000000. */
     public val count: Long,
-    /** Type of exercise being repeated. Required field. Allowed values: [ExerciseType]. */
+    /**
+     * Type of exercise being repeated. Required field. Allowed values: [ExerciseType].
+     *
+     * @see ExerciseType
+     */
     @property:ExerciseTypes public val type: String,
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
index e0cddf1..fee9475 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
@@ -33,6 +33,8 @@
 public class ExerciseSessionRecord(
     /**
      * Type of exercise (e.g. walking, swimming). Required field. Allowed values: [ExerciseType].
+     *
+     * @see ExerciseType
      */
     @property:ExerciseTypes public val exerciseType: String,
     /** Title of the session. Optional field. */
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/HydrationRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/HydrationRecord.kt
index dab1191..7228b46 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/HydrationRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/HydrationRecord.kt
@@ -17,13 +17,14 @@
 
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Volume
 import java.time.Instant
 import java.time.ZoneOffset
 
 /** Captures how much water a user drank in a single drink. */
 public class HydrationRecord(
-    /** Volume of water in liters. Required field. Valid range: 0-100. */
-    public val volumeLiters: Double,
+    /** Volume of water in [Volume] unit. Required field. Valid range: 0-100 liters. */
+    public val volume: Volume,
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
@@ -32,14 +33,14 @@
 ) : IntervalRecord {
 
     init {
-        requireNonNegative(value = volumeLiters, name = "volumeLiters")
+        volume.requireNotLess(other = volume.zero(), name = "volume")
     }
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is HydrationRecord) return false
 
-        if (volumeLiters != other.volumeLiters) return false
+        if (volume != other.volume) return false
         if (startTime != other.startTime) return false
         if (startZoneOffset != other.startZoneOffset) return false
         if (endTime != other.endTime) return false
@@ -50,8 +51,8 @@
     }
 
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + volumeLiters.hashCode()
+        var result = volume.hashCode()
+        result = 31 * result + startTime.hashCode()
         result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
         result = 31 * result + endTime.hashCode()
         result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
@@ -65,11 +66,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val VOLUME_TOTAL: AggregateMetric<Double> =
+        val VOLUME_TOTAL: AggregateMetric<Volume> =
             AggregateMetric.doubleMetric(
-                "Hydration",
-                AggregateMetric.AggregationType.TOTAL,
-                "volume"
+                dataTypeName = "Hydration",
+                aggregationType = AggregateMetric.AggregationType.TOTAL,
+                fieldName = "volume",
+                mapper = Volume::liters,
             )
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/LeanBodyMassRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/LeanBodyMassRecord.kt
index 94817d1..93d6113 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/LeanBodyMassRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/LeanBodyMassRecord.kt
@@ -16,6 +16,7 @@
 package androidx.health.connect.client.records
 
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Mass
 import java.time.Instant
 import java.time.ZoneOffset
 
@@ -23,22 +24,25 @@
  * Captures the user's lean body mass. Each record represents a single instantaneous measurement.
  */
 public class LeanBodyMassRecord(
-    /** Mass in kilograms. Required field. Valid range: 0-1000. */
-    public val massKg: Double,
+    /** Mass in [Mass] unit. Required field. Valid range: 0-1000 kilograms. */
+    public val mass: Mass,
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : InstantaneousRecord {
 
     init {
-        requireNonNegative(value = massKg, name = "massKg")
+        mass.requireNotLess(other = mass.zero(), name = "mass")
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is LeanBodyMassRecord) return false
 
-        if (massKg != other.massKg) return false
+        if (mass != other.mass) return false
         if (time != other.time) return false
         if (zoneOffset != other.zoneOffset) return false
         if (metadata != other.metadata) return false
@@ -46,9 +50,11 @@
         return true
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + massKg.hashCode()
+        var result = mass.hashCode()
         result = 31 * result + time.hashCode()
         result = 31 * result + (zoneOffset?.hashCode() ?: 0)
         result = 31 * result + metadata.hashCode()
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/MenstruationRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/MenstruationRecord.kt
index addd444..4f3238e 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/MenstruationRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/MenstruationRecord.kt
@@ -26,7 +26,11 @@
  * heavy). Each record represents a description of how heavy the user's menstrual bleeding was.
  */
 public class MenstruationRecord(
-    /** How heavy the user's menstrual flow was. Optional field. Allowed values: [Flows]. */
+    /**
+     * How heavy the user's menstrual flow was. Optional field. Allowed values: [Flow].
+     *
+     * @see Flow
+     */
     @property:Flows public val flow: String? = null,
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt
index 7fec5a2..fe3ea9fc 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt
@@ -16,101 +16,107 @@
 package androidx.health.connect.client.records
 
 import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.aggregate.AggregateMetric.AggregationType
+import androidx.health.connect.client.aggregate.AggregateMetric.Companion.doubleMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Energy
+import androidx.health.connect.client.units.Mass
 import java.time.Instant
 import java.time.ZoneOffset
 
 /** Captures what nutrients were consumed as part of a meal or a food item. */
 public class NutritionRecord(
-    /** Biotin in grams. Optional field. Valid range: 0-100. */
-    public val biotinGrams: Double = 0.0,
-    /** Caffeine in grams. Optional field. Valid range: 0-100. */
-    public val caffeineGrams: Double = 0.0,
-    /** Calcium in grams. Optional field. Valid range: 0-100. */
-    public val calciumGrams: Double = 0.0,
-    /** Calories in kilocalories. Optional field. Valid range: 0-100000. */
-    public val kcal: Double = 0.0,
-    /** Calories from fat in kilocalories. Optional field. Valid range: 0-100000. */
-    public val kcalFromFat: Double = 0.0,
-    /** Chloride in grams. Optional field. Valid range: 0-100. */
-    public val chlorideGrams: Double = 0.0,
-    /** Cholesterol in grams. Optional field. Valid range: 0-100. */
-    public val cholesterolGrams: Double = 0.0,
-    /** Chromium in grams. Optional field. Valid range: 0-100. */
-    public val chromiumGrams: Double = 0.0,
-    /** Copper in grams. Optional field. Valid range: 0-100. */
-    public val copperGrams: Double = 0.0,
-    /** Dietary fiber in grams. Optional field. Valid range: 0-100000. */
-    public val dietaryFiberGrams: Double = 0.0,
-    /** Folate in grams. Optional field. Valid range: 0-100. */
-    public val folateGrams: Double = 0.0,
-    /** Folic acid in grams. Optional field. Valid range: 0-100. */
-    public val folicAcidGrams: Double = 0.0,
-    /** Iodine in grams. Optional field. Valid range: 0-100. */
-    public val iodineGrams: Double = 0.0,
-    /** Iron in grams. Optional field. Valid range: 0-100. */
-    public val ironGrams: Double = 0.0,
-    /** Magnesium in grams. Optional field. Valid range: 0-100. */
-    public val magnesiumGrams: Double = 0.0,
-    /** Manganese in grams. Optional field. Valid range: 0-100. */
-    public val manganeseGrams: Double = 0.0,
-    /** Molybdenum in grams. Optional field. Valid range: 0-100. */
-    public val molybdenumGrams: Double = 0.0,
-    /** Monounsaturated fat in grams. Optional field. Valid range: 0-100000. */
-    public val monounsaturatedFatGrams: Double = 0.0,
-    /** Niacin in grams. Optional field. Valid range: 0-100. */
-    public val niacinGrams: Double = 0.0,
-    /** Pantothenic acid in grams. Optional field. Valid range: 0-100. */
-    public val pantothenicAcidGrams: Double = 0.0,
-    /** Phosphorus in grams. Optional field. Valid range: 0-100. */
-    public val phosphorusGrams: Double = 0.0,
-    /** Polyunsaturated fat in grams. Optional field. Valid range: 0-100000. */
-    public val polyunsaturatedFatGrams: Double = 0.0,
-    /** Potassium in grams. Optional field. Valid range: 0-100. */
-    public val potassiumGrams: Double = 0.0,
-    /** Protein in grams. Optional field. Valid range: 0-100000. */
-    public val proteinGrams: Double = 0.0,
-    /** Riboflavin in grams. Optional field. Valid range: 0-100. */
-    public val riboflavinGrams: Double = 0.0,
-    /** Saturated fat in grams. Optional field. Valid range: 0-100000. */
-    public val saturatedFatGrams: Double = 0.0,
-    /** Selenium in grams. Optional field. Valid range: 0-100. */
-    public val seleniumGrams: Double = 0.0,
-    /** Sodium in grams. Optional field. Valid range: 0-100. */
-    public val sodiumGrams: Double = 0.0,
-    /** Sugar in grams. Optional field. Valid range: 0-100000. */
-    public val sugarGrams: Double = 0.0,
-    /** Thiamin in grams. Optional field. Valid range: 0-100. */
-    public val thiaminGrams: Double = 0.0,
-    /** Total carbohydrate in grams. Optional field. Valid range: 0-100000. */
-    public val totalCarbohydrateGrams: Double = 0.0,
-    /** Total fat in grams. Optional field. Valid range: 0-100000. */
-    public val totalFatGrams: Double = 0.0,
-    /** Trans fat in grams. Optional field. Valid range: 0-100000. */
-    public val transFatGrams: Double = 0.0,
-    /** Unsaturated fat in grams. Optional field. Valid range: 0-100000. */
-    public val unsaturatedFatGrams: Double = 0.0,
-    /** Vitamin A in grams. Optional field. Valid range: 0-100. */
-    public val vitaminAGrams: Double = 0.0,
-    /** Vitamin B12 in grams. Optional field. Valid range: 0-100. */
-    public val vitaminB12Grams: Double = 0.0,
-    /** Vitamin B6 in grams. Optional field. Valid range: 0-100. */
-    public val vitaminB6Grams: Double = 0.0,
-    /** Vitamin C in grams. Optional field. Valid range: 0-100. */
-    public val vitaminCGrams: Double = 0.0,
-    /** Vitamin D in grams. Optional field. Valid range: 0-100. */
-    public val vitaminDGrams: Double = 0.0,
-    /** Vitamin E in grams. Optional field. Valid range: 0-100. */
-    public val vitaminEGrams: Double = 0.0,
-    /** Vitamin K in grams. Optional field. Valid range: 0-100. */
-    public val vitaminKGrams: Double = 0.0,
-    /** Zinc in grams. Optional field. Valid range: 0-100. */
-    public val zincGrams: Double = 0.0,
+    /** Biotin in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val biotin: Mass? = null,
+    /** Caffeine in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val caffeine: Mass? = null,
+    /** Calcium in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val calcium: Mass? = null,
+    /** Energy in [Energy] unit. Optional field. Valid range: 0-100000 kcal. */
+    public val energy: Energy? = null,
+    /** Energy from fat in [Energy] unit. Optional field. Valid range: 0-100000 kcal. */
+    public val energyFromFat: Energy? = null,
+    /** Chloride in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val chloride: Mass? = null,
+    /** Cholesterol in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val cholesterol: Mass? = null,
+    /** Chromium in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val chromium: Mass? = null,
+    /** Copper in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val copper: Mass? = null,
+    /** Dietary fiber in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val dietaryFiber: Mass? = null,
+    /** Folate in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val folate: Mass? = null,
+    /** Folic acid in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val folicAcid: Mass? = null,
+    /** Iodine in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val iodine: Mass? = null,
+    /** Iron in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val iron: Mass? = null,
+    /** Magnesium in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val magnesium: Mass? = null,
+    /** Manganese in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val manganese: Mass? = null,
+    /** Molybdenum in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val molybdenum: Mass? = null,
+    /** Monounsaturated fat in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val monounsaturatedFat: Mass? = null,
+    /** Niacin in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val niacin: Mass? = null,
+    /** Pantothenic acid in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val pantothenicAcid: Mass? = null,
+    /** Phosphorus in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val phosphorus: Mass? = null,
+    /** Polyunsaturated fat in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val polyunsaturatedFat: Mass? = null,
+    /** Potassium in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val potassium: Mass? = null,
+    /** Protein in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val protein: Mass? = null,
+    /** Riboflavin in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val riboflavin: Mass? = null,
+    /** Saturated fat in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val saturatedFat: Mass? = null,
+    /** Selenium in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val selenium: Mass? = null,
+    /** Sodium in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val sodium: Mass? = null,
+    /** Sugar in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val sugar: Mass? = null,
+    /** Thiamin in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val thiamin: Mass? = null,
+    /** Total carbohydrate in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val totalCarbohydrate: Mass? = null,
+    /** Total fat in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val totalFat: Mass? = null,
+    /** Trans fat in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val transFat: Mass? = null,
+    /** Unsaturated fat in [Mass] unit. Optional field. Valid range: 0-100000 grams. */
+    public val unsaturatedFat: Mass? = null,
+    /** Vitamin A in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val vitaminA: Mass? = null,
+    /** Vitamin B12 in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val vitaminB12: Mass? = null,
+    /** Vitamin B6 in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val vitaminB6: Mass? = null,
+    /** Vitamin C in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val vitaminC: Mass? = null,
+    /** Vitamin D in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val vitaminD: Mass? = null,
+    /** Vitamin E in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val vitaminE: Mass? = null,
+    /** Vitamin K in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val vitaminK: Mass? = null,
+    /** Zinc in [Mass] unit. Optional field. Valid range: 0-100 grams. */
+    public val zinc: Mass? = null,
     /** Name for food or drink, provided by the user. Optional field. */
     public val name: String? = null,
     /**
      * Type of meal related to the nutrients consumed. Optional, enum field. Allowed values:
-     * [MealTypes].
+     * [MealType].
+     *
+     * @see MealType
      */
     @property:MealTypes public val mealType: String? = null,
     override val startTime: Instant,
@@ -119,54 +125,58 @@
     override val endZoneOffset: ZoneOffset?,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : IntervalRecord {
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is NutritionRecord) return false
 
-        if (biotinGrams != other.biotinGrams) return false
-        if (caffeineGrams != other.caffeineGrams) return false
-        if (calciumGrams != other.calciumGrams) return false
-        if (kcal != other.kcal) return false
-        if (kcalFromFat != other.kcalFromFat) return false
-        if (chlorideGrams != other.chlorideGrams) return false
-        if (cholesterolGrams != other.cholesterolGrams) return false
-        if (chromiumGrams != other.chromiumGrams) return false
-        if (copperGrams != other.copperGrams) return false
-        if (dietaryFiberGrams != other.dietaryFiberGrams) return false
-        if (folateGrams != other.folateGrams) return false
-        if (folicAcidGrams != other.folicAcidGrams) return false
-        if (iodineGrams != other.iodineGrams) return false
-        if (ironGrams != other.ironGrams) return false
-        if (magnesiumGrams != other.magnesiumGrams) return false
-        if (manganeseGrams != other.manganeseGrams) return false
-        if (molybdenumGrams != other.molybdenumGrams) return false
-        if (monounsaturatedFatGrams != other.monounsaturatedFatGrams) return false
-        if (niacinGrams != other.niacinGrams) return false
-        if (pantothenicAcidGrams != other.pantothenicAcidGrams) return false
-        if (phosphorusGrams != other.phosphorusGrams) return false
-        if (polyunsaturatedFatGrams != other.polyunsaturatedFatGrams) return false
-        if (potassiumGrams != other.potassiumGrams) return false
-        if (proteinGrams != other.proteinGrams) return false
-        if (riboflavinGrams != other.riboflavinGrams) return false
-        if (saturatedFatGrams != other.saturatedFatGrams) return false
-        if (seleniumGrams != other.seleniumGrams) return false
-        if (sodiumGrams != other.sodiumGrams) return false
-        if (sugarGrams != other.sugarGrams) return false
-        if (thiaminGrams != other.thiaminGrams) return false
-        if (totalCarbohydrateGrams != other.totalCarbohydrateGrams) return false
-        if (totalFatGrams != other.totalFatGrams) return false
-        if (transFatGrams != other.transFatGrams) return false
-        if (unsaturatedFatGrams != other.unsaturatedFatGrams) return false
-        if (vitaminAGrams != other.vitaminAGrams) return false
-        if (vitaminB12Grams != other.vitaminB12Grams) return false
-        if (vitaminB6Grams != other.vitaminB6Grams) return false
-        if (vitaminCGrams != other.vitaminCGrams) return false
-        if (vitaminDGrams != other.vitaminDGrams) return false
-        if (vitaminEGrams != other.vitaminEGrams) return false
-        if (vitaminKGrams != other.vitaminKGrams) return false
-        if (zincGrams != other.zincGrams) return false
-        if (mealType != other.mealType) return false
+        if (biotin != other.biotin) return false
+        if (caffeine != other.caffeine) return false
+        if (calcium != other.calcium) return false
+        if (energy != other.energy) return false
+        if (energyFromFat != other.energyFromFat) return false
+        if (chloride != other.chloride) return false
+        if (cholesterol != other.cholesterol) return false
+        if (chromium != other.chromium) return false
+        if (copper != other.copper) return false
+        if (dietaryFiber != other.dietaryFiber) return false
+        if (folate != other.folate) return false
+        if (folicAcid != other.folicAcid) return false
+        if (iodine != other.iodine) return false
+        if (iron != other.iron) return false
+        if (magnesium != other.magnesium) return false
+        if (manganese != other.manganese) return false
+        if (molybdenum != other.molybdenum) return false
+        if (monounsaturatedFat != other.monounsaturatedFat) return false
+        if (niacin != other.niacin) return false
+        if (pantothenicAcid != other.pantothenicAcid) return false
+        if (phosphorus != other.phosphorus) return false
+        if (polyunsaturatedFat != other.polyunsaturatedFat) return false
+        if (potassium != other.potassium) return false
+        if (protein != other.protein) return false
+        if (riboflavin != other.riboflavin) return false
+        if (saturatedFat != other.saturatedFat) return false
+        if (selenium != other.selenium) return false
+        if (sodium != other.sodium) return false
+        if (sugar != other.sugar) return false
+        if (thiamin != other.thiamin) return false
+        if (totalCarbohydrate != other.totalCarbohydrate) return false
+        if (totalFat != other.totalFat) return false
+        if (transFat != other.transFat) return false
+        if (unsaturatedFat != other.unsaturatedFat) return false
+        if (vitaminA != other.vitaminA) return false
+        if (vitaminB12 != other.vitaminB12) return false
+        if (vitaminB6 != other.vitaminB6) return false
+        if (vitaminC != other.vitaminC) return false
+        if (vitaminD != other.vitaminD) return false
+        if (vitaminE != other.vitaminE) return false
+        if (vitaminK != other.vitaminK) return false
+        if (zinc != other.zinc) return false
         if (name != other.name) return false
+        if (mealType != other.mealType) return false
         if (startTime != other.startTime) return false
         if (startZoneOffset != other.startZoneOffset) return false
         if (endTime != other.endTime) return false
@@ -176,52 +186,55 @@
         return true
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + biotinGrams.hashCode()
-        result = 31 * result + caffeineGrams.hashCode()
-        result = 31 * result + calciumGrams.hashCode()
-        result = 31 * result + kcal.hashCode()
-        result = 31 * result + kcalFromFat.hashCode()
-        result = 31 * result + chlorideGrams.hashCode()
-        result = 31 * result + cholesterolGrams.hashCode()
-        result = 31 * result + chromiumGrams.hashCode()
-        result = 31 * result + copperGrams.hashCode()
-        result = 31 * result + dietaryFiberGrams.hashCode()
-        result = 31 * result + folateGrams.hashCode()
-        result = 31 * result + folicAcidGrams.hashCode()
-        result = 31 * result + iodineGrams.hashCode()
-        result = 31 * result + ironGrams.hashCode()
-        result = 31 * result + magnesiumGrams.hashCode()
-        result = 31 * result + manganeseGrams.hashCode()
-        result = 31 * result + molybdenumGrams.hashCode()
-        result = 31 * result + monounsaturatedFatGrams.hashCode()
-        result = 31 * result + niacinGrams.hashCode()
-        result = 31 * result + pantothenicAcidGrams.hashCode()
-        result = 31 * result + phosphorusGrams.hashCode()
-        result = 31 * result + polyunsaturatedFatGrams.hashCode()
-        result = 31 * result + potassiumGrams.hashCode()
-        result = 31 * result + proteinGrams.hashCode()
-        result = 31 * result + riboflavinGrams.hashCode()
-        result = 31 * result + saturatedFatGrams.hashCode()
-        result = 31 * result + seleniumGrams.hashCode()
-        result = 31 * result + sodiumGrams.hashCode()
-        result = 31 * result + sugarGrams.hashCode()
-        result = 31 * result + thiaminGrams.hashCode()
-        result = 31 * result + totalCarbohydrateGrams.hashCode()
-        result = 31 * result + totalFatGrams.hashCode()
-        result = 31 * result + transFatGrams.hashCode()
-        result = 31 * result + unsaturatedFatGrams.hashCode()
-        result = 31 * result + vitaminAGrams.hashCode()
-        result = 31 * result + vitaminB12Grams.hashCode()
-        result = 31 * result + vitaminB6Grams.hashCode()
-        result = 31 * result + vitaminCGrams.hashCode()
-        result = 31 * result + vitaminDGrams.hashCode()
-        result = 31 * result + vitaminEGrams.hashCode()
-        result = 31 * result + vitaminKGrams.hashCode()
-        result = 31 * result + zincGrams.hashCode()
-        result = 31 * result + mealType.hashCode()
-        result = 31 * result + name.hashCode()
+        var result = biotin.hashCode()
+        result = 31 * result + caffeine.hashCode()
+        result = 31 * result + calcium.hashCode()
+        result = 31 * result + energy.hashCode()
+        result = 31 * result + energyFromFat.hashCode()
+        result = 31 * result + chloride.hashCode()
+        result = 31 * result + cholesterol.hashCode()
+        result = 31 * result + chromium.hashCode()
+        result = 31 * result + copper.hashCode()
+        result = 31 * result + dietaryFiber.hashCode()
+        result = 31 * result + folate.hashCode()
+        result = 31 * result + folicAcid.hashCode()
+        result = 31 * result + iodine.hashCode()
+        result = 31 * result + iron.hashCode()
+        result = 31 * result + magnesium.hashCode()
+        result = 31 * result + manganese.hashCode()
+        result = 31 * result + molybdenum.hashCode()
+        result = 31 * result + monounsaturatedFat.hashCode()
+        result = 31 * result + niacin.hashCode()
+        result = 31 * result + pantothenicAcid.hashCode()
+        result = 31 * result + phosphorus.hashCode()
+        result = 31 * result + polyunsaturatedFat.hashCode()
+        result = 31 * result + potassium.hashCode()
+        result = 31 * result + protein.hashCode()
+        result = 31 * result + riboflavin.hashCode()
+        result = 31 * result + saturatedFat.hashCode()
+        result = 31 * result + selenium.hashCode()
+        result = 31 * result + sodium.hashCode()
+        result = 31 * result + sugar.hashCode()
+        result = 31 * result + thiamin.hashCode()
+        result = 31 * result + totalCarbohydrate.hashCode()
+        result = 31 * result + totalFat.hashCode()
+        result = 31 * result + transFat.hashCode()
+        result = 31 * result + unsaturatedFat.hashCode()
+        result = 31 * result + vitaminA.hashCode()
+        result = 31 * result + vitaminB12.hashCode()
+        result = 31 * result + vitaminB6.hashCode()
+        result = 31 * result + vitaminC.hashCode()
+        result = 31 * result + vitaminD.hashCode()
+        result = 31 * result + vitaminE.hashCode()
+        result = 31 * result + vitaminK.hashCode()
+        result = 31 * result + zinc.hashCode()
+        result = 31 * result + (name?.hashCode() ?: 0)
+        result = 31 * result + (mealType?.hashCode() ?: 0)
+        result = 31 * result + startTime.hashCode()
         result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
         result = 31 * result + endTime.hashCode()
         result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
@@ -237,467 +250,335 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val BIOTIN_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(TYPE_NAME, AggregateMetric.AggregationType.TOTAL, "biotin")
+        val BIOTIN_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "biotin", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total caffeine from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val CAFFEINE_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "caffeine"
-            )
+        val CAFFEINE_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "caffeine", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total calcium from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val CALCIUM_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "calcium"
-            )
+        val CALCIUM_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "calcium", Mass::grams)
 
         /**
-         * Metric identifier to retrieve the total calories from
+         * Metric identifier to retrieve the total energy from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val CALORIES_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "calories"
-            )
+        val ENERGY_TOTAL: AggregateMetric<Energy> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "calories", Energy::calories)
 
         /**
-         * Metric identifier to retrieve the total calories from fat from
+         * Metric identifier to retrieve the total energy from fat from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val CALORIES_FROM_FAT_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "caloriesFromFat"
-            )
+        val ENERGY_FROM_FAT_TOTAL: AggregateMetric<Energy> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "caloriesFromFat", Energy::calories)
 
         /**
          * Metric identifier to retrieve the total chloride from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val CHLORIDE_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "chloride"
-            )
+        val CHLORIDE_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "chloride", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total cholesterol from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val CHOLESTEROL_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "cholesterol"
-            )
+        val CHOLESTEROL_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "cholesterol", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total chromium from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val CHROMIUM_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "chromium"
-            )
+        val CHROMIUM_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "chromium", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total copper from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val COPPER_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(TYPE_NAME, AggregateMetric.AggregationType.TOTAL, "copper")
+        val COPPER_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "copper", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total dietary fiber from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val DIETARY_FIBER_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "dietaryFiber"
-            )
+        val DIETARY_FIBER_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "dietaryFiber", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total folate from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val FOLATE_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(TYPE_NAME, AggregateMetric.AggregationType.TOTAL, "folate")
+        val FOLATE_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "folate", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total folic acid from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val FOLIC_ACID_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "folicAcid"
-            )
+        val FOLIC_ACID_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "folicAcid", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total iodine from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val IODINE_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(TYPE_NAME, AggregateMetric.AggregationType.TOTAL, "iodine")
+        val IODINE_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "iodine", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total iron from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val IRON_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(TYPE_NAME, AggregateMetric.AggregationType.TOTAL, "iron")
+        val IRON_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "iron", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total magnesium from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val MAGNESIUM_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "magnesium"
-            )
+        val MAGNESIUM_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "magnesium", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total manganese from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val MANGANESE_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "manganese"
-            )
+        val MANGANESE_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "manganese", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total molybdenum from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val MOLYBDENUM_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "molybdenum"
-            )
+        val MOLYBDENUM_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "molybdenum", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total monounsaturated fat from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val MONOUNSATURATED_FAT_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "monounsaturatedFat"
-            )
+        val MONOUNSATURATED_FAT_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "monounsaturatedFat", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total niacin from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val NIACIN_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(TYPE_NAME, AggregateMetric.AggregationType.TOTAL, "niacin")
+        val NIACIN_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "niacin", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total pantothenic acid from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val PANTOTHENIC_ACID_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "pantothenicAcid"
-            )
+        val PANTOTHENIC_ACID_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "pantothenicAcid", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total phosphorus from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val PHOSPHORUS_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "phosphorus"
-            )
+        val PHOSPHORUS_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "phosphorus", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total polyunsaturated fat from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val POLYUNSATURATED_FAT_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "polyunsaturatedFat"
-            )
+        val POLYUNSATURATED_FAT_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "polyunsaturatedFat", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total potassium from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val POTASSIUM_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "potassium"
-            )
+        val POTASSIUM_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "potassium", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total protein from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val PROTEIN_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "protein"
-            )
+        val PROTEIN_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "protein", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total riboflavin from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val RIBOFLAVIN_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "riboflavin"
-            )
+        val RIBOFLAVIN_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "riboflavin", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total saturated fat from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SATURATED_FAT_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "saturatedFat"
-            )
+        val SATURATED_FAT_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "saturatedFat", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total selenium from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SELENIUM_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "selenium"
-            )
+        val SELENIUM_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "selenium", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total sodium from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SODIUM_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(TYPE_NAME, AggregateMetric.AggregationType.TOTAL, "sodium")
+        val SODIUM_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "sodium", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total sugar from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val SUGAR_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(TYPE_NAME, AggregateMetric.AggregationType.TOTAL, "sugar")
+        val SUGAR_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "sugar", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total thiamin from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val THIAMIN_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "thiamin"
-            )
+        val THIAMIN_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "thiamin", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total total carbohydrate from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val TOTAL_CARBOHYDRATE_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "totalCarbohydrate"
-            )
+        val TOTAL_CARBOHYDRATE_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "totalCarbohydrate", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total total fat from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val TOTAL_FAT_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "totalFat"
-            )
+        val TOTAL_FAT_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "totalFat", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total trans fat from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val TRANS_FAT_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "transFat"
-            )
+        val TRANS_FAT_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "transFat", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total unsaturated fat from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val UNSATURATED_FAT_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "unsaturatedFat"
-            )
+        val UNSATURATED_FAT_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "unsaturatedFat", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total vitamin a from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val VITAMIN_A_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "vitaminA"
-            )
+        val VITAMIN_A_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "vitaminA", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total vitamin b12 from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val VITAMIN_B12_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "vitaminB12"
-            )
+        val VITAMIN_B12_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "vitaminB12", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total vitamin b6 from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val VITAMIN_B6_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "vitaminB6"
-            )
+        val VITAMIN_B6_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "vitaminB6", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total vitamin c from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val VITAMIN_C_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "vitaminC"
-            )
+        val VITAMIN_C_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "vitaminC", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total vitamin d from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val VITAMIN_D_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "vitaminD"
-            )
+        val VITAMIN_D_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "vitaminD", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total vitamin e from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val VITAMIN_E_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "vitaminE"
-            )
+        val VITAMIN_E_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "vitaminE", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total vitamin k from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val VITAMIN_K_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(
-                TYPE_NAME,
-                AggregateMetric.AggregationType.TOTAL,
-                "vitaminK"
-            )
+        val VITAMIN_K_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "vitaminK", Mass::grams)
 
         /**
          * Metric identifier to retrieve the total zinc from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val ZINC_TOTAL: AggregateMetric<Double> =
-            AggregateMetric.doubleMetric(TYPE_NAME, AggregateMetric.AggregationType.TOTAL, "zinc")
+        val ZINC_TOTAL: AggregateMetric<Mass> =
+            doubleMetric(TYPE_NAME, AggregationType.TOTAL, "zinc", Mass::grams)
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/OvulationTestRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/OvulationTestRecord.kt
index 15c99bc..affb5ad 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/OvulationTestRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/OvulationTestRecord.kt
@@ -25,7 +25,9 @@
 public class OvulationTestRecord(
     /**
      * The result of a user's ovulation test, which shows if they're ovulating or not. Required
-     * field. Allowed values: [Results].
+     * field. Allowed values: [Result].
+     *
+     * @see Result
      */
     @property:Results public val result: String,
     override val time: Instant,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/PowerRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/PowerRecord.kt
index 39fc1c6..8af59e1 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/PowerRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/PowerRecord.kt
@@ -15,13 +15,13 @@
  */
 package androidx.health.connect.client.records
 
-import androidx.annotation.FloatRange
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.aggregate.AggregateMetric.AggregationType.AVERAGE
 import androidx.health.connect.client.aggregate.AggregateMetric.AggregationType.MAXIMUM
 import androidx.health.connect.client.aggregate.AggregateMetric.AggregationType.MINIMUM
 import androidx.health.connect.client.aggregate.AggregateMetric.Companion.doubleMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Power
 import java.time.Instant
 import java.time.ZoneOffset
 
@@ -34,9 +34,9 @@
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
-    override val samples: List<Power>,
+    override val samples: List<Sample>,
     override val metadata: Metadata = Metadata.EMPTY,
-) : SeriesRecord<Power> {
+) : SeriesRecord<PowerRecord.Sample> {
 
     /*
      * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
@@ -76,59 +76,80 @@
          * Metric identifier to retrieve average power from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
-        @JvmField val WATTS_AVG: AggregateMetric<Double> = doubleMetric(TYPE, AVERAGE, POWER_FIELD)
+        @JvmField
+        val POWER_AVG: AggregateMetric<Power> =
+            doubleMetric(
+                dataTypeName = TYPE,
+                aggregationType = AVERAGE,
+                fieldName = POWER_FIELD,
+                mapper = Power::watts,
+            )
 
         /**
          * Metric identifier to retrieve minimum power from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
-        @JvmField val WATTS_MIN: AggregateMetric<Double> = doubleMetric(TYPE, MINIMUM, POWER_FIELD)
+        @JvmField
+        val POWER_MIN: AggregateMetric<Power> =
+            doubleMetric(
+                dataTypeName = TYPE,
+                aggregationType = MINIMUM,
+                fieldName = POWER_FIELD,
+                mapper = Power::watts,
+            )
 
         /**
          * Metric identifier to retrieve maximum power from
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
-        @JvmField val WATTS_MAX: AggregateMetric<Double> = doubleMetric(TYPE, MAXIMUM, POWER_FIELD)
-    }
-}
-
-/**
- * Represents a single measurement of power. For example, using a power meter when exercising on a
- * stationary bike.
- *
- * @param time The point in time when the measurement was taken.
- * @param watts Power generated, in watts. Valid range: 0-100000.
- *
- * @see PowerRecord
- */
-public class Power(
-    val time: Instant,
-    @FloatRange(from = 0.0, to = 100_000.0) val watts: Double,
-) {
-
-    init {
-        requireNonNegative(value = watts, name = "watts")
+        @JvmField
+        val POWER_MAX: AggregateMetric<Power> =
+            doubleMetric(
+                dataTypeName = TYPE,
+                aggregationType = MAXIMUM,
+                fieldName = POWER_FIELD,
+                mapper = Power::watts,
+            )
     }
 
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+    /**
+     * Represents a single measurement of power. For example, using a power meter when exercising on
+     * a stationary bike.
+     *
+     * @param time The point in time when the measurement was taken.
+     * @param power Power generated, in [Power] unit. Valid range: 0-100000 Watts.
+     *
+     * @see PowerRecord
      */
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is Power) return false
+    public class Sample(
+        val time: Instant,
+        val power: Power,
+    ) {
 
-        if (time != other.time) return false
-        if (watts != other.watts) return false
+        init {
+            power.requireNotLess(other = power.zero(), name = "power")
+        }
 
-        return true
-    }
+        /*
+         * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+         */
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Sample) return false
 
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun hashCode(): Int {
-        var result = time.hashCode()
-        result = 31 * result + watts.hashCode()
-        return result
+            if (time != other.time) return false
+            if (power != other.power) return false
+
+            return true
+        }
+
+        /*
+         * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+         */
+        override fun hashCode(): Int {
+            var result = time.hashCode()
+            result = 31 * result + power.hashCode()
+            return result
+        }
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SexualActivityRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SexualActivityRecord.kt
index 3675085..f08d02c 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SexualActivityRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SexualActivityRecord.kt
@@ -29,6 +29,8 @@
     /**
      * Whether protection was used during sexual activity. Optional field, null if unknown. Allowed
      * values: [Protection].
+     *
+     * @see Protection
      */
     @property:Protections public val protectionUsed: String? = null,
     override val time: Instant,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt
index 327dcfb..0606c806 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt
@@ -23,7 +23,11 @@
 
 /** Captures the sleep stage the user entered during a sleep session. */
 public class SleepStageRecord(
-    /** Type of sleep stage. Required field. Allowed values: [StageType]. */
+    /**
+     * Type of sleep stage. Required field. Allowed values: [StageType].
+     *
+     * @see StageType
+     */
     @property:StageTypes public val stage: String,
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt
index 687e180..9af1358 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt
@@ -25,7 +25,11 @@
 public class SwimmingStrokesRecord(
     /** Count of strokes. Optional field. Valid range: 1-1000000. */
     public val count: Long = 0,
-    /** Swimming style. Required field. Allowed values: [SwimmingType]. */
+    /**
+     * Swimming style. Required field. Allowed values: [SwimmingType].
+     *
+     * @see SwimmingType
+     */
     @property:SwimmingTypes public val type: String,
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/TotalCaloriesBurnedRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/TotalCaloriesBurnedRecord.kt
index 090cc10..3c2c286 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/TotalCaloriesBurnedRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/TotalCaloriesBurnedRecord.kt
@@ -17,6 +17,7 @@
 
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Energy
 import java.time.Instant
 import java.time.ZoneOffset
 
@@ -25,22 +26,27 @@
  * Each record represents the total kilocalories burned over a time interval.
  */
 public class TotalCaloriesBurnedRecord(
-    /** Energy in kilocalories. Required field. Valid range: 0-1000000. */
-    public val energyKcal: Double,
+    /** Energy in [Energy] unit. Required field. Valid range: 0-1000000 kcal. */
+    public val energy: Energy,
     override val startTime: Instant,
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : IntervalRecord {
+
     init {
-        requireNonNegative(value = energyKcal, name = "energyKcal")
+        energy.requireNotLess(other = energy.zero(), "energy")
     }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is TotalCaloriesBurnedRecord) return false
 
-        if (energyKcal != other.energyKcal) return false
+        if (energy != other.energy) return false
         if (startTime != other.startTime) return false
         if (startZoneOffset != other.startZoneOffset) return false
         if (endTime != other.endTime) return false
@@ -50,9 +56,12 @@
         return true
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + energyKcal.hashCode()
+        var result = energy.hashCode()
+        result = 31 * result + startTime.hashCode()
         result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
         result = 31 * result + endTime.hashCode()
         result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
@@ -66,11 +75,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val CALORIES_TOTAL: AggregateMetric<Double> =
+        val ENERGY_TOTAL: AggregateMetric<Energy> =
             AggregateMetric.doubleMetric(
-                "TotalCaloriesBurned",
-                AggregateMetric.AggregationType.TOTAL,
-                "energy"
+                dataTypeName = "TotalCaloriesBurned",
+                aggregationType = AggregateMetric.AggregationType.TOTAL,
+                fieldName = "energy",
+                mapper = Energy::calories,
             )
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/Vo2MaxRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/Vo2MaxRecord.kt
index 487645e..094be6f 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/Vo2MaxRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/Vo2MaxRecord.kt
@@ -25,7 +25,11 @@
 public class Vo2MaxRecord(
     /** Maximal aerobic capacity (VO2 max) in milliliters. Required field. Valid range: 0-100. */
     public val vo2MillilitersPerMinuteKilogram: Double,
-    /** VO2 max measurement method. Optional field. Allowed values: [MeasurementMethods]. */
+    /**
+     * VO2 max measurement method. Optional field. Allowed values: [MeasurementMethod].
+     *
+     * @see MeasurementMethod
+     */
     @property:MeasurementMethods public val measurementMethod: String? = null,
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/WeightRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/WeightRecord.kt
index cab9e1a..a01d1d0 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/WeightRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/WeightRecord.kt
@@ -17,25 +17,28 @@
 
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Mass
 import java.time.Instant
 import java.time.ZoneOffset
 
 /** Captures that user's weight in kilograms. */
 public class WeightRecord(
-    /** User's weight in kilograms. Required field. Valid range: 0-1000. */
-    public val weightKg: Double,
+    /** User's weight in kilograms. Required field. Valid range: 0-1000 kilograms. */
+    public val weight: Mass,
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : InstantaneousRecord {
+
     init {
-        requireNonNegative(value = weightKg, name = "weightKg")
+        weight.requireNotLess(other = weight.zero(), name = "weight")
     }
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is WeightRecord) return false
 
-        if (weightKg != other.weightKg) return false
+        if (weight != other.weight) return false
         if (time != other.time) return false
         if (zoneOffset != other.zoneOffset) return false
         if (metadata != other.metadata) return false
@@ -43,15 +46,20 @@
         return true
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + weightKg.hashCode()
+        var result = weight.hashCode()
         result = 31 * result + time.hashCode()
         result = 31 * result + (zoneOffset?.hashCode() ?: 0)
         result = 31 * result + metadata.hashCode()
         return result
     }
 
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
     companion object {
         private const val WEIGHT_NAME = "Weight"
         private const val WEIGHT_FIELD = "weight"
@@ -61,11 +69,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val WEIGHT_AVG: AggregateMetric<Double> =
+        val WEIGHT_AVG: AggregateMetric<Mass> =
             AggregateMetric.doubleMetric(
-                WEIGHT_NAME,
-                AggregateMetric.AggregationType.AVERAGE,
-                WEIGHT_FIELD
+                dataTypeName = WEIGHT_NAME,
+                aggregationType = AggregateMetric.AggregationType.AVERAGE,
+                fieldName = WEIGHT_FIELD,
+                mapper = Mass::kilograms,
             )
 
         /**
@@ -73,11 +82,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val WEIGHT_MIN: AggregateMetric<Double> =
+        val WEIGHT_MIN: AggregateMetric<Mass> =
             AggregateMetric.doubleMetric(
-                WEIGHT_NAME,
-                AggregateMetric.AggregationType.MINIMUM,
-                WEIGHT_FIELD
+                dataTypeName = WEIGHT_NAME,
+                aggregationType = AggregateMetric.AggregationType.MINIMUM,
+                fieldName = WEIGHT_FIELD,
+                mapper = Mass::kilograms,
             )
 
         /**
@@ -85,11 +95,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val WEIGHT_MAX: AggregateMetric<Double> =
+        val WEIGHT_MAX: AggregateMetric<Mass> =
             AggregateMetric.doubleMetric(
-                WEIGHT_NAME,
-                AggregateMetric.AggregationType.MAXIMUM,
-                WEIGHT_FIELD
+                dataTypeName = WEIGHT_NAME,
+                aggregationType = AggregateMetric.AggregationType.MAXIMUM,
+                fieldName = WEIGHT_FIELD,
+                mapper = Mass::kilograms,
             )
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Energy.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Energy.kt
new file mode 100644
index 0000000..942982a
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Energy.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2022 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.units
+
+/**
+ * Represents a unit of energy. Supported units:
+ *
+ * - calories - see [Energy.calories], [Double.calories]
+ * - kilocalories - see [Energy.kilocalories], [Double.kilocalories]
+ * - joules - see [Energy.joules], [Double.joules]
+ * - kilojoules - see [Energy.kilojoules], [Double.kilojoules]
+ */
+class Energy private constructor(
+    private val value: Double,
+    private val type: Type,
+) : Comparable<Energy> {
+
+    /** Returns the energy in calories. */
+    @get:JvmName("getCalories")
+    val inCalories: Double
+        get() = value * type.caloriesPerUnit
+
+    /** Returns the energy in kilocalories. */
+    @get:JvmName("getKilocalories")
+    val inKilocalories: Double
+        get() = get(type = Type.KILOCALORIES)
+
+    /** Returns the energy in joules. */
+    @get:JvmName("getJoules")
+    val inJoules: Double
+        get() = get(type = Type.JOULES)
+
+    /** Returns the energy in kilojoules. */
+    @get:JvmName("getKilojoules")
+    val inKilojoules: Double
+        get() = get(type = Type.KILOJOULES)
+
+    private fun get(type: Type): Double =
+        if (this.type == type) value else inCalories / type.caloriesPerUnit
+
+    /** Returns zero [Energy] of the same [Type]. */
+    internal fun zero(): Energy = ZEROS.getValue(type)
+
+    override fun compareTo(other: Energy): Int =
+        if (type == other.type) {
+            value.compareTo(other.value)
+        } else {
+            inCalories.compareTo(other.inCalories)
+        }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Energy) return false
+
+        if (value != other.value) return false
+        if (type != other.type) return false
+
+        return true
+    }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun hashCode(): Int {
+        var result = value.hashCode()
+        result = 31 * result + type.hashCode()
+        return result
+    }
+
+    override fun toString(): String = "$value ${type.title}"
+
+    companion object {
+        private val ZEROS = Type.values().associateWith { Energy(value = 0.0, type = it) }
+
+        /** Creates [Energy] with the specified value in calories. */
+        @JvmStatic fun calories(value: Double): Energy = Energy(value, Type.CALORIES)
+
+        /** Creates [Energy] with the specified value in kilocalories. */
+        @JvmStatic fun kilocalories(value: Double): Energy = Energy(value, Type.KILOCALORIES)
+
+        /** Creates [Energy] with the specified value in joules. */
+        @JvmStatic fun joules(value: Double): Energy = Energy(value, Type.JOULES)
+
+        /** Creates [Energy] with the specified value in kilojoules. */
+        @JvmStatic fun kilojoules(value: Double): Energy = Energy(value, Type.KILOCALORIES)
+    }
+
+    private enum class Type {
+        CALORIES {
+            override val caloriesPerUnit: Double = 1.0
+            override val title: String = "cal"
+        },
+        KILOCALORIES {
+            override val caloriesPerUnit: Double = 1000.0
+            override val title: String = "kcal"
+        },
+        JOULES {
+            override val caloriesPerUnit: Double = 0.2390057361
+            override val title: String = "J"
+        },
+        KILOJOULES {
+            override val caloriesPerUnit: Double = 239.0057361
+            override val title: String = "kJ"
+        };
+
+        abstract val caloriesPerUnit: Double
+        abstract val title: String
+    }
+}
+
+/** Creates [Energy] with the specified value in calories. */
+@get:JvmSynthetic
+val Double.calories: Energy
+    get() = Energy.calories(value = this)
+
+/** Creates [Energy] with the specified value in calories. */
+@get:JvmSynthetic
+val Long.calories: Energy
+    get() = toDouble().calories
+
+/** Creates [Energy] with the specified value in calories. */
+@get:JvmSynthetic
+val Float.calories: Energy
+    get() = toDouble().calories
+
+/** Creates [Energy] with the specified value in calories. */
+@get:JvmSynthetic
+val Int.calories: Energy
+    get() = toDouble().calories
+
+/** Creates [Energy] with the specified value in kilocalories. */
+@get:JvmSynthetic
+val Double.kilocalories: Energy
+    get() = Energy.kilocalories(value = this)
+
+/** Creates [Energy] with the specified value in kilocalories. */
+@get:JvmSynthetic
+val Long.kilocalories: Energy
+    get() = toDouble().kilocalories
+
+/** Creates [Energy] with the specified value in kilocalories. */
+@get:JvmSynthetic
+val Float.kilocalories: Energy
+    get() = toDouble().kilocalories
+
+/** Creates [Energy] with the specified value in kilocalories. */
+@get:JvmSynthetic
+val Int.kilocalories: Energy
+    get() = toDouble().kilocalories
+
+/** Creates [Energy] with the specified value in joules. */
+@get:JvmSynthetic
+val Double.joules: Energy
+    get() = Energy.joules(value = this)
+
+/** Creates [Energy] with the specified value in joules. */
+@get:JvmSynthetic
+val Long.joules: Energy
+    get() = toDouble().joules
+
+/** Creates [Energy] with the specified value in joules. */
+@get:JvmSynthetic
+val Float.joules: Energy
+    get() = toDouble().joules
+
+/** Creates [Energy] with the specified value in joules. */
+@get:JvmSynthetic
+val Int.joules: Energy
+    get() = toDouble().joules
+
+/** Creates [Energy] with the specified value in kilojoules. */
+@get:JvmSynthetic
+val Double.kilojoules: Energy
+    get() = Energy.kilojoules(value = this)
+
+/** Creates [Energy] with the specified value in kilojoules. */
+@get:JvmSynthetic
+val Long.kilojoules: Energy
+    get() = toDouble().kilojoules
+
+/** Creates [Energy] with the specified value in kilojoules. */
+@get:JvmSynthetic
+val Float.kilojoules: Energy
+    get() = toDouble().kilojoules
+
+/** Creates [Energy] with the specified value in kilojoules. */
+@get:JvmSynthetic
+val Int.kilojoules: Energy
+    get() = toDouble().kilojoules
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Mass.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Mass.kt
new file mode 100644
index 0000000..eef031b
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Mass.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2022 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.units
+
+/**
+ * Represents a unit of mass. Supported units:
+ *
+ * - grams - see [Mass.grams], [Double.grams]
+ * - kilograms - see [Mass.kilograms], [Double.kilograms]
+ * - milligrams - see [Mass.milligrams], [Double.milligrams]
+ * - micrograms - see [Mass.micrograms], [Double.micrograms]
+ * - ounces - see [Mass.ounces], [Double.ounces]
+ */
+class Mass private constructor(
+    private val value: Double,
+    private val type: Type,
+) : Comparable<Mass> {
+
+    /** Returns the mass in grams. */
+    @get:JvmName("getGrams")
+    val inGrams: Double
+        get() = value * type.gramsPerUnit
+
+    /** Returns the mass in kilograms. */
+    @get:JvmName("getKilograms")
+    val inKilograms: Double
+        get() = get(type = Type.KILOGRAMS)
+
+    /** Returns the mass in milligrams. */
+    @get:JvmName("getMilligrams")
+    val inMilligrams: Double
+        get() = get(type = Type.MILLIGRAMS)
+
+    /** Returns the mass in micrograms. */
+    @get:JvmName("getMicrograms")
+    val inMicrograms: Double
+        get() = get(type = Type.MICROGRAMS)
+
+    /** Returns the mass in ounces. */
+    @get:JvmName("getOunces")
+    val inOunces: Double
+        get() = get(type = Type.OUNCES)
+
+    private fun get(type: Type): Double =
+        if (this.type == type) value else inGrams / type.gramsPerUnit
+
+    /** Returns zero [Mass] of the same [Type]. */
+    internal fun zero(): Mass = ZEROS.getValue(type)
+
+    override fun compareTo(other: Mass): Int =
+        if (type == other.type) {
+            value.compareTo(other.value)
+        } else {
+            inGrams.compareTo(other.inGrams)
+        }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Mass) return false
+
+        if (value != other.value) return false
+        if (type != other.type) return false
+
+        return true
+    }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun hashCode(): Int {
+        var result = value.hashCode()
+        result = 31 * result + type.hashCode()
+        return result
+    }
+
+    override fun toString(): String = "$value ${type.name.lowercase()}"
+
+    companion object {
+        private val ZEROS = Type.values().associateWith { Mass(value = 0.0, type = it) }
+
+        /** Creates [Mass] with the specified value in grams. */
+        @JvmStatic fun grams(value: Double): Mass = Mass(value, Type.GRAMS)
+
+        /** Creates [Mass] with the specified value in kilograms. */
+        @JvmStatic fun kilograms(value: Double): Mass = Mass(value, Type.KILOGRAMS)
+
+        /** Creates [Mass] with the specified value in milligrams. */
+        @JvmStatic fun milligrams(value: Double): Mass = Mass(value, Type.MILLIGRAMS)
+
+        /** Creates [Mass] with the specified value in micrograms. */
+        @JvmStatic fun micrograms(value: Double): Mass = Mass(value, Type.MICROGRAMS)
+
+        /** Creates [Mass] with the specified value in ounces. */
+        @JvmStatic fun ounces(value: Double): Mass = Mass(value, Type.OUNCES)
+    }
+
+    private enum class Type {
+        GRAMS {
+            override val gramsPerUnit: Double = 1.0
+        },
+        KILOGRAMS {
+            override val gramsPerUnit: Double = 1000.0
+        },
+        MILLIGRAMS {
+            override val gramsPerUnit: Double = 0.001
+        },
+        MICROGRAMS {
+            override val gramsPerUnit: Double = 0.000001
+        },
+        OUNCES {
+            override val gramsPerUnit: Double = 28.34952
+        };
+
+        abstract val gramsPerUnit: Double
+    }
+}
+
+/** Creates [Mass] with the specified value in grams. */
+@get:JvmSynthetic
+val Double.grams: Mass
+    get() = Mass.grams(value = this)
+
+/** Creates [Mass] with the specified value in grams. */
+@get:JvmSynthetic
+val Float.grams: Mass
+    get() = toDouble().grams
+
+/** Creates [Mass] with the specified value in grams. */
+@get:JvmSynthetic
+val Long.grams: Mass
+    get() = toDouble().grams
+
+/** Creates [Mass] with the specified value in grams. */
+@get:JvmSynthetic
+val Int.grams: Mass
+    get() = toDouble().grams
+
+/** Creates [Mass] with the specified value in kilograms. */
+@get:JvmSynthetic
+val Double.kilograms: Mass
+    get() = Mass.kilograms(value = this)
+
+/** Creates [Mass] with the specified value in kilograms. */
+@get:JvmSynthetic
+val Float.kilograms: Mass
+    get() = toDouble().kilograms
+
+/** Creates [Mass] with the specified value in kilograms. */
+@get:JvmSynthetic
+val Long.kilograms: Mass
+    get() = toDouble().kilograms
+
+/** Creates [Mass] with the specified value in kilograms. */
+@get:JvmSynthetic
+val Int.kilograms: Mass
+    get() = toDouble().kilograms
+
+/** Creates [Mass] with the specified value in milligrams. */
+@get:JvmSynthetic
+val Double.milligrams: Mass
+    get() = Mass.milligrams(value = this)
+
+/** Creates [Mass] with the specified value in milligrams. */
+@get:JvmSynthetic
+val Float.milligrams: Mass
+    get() = toDouble().milligrams
+
+/** Creates [Mass] with the specified value in milligrams. */
+@get:JvmSynthetic
+val Long.milligrams: Mass
+    get() = toDouble().milligrams
+
+/** Creates [Mass] with the specified value in milligrams. */
+@get:JvmSynthetic
+val Int.milligrams: Mass
+    get() = toDouble().milligrams
+
+/** Creates [Mass] with the specified value in micrograms. */
+@get:JvmSynthetic
+val Double.micrograms: Mass
+    get() = Mass.micrograms(value = this)
+
+/** Creates [Mass] with the specified value in micrograms. */
+@get:JvmSynthetic
+val Float.micrograms: Mass
+    get() = toDouble().micrograms
+
+/** Creates [Mass] with the specified value in micrograms. */
+@get:JvmSynthetic
+val Long.micrograms: Mass
+    get() = toDouble().micrograms
+
+/** Creates [Mass] with the specified value in micrograms. */
+@get:JvmSynthetic
+val Int.micrograms: Mass
+    get() = toDouble().micrograms
+
+/** Creates [Mass] with the specified value in ounces. */
+@get:JvmSynthetic
+val Double.ounces: Mass
+    get() = Mass.ounces(value = this)
+
+/** Creates [Mass] with the specified value in ounces. */
+@get:JvmSynthetic
+val Float.ounces: Mass
+    get() = toDouble().ounces
+
+/** Creates [Mass] with the specified value in ounces. */
+@get:JvmSynthetic
+val Long.ounces: Mass
+    get() = toDouble().ounces
+
+/** Creates [Mass] with the specified value in ounces. */
+@get:JvmSynthetic
+val Int.ounces: Mass
+    get() = toDouble().ounces
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Power.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Power.kt
new file mode 100644
index 0000000..41dfd33
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Power.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2022 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.units
+
+/**
+ * Represents a unit of power. Supported units:
+ *
+ * - watts - see [Power.watts], [Double.watts]
+ * - kilocalories/day - see [Power.kilocaloriesPerDay], [Double.kilocaloriesPerDay]
+ **/
+class Power private constructor(
+    private val value: Double,
+    private val type: Type,
+) : Comparable<Power> {
+
+    /** Returns the power in Watts. */
+    @get:JvmName("getWatts")
+    val inWatts: Double
+        get() = value * type.wattsPerUnit
+
+    /** Returns the power in kilocalories/day. */
+    @get:JvmName("getKilocaloriesPerDay")
+    val inKilocaloriesPerDay: Double
+        get() = get(type = Type.KILOCALORIES_PER_DAY)
+
+    private fun get(type: Type): Double =
+        if (this.type == type) value else inWatts / type.wattsPerUnit
+
+    /** Returns zero [Power] of the same [Type]. */
+    internal fun zero(): Power = ZEROS.getValue(type)
+
+    override fun compareTo(other: Power): Int =
+        if (type == other.type) {
+            value.compareTo(other.value)
+        } else {
+            inWatts.compareTo(other.inWatts)
+        }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Power) return false
+
+        if (value != other.value) return false
+        if (type != other.type) return false
+
+        return true
+    }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun hashCode(): Int {
+        var result = value.hashCode()
+        result = 31 * result + type.hashCode()
+        return result
+    }
+
+    override fun toString(): String = "$value ${type.title}"
+
+    companion object {
+        private val ZEROS = Type.values().associateWith { Power(value = 0.0, type = it) }
+
+        /** Creates [Power] with the specified value in Watts. */
+        @JvmStatic fun watts(value: Double): Power = Power(value, Type.WATTS)
+
+        /** Creates [Power] with the specified value in kilocalories/day. */
+        @JvmStatic
+        fun kilocaloriesPerDay(value: Double): Power = Power(value, Type.KILOCALORIES_PER_DAY)
+    }
+
+    private enum class Type {
+        WATTS {
+            override val wattsPerUnit: Double = 1.0
+            override val title: String = "Watts"
+        },
+        KILOCALORIES_PER_DAY {
+            override val wattsPerUnit: Double = 0.0484259259
+            override val title: String = "kcal/day"
+        };
+
+        abstract val wattsPerUnit: Double
+        abstract val title: String
+    }
+}
+
+/** Creates [Power] with the specified value in Watts. */
+@get:JvmSynthetic
+val Double.watts: Power
+    get() = Power.watts(value = this)
+
+/** Creates [Power] with the specified value in Watts. */
+@get:JvmSynthetic
+val Long.watts: Power
+    get() = toDouble().watts
+
+/** Creates [Power] with the specified value in Watts. */
+@get:JvmSynthetic
+val Float.watts: Power
+    get() = toDouble().watts
+
+/** Creates [Power] with the specified value in Watts. */
+@get:JvmSynthetic
+val Int.watts: Power
+    get() = toDouble().watts
+
+/** Creates [Power] with the specified value in kilocalories/day. */
+@get:JvmSynthetic
+val Double.kilocaloriesPerDay: Power
+    get() = Power.kilocaloriesPerDay(value = this)
+
+/** Creates [Power] with the specified value in kilocalories/day. */
+@get:JvmSynthetic
+val Long.kilocaloriesPerDay: Power
+    get() = toDouble().kilocaloriesPerDay
+
+/** Creates [Power] with the specified value in kilocalories/day. */
+@get:JvmSynthetic
+val Float.kilocaloriesPerDay: Power
+    get() = toDouble().kilocaloriesPerDay
+
+/** Creates [Power] with the specified value in kilocalories/day. */
+@get:JvmSynthetic
+val Int.kilocaloriesPerDay: Power
+    get() = toDouble().kilocaloriesPerDay
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Pressure.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Pressure.kt
new file mode 100644
index 0000000..c0091f5
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Pressure.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2022 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.units
+
+/**
+ * Represents a unit of pressure. Supported units:
+ *
+ * - millimeters of Mercury (mmHg) - see [Pressure.millimetersOfMercury],
+ * [Double.millimetersOfMercury].
+ */
+class Pressure private constructor(
+    private val value: Double,
+) : Comparable<Pressure> {
+
+    /** Returns the pressure in millimeters of Mercury (mmHg). */
+    @get:JvmName("getMillimetersOfMercury")
+    val inMillimetersOfMercury: Double
+        get() = value
+
+    /** Returns zero [Pressure] of the same type (currently there is only one type - mmHg). */
+    internal fun zero(): Pressure = ZERO
+
+    override fun compareTo(other: Pressure): Int = value.compareTo(other.value)
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Pressure) return false
+
+        if (value != other.value) return false
+
+        return true
+    }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun hashCode(): Int {
+        return value.hashCode()
+    }
+
+    override fun toString(): String = "$value mmHg"
+
+    companion object {
+        private val ZERO = Pressure(value = 0.0)
+
+        /** Creates [Pressure] with the specified value in millimeters of Mercury (mmHg). */
+        @JvmStatic fun millimetersOfMercury(value: Double): Pressure = Pressure(value)
+    }
+}
+
+/** Creates [Pressure] with the specified value in millimeters of Mercury (mmHg). */
+@get:JvmSynthetic
+val Double.millimetersOfMercury: Pressure
+    get() = Pressure.millimetersOfMercury(value = this)
+
+/** Creates [Pressure] with the specified value in millimeters of Mercury (mmHg). */
+@get:JvmSynthetic
+val Long.millimetersOfMercury: Pressure
+    get() = toDouble().millimetersOfMercury
+
+/** Creates [Pressure] with the specified value in millimeters of Mercury (mmHg). */
+@get:JvmSynthetic
+val Float.millimetersOfMercury: Pressure
+    get() = toDouble().millimetersOfMercury
+
+/** Creates [Pressure] with the specified value in millimeters of Mercury (mmHg). */
+@get:JvmSynthetic
+val Int.millimetersOfMercury: Pressure
+    get() = toDouble().millimetersOfMercury
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Volume.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Volume.kt
new file mode 100644
index 0000000..fff04b1
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/units/Volume.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2022 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.units
+
+/**
+ * Represents a unit of volume. Supported units:
+ *
+ * - liters - see [Volume.liters], [Double.liters]
+ * - milliliters - see [Volume.milliliters], [Double.milliliters]
+ */
+class Volume private constructor(
+    private val value: Double,
+    private val type: Type,
+) : Comparable<Volume> {
+
+    /** Returns the volume in liters. */
+    @get:JvmName("getLiters")
+    val inLiters: Double
+        get() = value * type.litersPerUnit
+
+    /** Returns the volume in milliliters. */
+    @get:JvmName("getMilliliters")
+    val inMilliliters: Double
+        get() = get(type = Type.MILLILITERS)
+
+    private fun get(type: Type): Double =
+        if (this.type == type) value else inLiters / type.litersPerUnit
+
+    /** Returns zero [Volume] of the same [Type]. */
+    internal fun zero(): Volume = ZEROS.getValue(type)
+
+    override fun compareTo(other: Volume): Int =
+        if (type == other.type) {
+            value.compareTo(other.value)
+        } else {
+            inLiters.compareTo(other.inLiters)
+        }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Volume) return false
+
+        if (value != other.value) return false
+        if (type != other.type) return false
+
+        return true
+    }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun hashCode(): Int {
+        var result = value.hashCode()
+        result = 31 * result + type.hashCode()
+        return result
+    }
+
+    override fun toString(): String = "$value ${type.title}"
+
+    companion object {
+        private val ZEROS = Type.values().associateWith { Volume(value = 0.0, type = it) }
+
+        /** Creates [Volume] with the specified value in liters. */
+        @JvmStatic fun liters(value: Double): Volume = Volume(value, Type.LITERS)
+
+        /** Creates [Volume] with the specified value in milliliters. */
+        @JvmStatic fun milliliters(value: Double): Volume = Volume(value, Type.MILLILITERS)
+    }
+
+    private enum class Type {
+        LITERS {
+            override val litersPerUnit: Double = 1.0
+            override val title: String = "L"
+        },
+        MILLILITERS {
+            override val litersPerUnit: Double = 0.001
+            override val title: String = "mL"
+        };
+
+        abstract val litersPerUnit: Double
+        abstract val title: String
+    }
+}
+
+/** Creates [Volume] with the specified value in liters. */
+@get:JvmSynthetic
+val Double.liters: Volume
+    get() = Volume.liters(value = this)
+
+/** Creates [Volume] with the specified value in liters. */
+@get:JvmSynthetic
+val Long.liters: Volume
+    get() = toDouble().liters
+
+/** Creates [Volume] with the specified value in liters. */
+@get:JvmSynthetic
+val Float.liters: Volume
+    get() = toDouble().liters
+
+/** Creates [Volume] with the specified value in liters. */
+@get:JvmSynthetic
+val Int.liters: Volume
+    get() = toDouble().liters
+
+/** Creates [Volume] with the specified value in milliliters. */
+@get:JvmSynthetic
+val Double.milliliters: Volume
+    get() = Volume.milliliters(value = this)
+
+/** Creates [Volume] with the specified value in milliliters. */
+@get:JvmSynthetic
+val Long.milliliters: Volume
+    get() = toDouble().milliliters
+
+/** Creates [Volume] with the specified value in milliliters. */
+@get:JvmSynthetic
+val Float.milliliters: Volume
+    get() = toDouble().milliliters
+
+/** Creates [Volume] with the specified value in milliliters. */
+@get:JvmSynthetic
+val Int.milliliters: Volume
+    get() = toDouble().milliliters
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
index 358ed62..37fa612 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
@@ -44,6 +44,8 @@
 import androidx.health.connect.client.response.ReadRecordResponse
 import androidx.health.connect.client.response.ReadRecordsResponse
 import androidx.health.connect.client.time.TimeRangeFilter
+import androidx.health.connect.client.units.grams
+import androidx.health.connect.client.units.kilograms
 import androidx.health.platform.client.impl.ServiceBackedHealthDataClient
 import androidx.health.platform.client.impl.error.errorCodeExceptionMap
 import androidx.health.platform.client.impl.ipc.ClientConfiguration
@@ -257,7 +259,7 @@
             healthConnectClient.insertRecords(
                 listOf(
                     WeightRecord(
-                        weightKg = 45.8,
+                        weight = 45.8.kilograms,
                         time = Instant.ofEpochMilli(1234L),
                         zoneOffset = null,
                     )
@@ -287,8 +289,8 @@
             healthConnectClient.insertRecords(
                 listOf(
                     NutritionRecord(
-                        vitaminEGrams = 10.0,
-                        vitaminCGrams = 20.0,
+                        vitaminE = 10.grams,
+                        vitaminC = 20.grams,
                         startTime = Instant.ofEpochMilli(1234L),
                         startZoneOffset = null,
                         endTime = Instant.ofEpochMilli(5678L),
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index a7a6e1a..ea11ca7 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -61,7 +61,6 @@
 import androidx.health.connect.client.records.OvulationTestRecord
 import androidx.health.connect.client.records.OvulationTestRecord.Result
 import androidx.health.connect.client.records.OxygenSaturationRecord
-import androidx.health.connect.client.records.Power
 import androidx.health.connect.client.records.PowerRecord
 import androidx.health.connect.client.records.Record
 import androidx.health.connect.client.records.RespiratoryRateRecord
@@ -86,7 +85,14 @@
 import androidx.health.connect.client.records.metadata.Device
 import androidx.health.connect.client.records.metadata.Metadata
 import androidx.health.connect.client.units.celsius
+import androidx.health.connect.client.units.grams
+import androidx.health.connect.client.units.kilocalories
+import androidx.health.connect.client.units.kilocaloriesPerDay
+import androidx.health.connect.client.units.kilograms
+import androidx.health.connect.client.units.liters
 import androidx.health.connect.client.units.meters
+import androidx.health.connect.client.units.millimetersOfMercury
+import androidx.health.connect.client.units.watts
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
 import java.time.Instant
@@ -144,7 +150,7 @@
     fun testBasalMetabolicRate() {
         val data =
             BasalMetabolicRateRecord(
-                kcalPerDay = 1.0,
+                basalMetabolicRate = 1.kilocaloriesPerDay,
                 time = START_TIME,
                 zoneOffset = END_ZONE_OFFSET,
                 metadata = TEST_METADATA
@@ -175,8 +181,8 @@
     fun testBloodPressure() {
         val data =
             BloodPressureRecord(
-                systolicMillimetersOfMercury = 20.0,
-                diastolicMillimetersOfMercury = 10.0,
+                systolic = 20.millimetersOfMercury,
+                diastolic = 10.millimetersOfMercury,
                 bodyPosition = null,
                 measurementLocation = null,
                 time = START_TIME,
@@ -221,7 +227,7 @@
     fun testBodyWaterMass() {
         val data =
             BodyWaterMassRecord(
-                massKg = 1.0,
+                mass = 1.kilograms,
                 time = START_TIME,
                 zoneOffset = END_ZONE_OFFSET,
                 metadata = TEST_METADATA
@@ -235,7 +241,7 @@
     fun testBoneMass() {
         val data =
             BoneMassRecord(
-                massKg = 1.0,
+                mass = 1.kilograms,
                 time = START_TIME,
                 zoneOffset = END_ZONE_OFFSET,
                 metadata = TEST_METADATA
@@ -474,7 +480,7 @@
     fun testLeanBodyMass() {
         val data =
             LeanBodyMassRecord(
-                massKg = 1.0,
+                mass = 1.kilograms,
                 time = START_TIME,
                 zoneOffset = END_ZONE_OFFSET,
                 metadata = TEST_METADATA
@@ -536,13 +542,13 @@
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
                     listOf(
-                        Power(
+                        PowerRecord.Sample(
                             time = START_TIME,
-                            watts = 1.0,
+                            power = 1.watts,
                         ),
-                        Power(
+                        PowerRecord.Sample(
                             time = START_TIME,
-                            watts = 2.0,
+                            power = 2.watts,
                         ),
                     ),
                 metadata = TEST_METADATA,
@@ -679,7 +685,7 @@
     fun testWeight() {
         val data =
             WeightRecord(
-                weightKg = 1.0,
+                weight = 1.kilograms,
                 time = START_TIME,
                 zoneOffset = END_ZONE_OFFSET,
                 metadata = TEST_METADATA
@@ -693,7 +699,7 @@
     fun testActiveCaloriesBurned() {
         val data =
             ActiveCaloriesBurnedRecord(
-                energyKcal = 1.0,
+                energy = 1.kilocalories,
                 startTime = START_TIME,
                 startZoneOffset = START_ZONE_OFFSET,
                 endTime = END_TIME,
@@ -807,7 +813,7 @@
     fun testHydration() {
         val data =
             HydrationRecord(
-                volumeLiters = 1.0,
+                volume = 1.liters,
                 startTime = START_TIME,
                 startZoneOffset = START_ZONE_OFFSET,
                 endTime = END_TIME,
@@ -823,48 +829,48 @@
     fun testNutrition() {
         val data =
             NutritionRecord(
-                biotinGrams = 1.0,
-                caffeineGrams = 1.0,
-                calciumGrams = 1.0,
-                kcal = 1.0,
-                kcalFromFat = 1.0,
-                chlorideGrams = 1.0,
-                cholesterolGrams = 1.0,
-                chromiumGrams = 1.0,
-                copperGrams = 1.0,
-                dietaryFiberGrams = 1.0,
-                folateGrams = 1.0,
-                folicAcidGrams = 1.0,
-                iodineGrams = 1.0,
-                ironGrams = 1.0,
-                magnesiumGrams = 1.0,
-                manganeseGrams = 1.0,
-                molybdenumGrams = 1.0,
-                monounsaturatedFatGrams = 1.0,
-                niacinGrams = 1.0,
-                pantothenicAcidGrams = 1.0,
-                phosphorusGrams = 1.0,
-                polyunsaturatedFatGrams = 1.0,
-                potassiumGrams = 1.0,
-                proteinGrams = 1.0,
-                riboflavinGrams = 1.0,
-                saturatedFatGrams = 1.0,
-                seleniumGrams = 1.0,
-                sodiumGrams = 1.0,
-                sugarGrams = 1.0,
-                thiaminGrams = 1.0,
-                totalCarbohydrateGrams = 1.0,
-                totalFatGrams = 1.0,
-                transFatGrams = 1.0,
-                unsaturatedFatGrams = 1.0,
-                vitaminAGrams = 1.0,
-                vitaminB12Grams = 1.0,
-                vitaminB6Grams = 1.0,
-                vitaminCGrams = 1.0,
-                vitaminDGrams = 1.0,
-                vitaminEGrams = 1.0,
-                vitaminKGrams = 1.0,
-                zincGrams = 1.0,
+                biotin = 1.grams,
+                caffeine = 1.grams,
+                calcium = 1.grams,
+                energy = 1.kilocalories,
+                energyFromFat = 1.kilocalories,
+                chloride = 1.grams,
+                cholesterol = 1.grams,
+                chromium = 1.grams,
+                copper = 1.grams,
+                dietaryFiber = 1.grams,
+                folate = 1.grams,
+                folicAcid = 1.grams,
+                iodine = 1.grams,
+                iron = 1.grams,
+                magnesium = 1.grams,
+                manganese = 1.grams,
+                molybdenum = 1.grams,
+                monounsaturatedFat = 1.grams,
+                niacin = 1.grams,
+                pantothenicAcid = 1.grams,
+                phosphorus = 1.grams,
+                polyunsaturatedFat = 1.grams,
+                potassium = 1.grams,
+                protein = 1.grams,
+                riboflavin = 1.grams,
+                saturatedFat = 1.grams,
+                selenium = 1.grams,
+                sodium = 1.grams,
+                sugar = 1.grams,
+                thiamin = 1.grams,
+                totalCarbohydrate = 1.grams,
+                totalFat = 1.grams,
+                transFat = 1.grams,
+                unsaturatedFat = 1.grams,
+                vitaminA = 1.grams,
+                vitaminB12 = 1.grams,
+                vitaminB6 = 1.grams,
+                vitaminC = 1.grams,
+                vitaminD = 1.grams,
+                vitaminE = 1.grams,
+                vitaminK = 1.grams,
+                zinc = 1.grams,
                 mealType = null,
                 name = null,
                 startTime = START_TIME,
@@ -965,7 +971,7 @@
     fun testTotalCaloriesBurned() {
         val data =
             TotalCaloriesBurnedRecord(
-                energyKcal = 1.0,
+                energy = 1.kilocalories,
                 startTime = START_TIME,
                 startZoneOffset = START_ZONE_OFFSET,
                 endTime = END_TIME,
diff --git a/libraryversions.toml b/libraryversions.toml
index ac210a3..44bc815 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -111,6 +111,7 @@
 TRACING = "1.2.0-alpha01"
 TRACING_PERFETTO = "1.0.0-alpha01"
 TRANSITION = "1.5.0-alpha01"
+TV = "1.0.0-alpha01"
 TVPROVIDER = "1.1.0-alpha02"
 VECTORDRAWABLE = "1.2.0-beta02"
 VECTORDRAWABLE_ANIMATED = "1.2.0-beta01"
@@ -220,6 +221,7 @@
 TEXT = { group = "androidx.text", atomicGroupVersion = "versions.TEXT" }
 TRACING = { group = "androidx.tracing", atomicGroupVersion = "versions.TRACING" }
 TRANSITION = { group = "androidx.transition", atomicGroupVersion = "versions.TRANSITION" }
+TV = { group = "androidx.tv", atomicGroupVersion = "versions.TV" }
 TVPROVIDER = { group = "androidx.tvprovider", atomicGroupVersion = "versions.TVPROVIDER" }
 VECTORDRAWABLE = { group = "androidx.vectordrawable" }
 VERSIONEDPARCELABLE = { group = "androidx.versionedparcelable" }
diff --git a/lifecycle/lifecycle-common/api/restricted_current.txt b/lifecycle/lifecycle-common/api/restricted_current.txt
index c09d138..19ccca0 100644
--- a/lifecycle/lifecycle-common/api/restricted_current.txt
+++ b/lifecycle/lifecycle-common/api/restricted_current.txt
@@ -11,7 +11,7 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface GeneratedAdapter {
-    method public void callMethods(androidx.lifecycle.LifecycleOwner!, androidx.lifecycle.Lifecycle.Event!, boolean, androidx.lifecycle.MethodCallsLogger!);
+    method public void callMethods(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.Event, boolean, androidx.lifecycle.MethodCallsLogger?);
   }
 
   @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface GenericLifecycleObserver extends androidx.lifecycle.LifecycleEventObserver {
@@ -60,7 +60,7 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Lifecycling {
-    method public static String! getAdapterName(String!);
+    method public static String getAdapterName(String);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MethodCallsLogger {
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ClassesInfoCache.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ClassesInfoCache.java
index 2578230..3c47fc4 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ClassesInfoCache.java
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ClassesInfoCache.java
@@ -214,8 +214,8 @@
             mMethod.setAccessible(true);
         }
 
+        @SuppressWarnings("BanUncheckedReflection")
         void invokeCallback(LifecycleOwner source, Lifecycle.Event event, Object target) {
-            //noinspection TryWithIdenticalCatches
             try {
                 switch (mCallType) {
                     case CALL_TYPE_NO_ARG:
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GeneratedAdapter.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GeneratedAdapter.java
index 7d5dede2..a09f980 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GeneratedAdapter.java
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GeneratedAdapter.java
@@ -16,6 +16,8 @@
 
 package androidx.lifecycle;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
 /**
@@ -33,6 +35,7 @@
      * @param logger if passed, used to track called methods and prevent calling the same method
      *              twice
      */
-    void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny,
-            MethodCallsLogger logger);
+    @SuppressWarnings("LambdaLast")
+    void callMethods(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event, boolean onAny,
+            @Nullable MethodCallsLogger logger);
 }
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleEventObserver.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleEventObserver.java
index a32fa41f..79a8908 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleEventObserver.java
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleEventObserver.java
@@ -29,6 +29,7 @@
  * If a class implements this interface and in the same time uses {@link OnLifecycleEvent}, then
  * annotations will be ignored.
  */
+@SuppressWarnings("LambdaLast")
 public interface LifecycleEventObserver extends LifecycleObserver {
     /**
      * Called when a state transition event happens.
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java
index bec241a..2714780 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java
@@ -205,7 +205,8 @@
     /**
      * Create a name for an adapter class.
      */
-    public static String getAdapterName(String className) {
+    @NonNull
+    public static String getAdapterName(@NonNull String className) {
         return className.replace(".", "_") + "_LifecycleAdapter";
     }
 
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.java b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.java
index 155825c..d3eacca 100644
--- a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.java
+++ b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.java
@@ -16,6 +16,7 @@
 
 package androidx.lifecycle.observers;
 
+import androidx.annotation.NonNull;
 import androidx.lifecycle.GeneratedAdapter;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleOwner;
@@ -27,8 +28,8 @@
     }
 
     @Override
-    public void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny,
-            MethodCallsLogger logger) {
+    public void callMethods(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event,
+            boolean onAny, MethodCallsLogger logger) {
 
     }
 }
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.java b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.java
index 5dc2fd2..4f5b51a 100644
--- a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.java
+++ b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.java
@@ -16,6 +16,7 @@
 
 package androidx.lifecycle.observers;
 
+import androidx.annotation.NonNull;
 import androidx.lifecycle.GeneratedAdapter;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleOwner;
@@ -27,8 +28,8 @@
     }
 
     @Override
-    public void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny,
-            MethodCallsLogger logger) {
+    public void callMethods(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event,
+            boolean onAny, MethodCallsLogger logger) {
 
     }
 }
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.java b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.java
index 9c02c43..381d104 100644
--- a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.java
+++ b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.java
@@ -16,6 +16,7 @@
 
 package androidx.lifecycle.observers;
 
+import androidx.annotation.NonNull;
 import androidx.lifecycle.GeneratedAdapter;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleOwner;
@@ -27,8 +28,8 @@
     }
 
     @Override
-    public void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny,
-            MethodCallsLogger logger) {
+    public void callMethods(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event,
+            boolean onAny, MethodCallsLogger logger) {
 
     }
 }
diff --git a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
index eaaa9d7..017d27d 100644
--- a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
+++ b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
@@ -16,6 +16,8 @@
 
 package androidx.lifecycle;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.arch.core.executor.ArchTaskExecutor;
@@ -51,6 +53,7 @@
      * other items emitted during the time there was no backpressure requested will be dropped.
      */
     @NonNull
+    @SuppressLint("LambdaLast")
     public static <T> Publisher<T> toPublisher(
             @NonNull LifecycleOwner lifecycle, @NonNull LiveData<T> liveData) {
 
diff --git a/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsJava.java b/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsJava.java
index 4f32ad8..2e1f80d 100644
--- a/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsJava.java
+++ b/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsJava.java
@@ -21,8 +21,12 @@
 
 /**
  * Sample class for testing JetBrains nullability annotations.
+ *
+ * We are purposely using the non-AndroidX versions of @NotNull and @Nullable in this file for
+ * testing the annotations lint check.  @SuppressWarnings is used here to bypass the lint check
+ * for this test file.
  */
-@SuppressWarnings("unused")
+@SuppressWarnings({"NullabilityAnnotationsDetector", "unused"})
 public class NullabilityAnnotationsJava {
     /**
      * Sample method
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCallbackWithMediaControllerCompatTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCallbackWithMediaControllerCompatTest.java
index d388a0b..e8024ed6 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCallbackWithMediaControllerCompatTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCallbackWithMediaControllerCompatTest.java
@@ -25,6 +25,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.PendingIntent;
 import android.content.Context;
@@ -149,6 +150,9 @@
 
     @Test
     public void disconnectedAfterTimeout() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         CountDownLatch disconnectedLatch = new CountDownLatch(1);
         RemoteMediaControllerCompat controller = null;
         try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
@@ -431,6 +435,9 @@
 
     @Test
     public void setRepeatMode() throws InterruptedException {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         final int testRepeatMode = SessionPlayer.REPEAT_MODE_GROUP;
         mController.getTransportControls().setRepeatMode(testRepeatMode);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
@@ -458,6 +465,9 @@
 
     @Test
     public void adjustVolume() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         final int maxVolume = 100;
         final int currentVolume = 23;
         final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
@@ -475,6 +485,9 @@
 
     @Test
     public void setVolumeWithLocalVolume() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
             // This test is not eligible for this device.
             return;
@@ -521,6 +534,9 @@
 
     @Test
     public void adjustVolumeWithLocalVolume() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
             // This test is not eligible for this device.
             return;
@@ -567,6 +583,9 @@
 
     @Test
     public void sendCommand() throws InterruptedException {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         // TODO(jaewan): Need to revisit with the permission.
         final String testCommand = "test_command";
         final Bundle testArgs = new Bundle();
@@ -606,6 +625,9 @@
 
     @Test
     public void controllerCallback_sessionRejects() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         final SessionCallback sessionCallback = new SessionCallback() {
             @Override
             public SessionCommandGroup onConnect(@NonNull MediaSession session,
@@ -703,6 +725,9 @@
 
     @Test
     public void playFromMediaUri() throws InterruptedException {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         final Uri request = Uri.parse("foo://bar");
         final Bundle bundle = new Bundle();
         bundle.putString("key", "value");
@@ -762,6 +787,9 @@
 
     @Test
     public void playFromMediaId() throws InterruptedException {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         final String mediaId = "media_id";
         final Bundle bundle = new Bundle();
         bundle.putString("key", "value");
@@ -852,6 +880,9 @@
 
     @Test
     public void setRating() throws InterruptedException {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         final int ratingType = RatingCompat.RATING_5_STARS;
         final float ratingValue = 3.5f;
         final RatingCompat rating = RatingCompat.newStarRating(ratingType, ratingValue);
@@ -884,6 +915,9 @@
 
     @Test
     public void onCommandCallback() throws InterruptedException {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         final ArrayList<SessionCommand> commands = new ArrayList<>();
         final CountDownLatch latchForPause = new CountDownLatch(1);
         final SessionCallback callback = new SessionCallback() {
@@ -1015,6 +1049,9 @@
     @Test
     @LargeTest
     public void controllerAfterSessionIsGone() throws InterruptedException {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         mSession.close();
         testSessionCallbackIsNotCalled();
 
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCompatCallbackWithMediaControllerTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCompatCallbackWithMediaControllerTest.java
index c89f5dd..42c6110 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCompatCallbackWithMediaControllerTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCompatCallbackWithMediaControllerTest.java
@@ -30,6 +30,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.PendingIntent;
 import android.content.Context;
@@ -95,6 +96,9 @@
     @Before
     @Override
     public void setUp() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         super.setUp();
         final Intent sessionActivity = new Intent(mContext, MockActivity.class);
         // Create this test specific MediaSession to use our own Handler.
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
index 50f0dcc..dc52ecb 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
@@ -23,8 +23,10 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import android.content.ComponentName;
+import android.os.Build;
 import android.os.Bundle;
 
 import androidx.annotation.NonNull;
@@ -60,6 +62,9 @@
     @Override
     @Before
     public void setUp() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         super.setUp();
         TestServiceRegistry.getInstance().cleanUp();
         TestServiceRegistry.getInstance().setHandler(sHandler);
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTest.java
index 5532762..7508881 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.os.Build;
 import android.os.Bundle;
@@ -67,6 +68,9 @@
     @Before
     @Override
     public void setUp() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         super.setUp();
         mPlayer = new MockPlayer(1);
 
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteMediaBrowserCompatTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteMediaBrowserCompatTest.java
index e5ae867..b49c342 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteMediaBrowserCompatTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteMediaBrowserCompatTest.java
@@ -19,8 +19,10 @@
 import static androidx.media2.test.common.CommonConstants.MOCK_MEDIA2_LIBRARY_SERVICE;
 
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
+import android.os.Build;
 
 import androidx.media2.test.service.RemoteMediaBrowserCompat;
 import androidx.test.core.app.ApplicationProvider;
@@ -40,6 +42,9 @@
 
     @Before
     public void setUp() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         mContext = ApplicationProvider.getApplicationContext();
         mRemoteBrowserCompat = new RemoteMediaBrowserCompat(mContext, MOCK_MEDIA2_LIBRARY_SERVICE);
     }
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteMediaControllerCompatTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteMediaControllerCompatTest.java
index 54febef..e527013 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteMediaControllerCompatTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteMediaControllerCompatTest.java
@@ -19,8 +19,10 @@
 import static androidx.media2.test.common.CommonConstants.DEFAULT_TEST_NAME;
 
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
+import android.os.Build;
 import android.support.v4.media.session.MediaSessionCompat;
 
 import androidx.media2.test.service.RemoteMediaControllerCompat;
@@ -45,6 +47,9 @@
 
     @Before
     public void setUp() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         mContext = ApplicationProvider.getApplicationContext();
         sHandler.postAndSync(new Runnable() {
             @Override
@@ -59,8 +64,12 @@
 
     @After
     public void cleanUp() {
-        mSessionCompat.release();
-        mRemoteControllerCompat.cleanUp();
+        if (mSessionCompat != null) {
+            mSessionCompat.release();
+        }
+        if (mRemoteControllerCompat != null) {
+            mRemoteControllerCompat.cleanUp();
+        }
     }
 
     @Test
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteSessionPlayerTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteSessionPlayerTest.java
index 3ed9ddc..31ba3c5 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteSessionPlayerTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/RemoteSessionPlayerTest.java
@@ -20,8 +20,10 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import android.media.AudioManager;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.media2.session.MediaSession;
@@ -54,6 +56,9 @@
     @Before
     @Override
     public void setUp() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         super.setUp();
         // Create this test specific MediaSession to use our own Handler.
         mSession = new MediaSession.Builder(mContext, new MockPlayer(1))
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/SessionPlayerTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/SessionPlayerTest.java
index 8639fe1..eb609a8 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/SessionPlayerTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/SessionPlayerTest.java
@@ -24,6 +24,9 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.media2.common.MediaItem;
@@ -62,6 +65,9 @@
     @Before
     @Override
     public void setUp() throws Exception {
+        // b/204596299
+        assumeTrue(Build.VERSION.SDK_INT != 17);
+
         super.setUp();
         mPlayer = new MockPlayer(1);
         mSession = new MediaSession.Builder(mContext, mPlayer)
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteSelector.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteSelector.java
index 79d5364..bc78b74 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteSelector.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteSelector.java
@@ -35,7 +35,7 @@
  *
  * <h3>Example</h3>
  * <pre>
- * MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
+ * MediaRouteSelector selector = new MediaRouteSelector.Builder()
  *         .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
  *         .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
  *         .build();
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index fe9681f..abab5da 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -395,6 +395,7 @@
     method public String getName();
     method public boolean isNullableAllowed();
     method public abstract T! parseValue(String value);
+    method public T! parseValue(String value, T? previousValue);
     method public abstract void put(android.os.Bundle bundle, String key, T? value);
     property public boolean isNullableAllowed;
     property public String name;
diff --git a/navigation/navigation-common/api/public_plus_experimental_current.txt b/navigation/navigation-common/api/public_plus_experimental_current.txt
index fe9681f..abab5da 100644
--- a/navigation/navigation-common/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-common/api/public_plus_experimental_current.txt
@@ -395,6 +395,7 @@
     method public String getName();
     method public boolean isNullableAllowed();
     method public abstract T! parseValue(String value);
+    method public T! parseValue(String value, T? previousValue);
     method public abstract void put(android.os.Bundle bundle, String key, T? value);
     property public boolean isNullableAllowed;
     property public String name;
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index fe9681f..abab5da 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -395,6 +395,7 @@
     method public String getName();
     method public boolean isNullableAllowed();
     method public abstract T! parseValue(String value);
+    method public T! parseValue(String value, T? previousValue);
     method public abstract void put(android.os.Bundle bundle, String key, T? value);
     property public boolean isNullableAllowed;
     property public String name;
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
index 6bf9f3f..f7a85be 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
@@ -20,6 +20,7 @@
 import androidx.navigation.test.intArgument
 import androidx.navigation.test.nullableStringArgument
 import androidx.navigation.test.stringArgument
+import androidx.navigation.test.stringArrayArgument
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -1300,4 +1301,34 @@
             .that(matchArgs?.getString("myarg"))
             .isEqualTo("name")
     }
+
+    @Test
+    fun deepLinkRepeatedQueryParamsMappedToArray() {
+        val deepLinkArgument = "$DEEP_LINK_EXACT_HTTPS/users?myarg={myarg}"
+        val deepLink = NavDeepLink(deepLinkArgument)
+
+        val matchArgs = deepLink.getMatchingArguments(
+            Uri.parse("$DEEP_LINK_EXACT_HTTPS/users?myarg=name1&myarg=name2"),
+            mapOf("myarg" to stringArrayArgument(null))
+        )
+        assertWithMessage("Args should not be null")
+            .that(matchArgs)
+            .isNotNull()
+        val matchArgsStringArray = matchArgs?.getStringArray("myarg")
+        assertWithMessage("Args list should not be null")
+            .that(matchArgsStringArray)
+            .isNotNull()
+        assertWithMessage("Args should contain first arg")
+            .that(matchArgsStringArray).asList()
+            .contains("name1")
+        assertWithMessage("Args should contain second arg")
+            .that(matchArgsStringArray).asList()
+            .contains("name2")
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun deepLinkNoRepeatedQueryParamsInPattern() {
+        val deepLinkArgument = "$DEEP_LINK_EXACT_HTTPS/users?myarg={myarg}&myarg={myarg}"
+        NavDeepLink(deepLinkArgument)
+    }
 }
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
index 59a00c0..e331d25 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
@@ -186,49 +186,56 @@
             for (paramName in paramArgMap.keys) {
                 var argMatcher: Matcher? = null
                 val storedParam = paramArgMap[paramName]
-                var inputParams = deepLink.getQueryParameter(paramName)
+                var inputParams = deepLink.getQueryParameters(paramName)
                 if (isSingleQueryParamValueOnly) {
                     // If the deep link contains a single query param with no value,
                     // we will treat everything after the '?' as the input parameter
                     val deepLinkString = deepLink.toString()
                     val argValue = deepLinkString.substringAfter('?')
                     if (argValue != deepLinkString) {
-                        inputParams = argValue
+                        inputParams = listOf(argValue)
                     }
                 }
-                if (inputParams != null) {
-                    // Match the input arguments with the saved regex
-                    argMatcher = Pattern.compile(
-                        storedParam!!.paramRegex, Pattern.DOTALL
-                    ).matcher(inputParams)
-                    if (!argMatcher.matches()) {
-                        return null
-                    }
-                }
-                val queryParamBundle = Bundle()
-                try {
-                    // Params could have multiple arguments, we need to handle them all
-                    for (index in 0 until storedParam!!.size()) {
-                        var value: String? = null
-                        if (argMatcher != null) {
-                            value = argMatcher.group(index + 1) ?: ""
-                        }
-                        val argName = storedParam.getArgumentName(index)
-                        val argument = arguments[argName]
-                        // Passing in a value the exact same as the placeholder will be treated the
-                        // as if no value was passed, being replaced if it is optional or throwing an
-                        // error if it is required.
-                        if (value != null && value != "{$argName}" &&
-                            parseArgument(queryParamBundle, argName, value, argument)
-                        ) {
+                // If the input query param is repeated, we want to do all the
+                // matching and parsing for each value
+                for (inputParam in inputParams) {
+                    if (inputParams != null) {
+                        // Match the input arguments with the saved regex
+                        argMatcher = Pattern.compile(
+                            storedParam!!.paramRegex, Pattern.DOTALL
+                        ).matcher(inputParam)
+                        if (!argMatcher.matches()) {
                             return null
                         }
                     }
-                    bundle.putAll(queryParamBundle)
-                } catch (e: IllegalArgumentException) {
-                    // Failed to parse means that at least one of the arguments that were supposed
-                    // to fill in the query parameter was not valid and therefore, we will exclude
-                    // that particular parameter from the argument bundle.
+                    val queryParamBundle = Bundle()
+                    try {
+                        // Params could have multiple arguments, we need to handle them all
+                        for (index in 0 until storedParam!!.size()) {
+                            var value: String? = null
+                            if (argMatcher != null) {
+                                value = argMatcher.group(index + 1) ?: ""
+                            }
+                            val argName = storedParam.getArgumentName(index)
+                            val argument = arguments[argName]
+                            // If we have a repeated param, treat it as such
+                            if (parseArgumentForRepeatedParam(bundle, argName, value, argument)) {
+                                // Passing in a value the exact same as the placeholder will be treated the
+                                // as if no value was passed, being replaced if it is optional or throwing an
+                                // error if it is required.
+                                if (value != null && value != "{$argName}" &&
+                                    parseArgument(queryParamBundle, argName, value, argument)
+                                ) {
+                                    return null
+                                }
+                            }
+                        }
+                        bundle.putAll(queryParamBundle)
+                    } catch (e: IllegalArgumentException) {
+                        // Failed to parse means that at least one of the arguments that were supposed
+                        // to fill in the query parameter was not valid and therefore, we will exclude
+                        // that particular parameter from the argument bundle.
+                    }
                 }
             }
         }
@@ -258,6 +265,23 @@
         return false
     }
 
+    private fun parseArgumentForRepeatedParam(
+        bundle: Bundle,
+        name: String,
+        value: String?,
+        argument: NavArgument?
+    ): Boolean {
+        if (!bundle.containsKey(name)) {
+            return true
+        }
+        if (argument != null) {
+            val type = argument.type
+            val previousValue = type[bundle, name]
+            type.parseAndPut(bundle, name, value, previousValue)
+        }
+        return false
+    }
+
     /**
      * Used to maintain query parameters and the mArguments they match with.
      */
@@ -451,7 +475,14 @@
                 }
                 for (paramName in parameterizedUri.queryParameterNames) {
                     val argRegex = StringBuilder()
-                    val queryParam = parameterizedUri.getQueryParameter(paramName)
+                    val queryParams = parameterizedUri.getQueryParameters(paramName)
+                    require(queryParams.size <= 1) {
+                        "Query parameter $paramName must only be present once in $uriPattern." +
+                            "To support repeated query parameters, use an array type for your" +
+                            "argument and the pattern provided in your URI will be used to" +
+                            "parse each query parameter instance."
+                    }
+                    val queryParam = queryParams.firstOrNull()
                         ?: paramName.apply { isSingleQueryParamValueOnly = true }
                     matcher = fillInPattern.matcher(queryParam)
                     var appendPos = 0
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavType.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavType.kt
index 7b68e57..5d15eed 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavType.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavType.kt
@@ -67,11 +67,22 @@
     public abstract fun parseValue(value: String): T
 
     /**
+     * Parse a value of this type from a String and then combine that
+     * parsed value with the given previousValue of the same type.
+     *
+     * @param value string representation of a value of this type
+     * @param previousValue previously parsed value of this type
+     * @return combined parsed value of the type represented by this NavType
+     * @throws IllegalArgumentException if value cannot be parsed into this type
+     */
+    public open fun parseValue(value: String, previousValue: T) = parseValue(value)
+
+    /**
      * Parse a value of this type from a String and put it in a `bundle`
      *
      * @param bundle bundle to put value in
      * @param key    bundle key under which to put the value
-     * @param value  parsed value
+     * @param value  string representation of a value of this type
      * @return parsed value of the type represented by this NavType
      * @suppress
      */
@@ -83,6 +94,31 @@
     }
 
     /**
+     * Parse a value of this type from a String, combine that parsed value
+     * with the given previousValue, and then put that combined parsed
+     * value in a `bundle`.
+     *
+     * @param bundle bundle to put value in
+     * @param key    bundle key under which to put the value
+     * @param value  string representation of a value of this type
+     * @param previousValue previously parsed value of this type
+     * @return combined parsed value of the type represented by this NavType
+     * @suppress
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun parseAndPut(bundle: Bundle, key: String, value: String?, previousValue: T): T {
+        if (!bundle.containsKey(key)) {
+            throw IllegalArgumentException("There is no previous value in this bundle.")
+        }
+        if (value != null) {
+            val parsedCombinedValue = parseValue(value, previousValue)
+            put(bundle, key, parsedCombinedValue)
+            return parsedCombinedValue
+        }
+        return previousValue
+    }
+
+    /**
      * The name of this type.
      *
      * This is the same value that is used in Navigation XML `argType` attribute.
@@ -326,7 +362,11 @@
             }
 
             override fun parseValue(value: String): IntArray {
-                throw UnsupportedOperationException("Arrays don't support default values.")
+                return intArrayOf(IntType.parseValue(value))
+            }
+
+            override fun parseValue(value: String, previousValue: IntArray?): IntArray {
+                return previousValue?.plus(parseValue(value)) ?: parseValue(value)
             }
         }
 
@@ -390,7 +430,11 @@
             }
 
             override fun parseValue(value: String): LongArray {
-                throw UnsupportedOperationException("Arrays don't support default values.")
+                return longArrayOf(LongType.parseValue(value))
+            }
+
+            override fun parseValue(value: String, previousValue: LongArray?): LongArray? {
+                return previousValue?.plus(parseValue(value)) ?: parseValue(value)
             }
         }
 
@@ -441,7 +485,11 @@
             }
 
             override fun parseValue(value: String): FloatArray {
-                throw UnsupportedOperationException("Arrays don't support default values.")
+                return floatArrayOf(FloatType.parseValue(value))
+            }
+
+            override fun parseValue(value: String, previousValue: FloatArray?): FloatArray? {
+                return previousValue?.plus(parseValue(value)) ?: parseValue(value)
             }
         }
 
@@ -500,7 +548,11 @@
             }
 
             override fun parseValue(value: String): BooleanArray {
-                throw UnsupportedOperationException("Arrays don't support default values.")
+                return booleanArrayOf(BoolType.parseValue(value))
+            }
+
+            override fun parseValue(value: String, previousValue: BooleanArray?): BooleanArray? {
+                return previousValue?.plus(parseValue(value)) ?: parseValue(value)
             }
         }
 
@@ -552,8 +604,12 @@
                 return bundle[key] as Array<String>?
             }
 
-            override fun parseValue(value: String): Array<String>? {
-                throw UnsupportedOperationException("Arrays don't support default values.")
+            override fun parseValue(value: String): Array<String> {
+                return arrayOf(value)
+            }
+
+            override fun parseValue(value: String, previousValue: Array<String>?): Array<String>? {
+                return previousValue?.plus(parseValue(value)) ?: parseValue(value)
             }
         }
     }
diff --git a/preference/preference/api/current.txt b/preference/preference/api/current.txt
index 7ece022..e79e26b8 100644
--- a/preference/preference/api/current.txt
+++ b/preference/preference/api/current.txt
@@ -270,18 +270,18 @@
 
   public abstract class PreferenceDataStore {
     ctor public PreferenceDataStore();
-    method public boolean getBoolean(String!, boolean);
-    method public float getFloat(String!, float);
-    method public int getInt(String!, int);
-    method public long getLong(String!, long);
-    method public String? getString(String!, String?);
-    method public java.util.Set<java.lang.String!>? getStringSet(String!, java.util.Set<java.lang.String!>?);
-    method public void putBoolean(String!, boolean);
-    method public void putFloat(String!, float);
-    method public void putInt(String!, int);
-    method public void putLong(String!, long);
-    method public void putString(String!, String?);
-    method public void putStringSet(String!, java.util.Set<java.lang.String!>?);
+    method public boolean getBoolean(String, boolean);
+    method public float getFloat(String, float);
+    method public int getInt(String, int);
+    method public long getLong(String, long);
+    method public String? getString(String, String?);
+    method public java.util.Set<java.lang.String!>? getStringSet(String, java.util.Set<java.lang.String!>?);
+    method public void putBoolean(String, boolean);
+    method public void putFloat(String, float);
+    method public void putInt(String, int);
+    method public void putLong(String, long);
+    method public void putString(String, String?);
+    method public void putStringSet(String, java.util.Set<java.lang.String!>?);
   }
 
   @Deprecated public abstract class PreferenceDialogFragment extends android.app.DialogFragment implements android.content.DialogInterface.OnClickListener {
diff --git a/preference/preference/api/public_plus_experimental_current.txt b/preference/preference/api/public_plus_experimental_current.txt
index 7ece022..e79e26b8 100644
--- a/preference/preference/api/public_plus_experimental_current.txt
+++ b/preference/preference/api/public_plus_experimental_current.txt
@@ -270,18 +270,18 @@
 
   public abstract class PreferenceDataStore {
     ctor public PreferenceDataStore();
-    method public boolean getBoolean(String!, boolean);
-    method public float getFloat(String!, float);
-    method public int getInt(String!, int);
-    method public long getLong(String!, long);
-    method public String? getString(String!, String?);
-    method public java.util.Set<java.lang.String!>? getStringSet(String!, java.util.Set<java.lang.String!>?);
-    method public void putBoolean(String!, boolean);
-    method public void putFloat(String!, float);
-    method public void putInt(String!, int);
-    method public void putLong(String!, long);
-    method public void putString(String!, String?);
-    method public void putStringSet(String!, java.util.Set<java.lang.String!>?);
+    method public boolean getBoolean(String, boolean);
+    method public float getFloat(String, float);
+    method public int getInt(String, int);
+    method public long getLong(String, long);
+    method public String? getString(String, String?);
+    method public java.util.Set<java.lang.String!>? getStringSet(String, java.util.Set<java.lang.String!>?);
+    method public void putBoolean(String, boolean);
+    method public void putFloat(String, float);
+    method public void putInt(String, int);
+    method public void putLong(String, long);
+    method public void putString(String, String?);
+    method public void putStringSet(String, java.util.Set<java.lang.String!>?);
   }
 
   @Deprecated public abstract class PreferenceDialogFragment extends android.app.DialogFragment implements android.content.DialogInterface.OnClickListener {
diff --git a/preference/preference/api/restricted_current.txt b/preference/preference/api/restricted_current.txt
index 6d18c3a..5afeceb 100644
--- a/preference/preference/api/restricted_current.txt
+++ b/preference/preference/api/restricted_current.txt
@@ -273,18 +273,18 @@
 
   public abstract class PreferenceDataStore {
     ctor public PreferenceDataStore();
-    method public boolean getBoolean(String!, boolean);
-    method public float getFloat(String!, float);
-    method public int getInt(String!, int);
-    method public long getLong(String!, long);
-    method public String? getString(String!, String?);
-    method public java.util.Set<java.lang.String!>? getStringSet(String!, java.util.Set<java.lang.String!>?);
-    method public void putBoolean(String!, boolean);
-    method public void putFloat(String!, float);
-    method public void putInt(String!, int);
-    method public void putLong(String!, long);
-    method public void putString(String!, String?);
-    method public void putStringSet(String!, java.util.Set<java.lang.String!>?);
+    method public boolean getBoolean(String, boolean);
+    method public float getFloat(String, float);
+    method public int getInt(String, int);
+    method public long getLong(String, long);
+    method public String? getString(String, String?);
+    method public java.util.Set<java.lang.String!>? getStringSet(String, java.util.Set<java.lang.String!>?);
+    method public void putBoolean(String, boolean);
+    method public void putFloat(String, float);
+    method public void putInt(String, int);
+    method public void putLong(String, long);
+    method public void putString(String, String?);
+    method public void putStringSet(String, java.util.Set<java.lang.String!>?);
   }
 
   @Deprecated public abstract class PreferenceDialogFragment extends android.app.DialogFragment implements android.content.DialogInterface.OnClickListener {
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceDataStore.java b/preference/preference/src/main/java/androidx/preference/PreferenceDataStore.java
index 55835a8..1e87bbb 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceDataStore.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceDataStore.java
@@ -16,6 +16,7 @@
 
 package androidx.preference;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import java.util.Set;
@@ -52,7 +53,7 @@
      * @param value The new value for the preference
      * @see #getString(String, String)
      */
-    public void putString(String key, @Nullable String value) {
+    public void putString(@NonNull String key, @Nullable String value) {
         throw new UnsupportedOperationException("Not implemented on this data store");
     }
 
@@ -65,7 +66,7 @@
      * @param values The set of new values for the preference
      * @see #getStringSet(String, Set)
      */
-    public void putStringSet(String key, @Nullable Set<String> values) {
+    public void putStringSet(@NonNull String key, @Nullable Set<String> values) {
         throw new UnsupportedOperationException("Not implemented on this data store");
     }
 
@@ -78,7 +79,7 @@
      * @param value The new value for the preference
      * @see #getInt(String, int)
      */
-    public void putInt(String key, int value) {
+    public void putInt(@NonNull String key, int value) {
         throw new UnsupportedOperationException("Not implemented on this data store");
     }
 
@@ -91,7 +92,7 @@
      * @param value The new value for the preference
      * @see #getLong(String, long)
      */
-    public void putLong(String key, long value) {
+    public void putLong(@NonNull String key, long value) {
         throw new UnsupportedOperationException("Not implemented on this data store");
     }
 
@@ -104,7 +105,7 @@
      * @param value The new value for the preference
      * @see #getFloat(String, float)
      */
-    public void putFloat(String key, float value) {
+    public void putFloat(@NonNull String key, float value) {
         throw new UnsupportedOperationException("Not implemented on this data store");
     }
 
@@ -117,7 +118,7 @@
      * @param value The new value for the preference
      * @see #getBoolean(String, boolean)
      */
-    public void putBoolean(String key, boolean value) {
+    public void putBoolean(@NonNull String key, boolean value) {
         throw new UnsupportedOperationException("Not implemented on this data store");
     }
 
@@ -130,7 +131,7 @@
      * @see #putString(String, String)
      */
     @Nullable
-    public String getString(String key, @Nullable String defValue) {
+    public String getString(@NonNull String key, @Nullable String defValue) {
         return defValue;
     }
 
@@ -143,7 +144,7 @@
      * @see #putStringSet(String, Set)
      */
     @Nullable
-    public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
+    public Set<String> getStringSet(@NonNull String key, @Nullable Set<String> defValues) {
         return defValues;
     }
 
@@ -155,7 +156,7 @@
      * @return The value from the data store or the default return value
      * @see #putInt(String, int)
      */
-    public int getInt(String key, int defValue) {
+    public int getInt(@NonNull String key, int defValue) {
         return defValue;
     }
 
@@ -167,7 +168,7 @@
      * @return The value from the data store or the default return value
      * @see #putLong(String, long)
      */
-    public long getLong(String key, long defValue) {
+    public long getLong(@NonNull String key, long defValue) {
         return defValue;
     }
 
@@ -179,7 +180,7 @@
      * @return The value from the data store or the default return value
      * @see #putFloat(String, float)
      */
-    public float getFloat(String key, float defValue) {
+    public float getFloat(@NonNull String key, float defValue) {
         return defValue;
     }
 
@@ -191,7 +192,7 @@
      * @return the value from the data store or the default return value
      * @see #getBoolean(String, boolean)
      */
-    public boolean getBoolean(String key, boolean defValue) {
+    public boolean getBoolean(@NonNull String key, boolean defValue) {
         return defValue;
     }
 }
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java b/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
index a816db3..97e57f5 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
@@ -18,6 +18,7 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.TypedArray;
@@ -324,6 +325,7 @@
      *
      * @return The {@link PreferenceManager} used by this fragment
      */
+    @SuppressLint("UnknownNullness")
     public PreferenceManager getPreferenceManager() {
         return mPreferenceManager;
     }
@@ -333,7 +335,11 @@
      *
      * @return The {@link PreferenceScreen} that is the root of the preference hierarchy
      */
+    @SuppressLint("UnknownNullness")
     public PreferenceScreen getPreferenceScreen() {
+        if (mPreferenceManager == null) {
+            return null;
+        }
         return mPreferenceManager.getPreferenceScreen();
     }
 
@@ -342,8 +348,9 @@
      *
      * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy
      */
-    public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
-        if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
+    public void setPreferenceScreen(
+            @SuppressLint("UnknownNullness") PreferenceScreen preferenceScreen) {
+        if (preferenceScreen != null && mPreferenceManager.setPreferences(preferenceScreen)) {
             onUnbindPreferences();
             mHavePrefs = true;
             if (mInitDone) {
@@ -546,6 +553,7 @@
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     protected void onUnbindPreferences() {}
 
+    @SuppressLint("UnknownNullness")
     public final RecyclerView getListView() {
         return mList;
     }
diff --git a/settings.gradle b/settings.gradle
index 1dd2861..5bd00f2 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -796,6 +796,8 @@
 includeProject(":tracing:tracing-perfetto-binary")
 includeProject(":transition:transition", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":transition:transition-ktx", [BuildType.MAIN, BuildType.FLAN])
+includeProject(":tv:tv-foundation", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":tv:tv-material", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":tvprovider:tvprovider", [BuildType.MAIN])
 includeProject(":vectordrawable:integration-tests:testapp", [BuildType.MAIN])
 includeProject(":vectordrawable:vectordrawable", [BuildType.MAIN])
diff --git a/slidingpanelayout/slidingpanelayout/api/current.txt b/slidingpanelayout/slidingpanelayout/api/current.txt
index 076425d..9340904 100644
--- a/slidingpanelayout/slidingpanelayout/api/current.txt
+++ b/slidingpanelayout/slidingpanelayout/api/current.txt
@@ -6,7 +6,7 @@
     ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?);
     ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?, int);
     method public void addPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener);
-    method protected boolean canScroll(android.view.View!, boolean, int, int, int);
+    method protected boolean canScroll(android.view.View, boolean, int, int, int);
     method @Deprecated public boolean canSlide();
     method public void close();
     method public boolean closePane();
diff --git a/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt b/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt
index 076425d..9340904 100644
--- a/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt
+++ b/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt
@@ -6,7 +6,7 @@
     ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?);
     ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?, int);
     method public void addPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener);
-    method protected boolean canScroll(android.view.View!, boolean, int, int, int);
+    method protected boolean canScroll(android.view.View, boolean, int, int, int);
     method @Deprecated public boolean canSlide();
     method public void close();
     method public boolean closePane();
diff --git a/slidingpanelayout/slidingpanelayout/api/restricted_current.txt b/slidingpanelayout/slidingpanelayout/api/restricted_current.txt
index 076425d..9340904 100644
--- a/slidingpanelayout/slidingpanelayout/api/restricted_current.txt
+++ b/slidingpanelayout/slidingpanelayout/api/restricted_current.txt
@@ -6,7 +6,7 @@
     ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?);
     ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?, int);
     method public void addPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener);
-    method protected boolean canScroll(android.view.View!, boolean, int, int, int);
+    method protected boolean canScroll(android.view.View, boolean, int, int, int);
     method @Deprecated public boolean canSlide();
     method public void close();
     method public boolean closePane();
diff --git a/slidingpanelayout/slidingpanelayout/lint-baseline.xml b/slidingpanelayout/slidingpanelayout/lint-baseline.xml
deleted file mode 100644
index dc294c0..0000000
--- a/slidingpanelayout/slidingpanelayout/lint-baseline.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.3.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (7.3.0-alpha07)" variant="all" version="7.3.0-alpha07">
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {"
-        errorLine2="                                ~~~~">
-        <location
-            file="src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected Parcelable onSaveInstanceState() {"
-        errorLine2="              ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java"/>
-    </issue>
-
-</issues>
diff --git a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
index 9ba74f4..9de4b03 100644
--- a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
+++ b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
@@ -1426,7 +1426,7 @@
      * @param y      Y coordinate of the active touch point
      * @return true if child views of v can be scrolled by delta of dx.
      */
-    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+    protected boolean canScroll(@NonNull View v, boolean checkV, int dx, int x, int y) {
         if (v instanceof ViewGroup) {
             final ViewGroup group = (ViewGroup) v;
             final int scrollX = v.getScrollX();
@@ -1479,6 +1479,7 @@
         return new LayoutParams(getContext(), attrs);
     }
 
+    @NonNull
     @Override
     protected Parcelable onSaveInstanceState() {
         Parcelable superState = super.onSaveInstanceState();
diff --git a/tv/OWNERS b/tv/OWNERS
new file mode 100644
index 0000000..40091b2
--- /dev/null
+++ b/tv/OWNERS
@@ -0,0 +1,4 @@
+yavivek@google.com
+vinekumar@google.com
+raharora@google.com
+apptica@google.com
diff --git a/tv/tv-foundation/api/current.txt b/tv/tv-foundation/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/tv/tv-foundation/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/tv/tv-foundation/api/public_plus_experimental_current.txt b/tv/tv-foundation/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/tv/tv-foundation/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/tv/tv-foundation/api/res-current.txt b/tv/tv-foundation/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tv/tv-foundation/api/res-current.txt
diff --git a/tv/tv-foundation/api/restricted_current.txt b/tv/tv-foundation/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/tv/tv-foundation/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
new file mode 100644
index 0000000..c4f9fe0
--- /dev/null
+++ b/tv/tv-foundation/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    // Add dependencies here
+}
+
+android {
+    namespace "androidx.tv.foundation"
+}
+
+androidx {
+    name = "androidx.tv:tv-foundation"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.TV
+    inceptionYear = "2022"
+    description = "This library makes it easier for developers" +
+            "to write Jetpack Compose applications for TV devices by providing " +
+            "functionality to support TV specific devices sizes, shapes and d-pad navigation " +
+            "supported components. It builds upon the Jetpack Compose libraries."
+}
diff --git a/tv/tv-foundation/src/main/androidx/tv/androidx-tv-tv-foundation-documentation.md b/tv/tv-foundation/src/main/androidx/tv/androidx-tv-tv-foundation-documentation.md
new file mode 100644
index 0000000..0c2ee00
--- /dev/null
+++ b/tv/tv-foundation/src/main/androidx/tv/androidx-tv-tv-foundation-documentation.md
@@ -0,0 +1,8 @@
+# Module root
+
+androidx.tv tv-foundation
+
+# Package androidx.tv.foundation
+
+This library makes it easier for developers to write Jetpack Compose applications for TV devices by providing functionality to support TV specific devices sizes,
+shapes and d-pad navigation supported components. It builds upon the Jetpack Compose libraries.
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/tv/tv-material/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/tv/tv-material/api/res-current.txt b/tv/tv-material/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tv/tv-material/api/res-current.txt
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/tv/tv-material/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
new file mode 100644
index 0000000..68c20e2
--- /dev/null
+++ b/tv/tv-material/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    // Add dependencies here
+}
+
+android {
+    namespace "androidx.tv.material"
+}
+
+androidx {
+    name = "androidx.tv:tv-material"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.TV
+    inceptionYear = "2022"
+    description = "build TV applications using controls that adhere to Material Design Language."
+}
diff --git a/tv/tv-material/src/main/androidx/tv/androidx-tv-tv-material-documentation.md b/tv/tv-material/src/main/androidx/tv/androidx-tv-tv-material-documentation.md
new file mode 100644
index 0000000..b9c621e
--- /dev/null
+++ b/tv/tv-material/src/main/androidx/tv/androidx-tv-tv-material-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+androidx.tv tv-material
+
+# Package androidx.tv.material
+
+This library would allow developers of TV apps to create controls that use Material design language.
diff --git a/viewpager/viewpager/api/current.txt b/viewpager/viewpager/api/current.txt
index c0a4ddd..5396c43 100644
--- a/viewpager/viewpager/api/current.txt
+++ b/viewpager/viewpager/api/current.txt
@@ -56,7 +56,7 @@
     method public void addOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener);
     method public boolean arrowScroll(int);
     method public boolean beginFakeDrag();
-    method protected boolean canScroll(android.view.View!, boolean, int, int, int);
+    method protected boolean canScroll(android.view.View, boolean, int, int, int);
     method public void clearOnPageChangeListeners();
     method public void endFakeDrag();
     method public boolean executeKeyEvent(android.view.KeyEvent);
@@ -69,7 +69,7 @@
     method public boolean isFakeDragging();
     method @CallSuper protected void onPageScrolled(int, float, int);
     method public void onRestoreInstanceState(android.os.Parcelable!);
-    method public android.os.Parcelable! onSaveInstanceState();
+    method public android.os.Parcelable onSaveInstanceState();
     method public void removeOnAdapterChangeListener(androidx.viewpager.widget.ViewPager.OnAdapterChangeListener);
     method public void removeOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener);
     method public void setAdapter(androidx.viewpager.widget.PagerAdapter?);
@@ -93,7 +93,7 @@
 
   public static class ViewPager.LayoutParams extends android.view.ViewGroup.LayoutParams {
     ctor public ViewPager.LayoutParams();
-    ctor public ViewPager.LayoutParams(android.content.Context!, android.util.AttributeSet!);
+    ctor public ViewPager.LayoutParams(android.content.Context, android.util.AttributeSet?);
     field public int gravity;
     field public boolean isDecor;
   }
diff --git a/viewpager/viewpager/api/public_plus_experimental_current.txt b/viewpager/viewpager/api/public_plus_experimental_current.txt
index c0a4ddd..5396c43 100644
--- a/viewpager/viewpager/api/public_plus_experimental_current.txt
+++ b/viewpager/viewpager/api/public_plus_experimental_current.txt
@@ -56,7 +56,7 @@
     method public void addOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener);
     method public boolean arrowScroll(int);
     method public boolean beginFakeDrag();
-    method protected boolean canScroll(android.view.View!, boolean, int, int, int);
+    method protected boolean canScroll(android.view.View, boolean, int, int, int);
     method public void clearOnPageChangeListeners();
     method public void endFakeDrag();
     method public boolean executeKeyEvent(android.view.KeyEvent);
@@ -69,7 +69,7 @@
     method public boolean isFakeDragging();
     method @CallSuper protected void onPageScrolled(int, float, int);
     method public void onRestoreInstanceState(android.os.Parcelable!);
-    method public android.os.Parcelable! onSaveInstanceState();
+    method public android.os.Parcelable onSaveInstanceState();
     method public void removeOnAdapterChangeListener(androidx.viewpager.widget.ViewPager.OnAdapterChangeListener);
     method public void removeOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener);
     method public void setAdapter(androidx.viewpager.widget.PagerAdapter?);
@@ -93,7 +93,7 @@
 
   public static class ViewPager.LayoutParams extends android.view.ViewGroup.LayoutParams {
     ctor public ViewPager.LayoutParams();
-    ctor public ViewPager.LayoutParams(android.content.Context!, android.util.AttributeSet!);
+    ctor public ViewPager.LayoutParams(android.content.Context, android.util.AttributeSet?);
     field public int gravity;
     field public boolean isDecor;
   }
diff --git a/viewpager/viewpager/api/restricted_current.txt b/viewpager/viewpager/api/restricted_current.txt
index c0a4ddd..5396c43 100644
--- a/viewpager/viewpager/api/restricted_current.txt
+++ b/viewpager/viewpager/api/restricted_current.txt
@@ -56,7 +56,7 @@
     method public void addOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener);
     method public boolean arrowScroll(int);
     method public boolean beginFakeDrag();
-    method protected boolean canScroll(android.view.View!, boolean, int, int, int);
+    method protected boolean canScroll(android.view.View, boolean, int, int, int);
     method public void clearOnPageChangeListeners();
     method public void endFakeDrag();
     method public boolean executeKeyEvent(android.view.KeyEvent);
@@ -69,7 +69,7 @@
     method public boolean isFakeDragging();
     method @CallSuper protected void onPageScrolled(int, float, int);
     method public void onRestoreInstanceState(android.os.Parcelable!);
-    method public android.os.Parcelable! onSaveInstanceState();
+    method public android.os.Parcelable onSaveInstanceState();
     method public void removeOnAdapterChangeListener(androidx.viewpager.widget.ViewPager.OnAdapterChangeListener);
     method public void removeOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener);
     method public void setAdapter(androidx.viewpager.widget.PagerAdapter?);
@@ -93,7 +93,7 @@
 
   public static class ViewPager.LayoutParams extends android.view.ViewGroup.LayoutParams {
     ctor public ViewPager.LayoutParams();
-    ctor public ViewPager.LayoutParams(android.content.Context!, android.util.AttributeSet!);
+    ctor public ViewPager.LayoutParams(android.content.Context, android.util.AttributeSet?);
     field public int gravity;
     field public boolean isDecor;
   }
diff --git a/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java b/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
index 9b600d8..f26ac3e 100644
--- a/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
+++ b/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
@@ -16,6 +16,7 @@
 
 package androidx.viewpager.widget;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -795,6 +796,7 @@
      *                      {@link View#LAYER_TYPE_SOFTWARE}, or
      *                      {@link View#LAYER_TYPE_NONE}.
      */
+    @SuppressLint("LambdaLast")
     public void setPageTransformer(boolean reverseDrawingOrder,
             @Nullable PageTransformer transformer, int pageLayerType) {
         final boolean hasTransformer = transformer != null;
@@ -919,7 +921,7 @@
     }
 
     @Override
-    protected boolean verifyDrawable(Drawable who) {
+    protected boolean verifyDrawable(@NonNull Drawable who) {
         return super.verifyDrawable(who) || who == mMarginDrawable;
     }
 
@@ -1439,6 +1441,7 @@
     }
 
     @Override
+    @NonNull
     public Parcelable onSaveInstanceState() {
         Parcelable superState = super.onSaveInstanceState();
         SavedState ss = new SavedState(superState);
@@ -2773,7 +2776,7 @@
      * @param y Y coordinate of the active touch point
      * @return true if child views of v can be scrolled by delta of dx.
      */
-    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+    protected boolean canScroll(@NonNull View v, boolean checkV, int dx, int x, int y) {
         if (v instanceof ViewGroup) {
             final ViewGroup group = (ViewGroup) v;
             final int scrollX = v.getScrollX();
@@ -3203,7 +3206,7 @@
             super(MATCH_PARENT, MATCH_PARENT);
         }
 
-        public LayoutParams(Context context, AttributeSet attrs) {
+        public LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
             super(context, attrs);
 
             final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/TransientStateFragmentTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/TransientStateFragmentTest.kt
index cb29e66..ec89330 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/TransientStateFragmentTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/TransientStateFragmentTest.kt
@@ -67,6 +67,7 @@
 
     private fun createTransientStateCallback(): FragmentManager.FragmentLifecycleCallbacks {
         return object : FragmentManager.FragmentLifecycleCallbacks() {
+            @SdkSuppress(minSdkVersion = 16)
             override fun onFragmentViewCreated(
                 fm: FragmentManager,
                 f: Fragment,
diff --git a/viewpager2/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java b/viewpager2/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
index 4b8f67c..1f750d6 100644
--- a/viewpager2/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
+++ b/viewpager2/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
@@ -178,6 +178,7 @@
     }
 
     @RequiresApi(21)
+    @SuppressLint("ClassVerificationFailure")
     public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
@@ -389,6 +390,7 @@
         Parcelable mAdapterState;
 
         @RequiresApi(24)
+        @SuppressLint("ClassVerificationFailure")
         SavedState(Parcel source, ClassLoader loader) {
             super(source, loader);
             readValues(source, loader);
@@ -579,7 +581,8 @@
     }
 
     public @Orientation int getOrientation() {
-        return mLayoutManager.getOrientation();
+        return mLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL
+                ? ViewPager2.ORIENTATION_VERTICAL : ViewPager2.ORIENTATION_HORIZONTAL;
     }
 
     boolean isRtl() {
diff --git a/wear/compose/compose-foundation/src/androidMain/baseline-prof.txt b/wear/compose/compose-foundation/src/androidMain/baseline-prof.txt
index 7f13663..85c1a377 100644
--- a/wear/compose/compose-foundation/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-foundation/src/androidMain/baseline-prof.txt
@@ -4,6 +4,7 @@
 HSPLandroidx/wear/compose/foundation/BaseCurvedChildWrapper;->**(**)**
 HSPLandroidx/wear/compose/foundation/BasicCurvedTextKt**->**(**)**
 HSPLandroidx/wear/compose/foundation/ContainerChild;->**(**)**
+Landroidx/wear/compose/foundation/CurvedAlignment;
 HSPLandroidx/wear/compose/foundation/CurvedChild;->**(**)**
 HSPLandroidx/wear/compose/foundation/CurvedDirection;->**(**)**
 HSPLandroidx/wear/compose/foundation/CurvedLayoutDirection;->**(**)**
@@ -15,6 +16,7 @@
 HSPLandroidx/wear/compose/foundation/CurvedModifierKt**->**(**)**
 SPLandroidx/wear/compose/foundation/CurvedPaddingKt**->**(**)**
 HSPLandroidx/wear/compose/foundation/CurvedRowChild;->**(**)**
+SPLandroidx/wear/compose/foundation/CurvedRowKt**->**(**)**
 HSPLandroidx/wear/compose/foundation/CurvedScope;->**(**)**
 Landroidx/wear/compose/foundation/CurvedScopeParentData;
 HSPLandroidx/wear/compose/foundation/CurvedTextChild;->**(**)**
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerScreenshotTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerScreenshotTest.kt
index 6b00164..c05dbae 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerScreenshotTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerScreenshotTest.kt
@@ -110,7 +110,7 @@
                 state = state,
                 gradientRatio = gradientRatio,
                 separation = separation,
-                contentDescription = { "" },
+                contentDescription = "",
             ) {
                 Text(items[it])
             }
@@ -146,7 +146,7 @@
                     initialNumberOfOptions = 100,
                     initiallySelectedOption = 11
                 ),
-                contentDescription = { "" },
+                contentDescription = "",
                 readOnly = false,
                 modifier = Modifier.size(64.dp, 100.dp),
                 option = { Option(MaterialTheme.colors.secondary, "%2d".format(it)) }
@@ -159,7 +159,7 @@
                     initialNumberOfOptions = 100,
                     initiallySelectedOption = 100
                 ),
-                contentDescription = { "" },
+                contentDescription = "",
                 readOnly = true,
                 readOnlyLabel = { if (readOnlyLabel != null) LabelText(readOnlyLabel) },
                 modifier = Modifier.size(64.dp, 100.dp),
diff --git a/wear/compose/compose-material/src/androidMain/baseline-prof.txt b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
index fb861a4..a23438e 100644
--- a/wear/compose/compose-material/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
@@ -29,10 +29,14 @@
 HSPLandroidx/wear/compose/material/DisplayState;->**(**)**
 SPLandroidx/wear/compose/material/EmptyScalingLazyListLayoutInfo;->**(**)**
 HSPLandroidx/wear/compose/material/FortyFiveDegreeLinearGradient;->**(**)**
+Landroidx/wear/compose/material/FractionPositionIndicatorState;
+Landroidx/wear/compose/material/FractionalThreshold;
 HSPLandroidx/wear/compose/material/IconKt**->**(**)**
 SPLandroidx/wear/compose/material/ImageResources;->**(**)**
+Landroidx/wear/compose/material/ImageWithScrimPainter;
 Landroidx/wear/compose/material/InlineSliderColors;
 HSPLandroidx/wear/compose/material/InlineSliderDefaults;->**(**)**
+Landroidx/wear/compose/material/LazyColumnStateAdapter;
 HSPLandroidx/wear/compose/material/ListHeaderKt**->**(**)**
 HSPLandroidx/wear/compose/material/MaterialRippleTheme;->**(**)**
 SPLandroidx/wear/compose/material/MaterialTextSelectionColorsKt**->**(**)**
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
index 49a6128..cfd8b26 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
@@ -379,25 +379,20 @@
     @Composable
     public fun cardBackgroundPainter(
         startBackgroundColor: Color =
-            MaterialTheme.colors.onSurfaceVariant.copy(alpha = 0.20f)
+            MaterialTheme.colors.primary.copy(alpha = 0.30f)
                 .compositeOver(MaterialTheme.colors.background),
         endBackgroundColor: Color =
-            MaterialTheme.colors.onSurfaceVariant.copy(alpha = 0.10f)
+            MaterialTheme.colors.onSurfaceVariant.copy(alpha = 0.20f)
                 .compositeOver(MaterialTheme.colors.background),
         gradientDirection: LayoutDirection = LocalLayoutDirection.current
     ): Painter {
-        val backgroundColors: List<Color> = if (gradientDirection == LayoutDirection.Ltr) {
-            listOf(
+        return BrushPainter(FortyFiveDegreeLinearGradient(
+            colors = listOf(
                 startBackgroundColor,
                 endBackgroundColor
-            )
-        } else {
-            listOf(
-                endBackgroundColor,
-                startBackgroundColor
-            )
-        }
-        return BrushPainter(FortyFiveDegreeLinearGradient(backgroundColors))
+            ),
+            ltr = gradientDirection == LayoutDirection.Ltr)
+        )
     }
 
     /**
@@ -455,16 +450,19 @@
 internal class FortyFiveDegreeLinearGradient internal constructor(
     private val colors: List<Color>,
     private val stops: List<Float>? = null,
-    private val tileMode: TileMode = TileMode.Clamp
+    private val tileMode: TileMode = TileMode.Clamp,
+    private val ltr: Boolean
 ) : ShaderBrush() {
 
     override fun createShader(size: Size): Shader {
         val minWidthHeight = min(size.height, size.width)
+        val from = if (ltr) Offset(0f, 0f) else Offset(minWidthHeight, 0f)
+        val to = if (ltr) Offset(minWidthHeight, minWidthHeight) else Offset(0f, minWidthHeight)
         return LinearGradientShader(
             colors = colors,
             colorStops = stops,
-            from = Offset(0f, 0f),
-            to = Offset(minWidthHeight, minWidthHeight),
+            from = from,
+            to = to,
             tileMode = tileMode
         )
     }
@@ -476,6 +474,7 @@
         if (colors != other.colors) return false
         if (stops != other.stops) return false
         if (tileMode != other.tileMode) return false
+        if (ltr != other.ltr) return false
 
         return true
     }
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
index a7f83d0..483a891 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
@@ -479,7 +479,7 @@
      */
     @Composable
     public fun gradientBackgroundChipColors(
-        startBackgroundColor: Color = MaterialTheme.colors.primary.copy(alpha = 0.325f)
+        startBackgroundColor: Color = MaterialTheme.colors.primary.copy(alpha = 0.5f)
             .compositeOver(MaterialTheme.colors.surface),
         endBackgroundColor: Color = MaterialTheme.colors.surface.copy(alpha = 0f)
             .compositeOver(MaterialTheme.colors.surface),
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleChip.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleChip.kt
index eda6db9..7da5bcb 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleChip.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleChip.kt
@@ -570,10 +570,10 @@
     @Composable
     public fun toggleChipColors(
         checkedStartBackgroundColor: Color =
-            MaterialTheme.colors.surface.copy(alpha = 0.75f)
+            MaterialTheme.colors.surface.copy(alpha = 0f)
                 .compositeOver(MaterialTheme.colors.surface),
         checkedEndBackgroundColor: Color =
-            MaterialTheme.colors.primary.copy(alpha = 0.325f)
+            MaterialTheme.colors.primary.copy(alpha = 0.5f)
                 .compositeOver(MaterialTheme.colors.surface),
         checkedContentColor: Color = MaterialTheme.colors.onSurface,
         checkedSecondaryContentColor: Color = MaterialTheme.colors.onSurfaceVariant,
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index 7f1a4d7..c24d494 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -239,6 +239,7 @@
   }
 
   public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+    method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
     method public float getMax();
     method public float getMin();
@@ -247,6 +248,7 @@
     method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
     method public float getValue();
     method @androidx.wear.watchface.complications.data.ComplicationExperimental public boolean isDrawSegmented();
+    property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.RangedValueComplicationData.ColorRamp? colorRamp;
     property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
     property @androidx.wear.watchface.complications.data.ComplicationExperimental public final boolean drawSegmented;
     property public final float max;
@@ -262,6 +264,7 @@
   public static final class RangedValueComplicationData.Builder {
     ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
+    method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.RangedValueComplicationData.ColorRamp? colorRamp);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setDataSource(android.content.ComponentName? dataSource);
     method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setDrawSegmented(boolean drawSegmented);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
@@ -271,6 +274,14 @@
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
 
+  @androidx.wear.watchface.complications.data.ComplicationExperimental public static final class RangedValueComplicationData.ColorRamp {
+    ctor public RangedValueComplicationData.ColorRamp(@ColorInt int minColor, @ColorInt int maxColor);
+    method public int getMaxColor();
+    method public int getMinColor();
+    property public final int maxColor;
+    property public final int minColor;
+  }
+
   public final class ShortTextComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
     method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
index 142e8b5..918105c 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -28,6 +28,7 @@
 import android.os.Parcelable;
 import android.util.Log;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -263,6 +264,8 @@
     private static final String FIELD_LIST_STYLE_HINT = "LIST_STYLE_HINT";
     private static final String FIELD_LONG_TITLE = "LONG_TITLE";
     private static final String FIELD_LONG_TEXT = "LONG_TEXT";
+    private static final String FIELD_MAX_COLOR = "MAX_COLOR";
+    private static final String FIELD_MIN_COLOR = "MIN_COLOR";
     private static final String FIELD_MAX_VALUE = "MAX_VALUE";
     private static final String FIELD_MIN_VALUE = "MIN_VALUE";
     private static final String FIELD_PLACEHOLDER_FIELDS = "PLACEHOLDER_FIELDS";
@@ -342,7 +345,9 @@
                     FIELD_TAP_ACTION,
                     FIELD_CONTENT_DESCRIPTION,
                     FIELD_DATA_SOURCE,
-                    FIELD_DRAW_SEGMENTED
+                    FIELD_DRAW_SEGMENTED,
+                    FIELD_MAX_COLOR,
+                    FIELD_MIN_COLOR
             }, // RANGED_VALUE
             {
                     FIELD_TAP_ACTION,
@@ -437,7 +442,7 @@
 
     @RequiresApi(api = Build.VERSION_CODES.P)
     private static class SerializedForm implements Serializable {
-        private static final int VERSION_NUMBER = 9;
+        private static final int VERSION_NUMBER = 10;
 
         @NonNull
         ComplicationData mComplicationData;
@@ -502,6 +507,24 @@
             if (isFieldValidForType(FIELD_DRAW_SEGMENTED, type)) {
                 oos.writeBoolean(mComplicationData.getDrawSegmented());
             }
+            if (isFieldValidForType(FIELD_MAX_COLOR, type)) {
+                Integer maxColor = mComplicationData.getRangedMaxColor();
+                if (maxColor != null) {
+                    oos.writeBoolean(true);
+                    oos.writeInt(maxColor);
+                } else {
+                    oos.writeBoolean(false);
+                }
+            }
+            if (isFieldValidForType(FIELD_MIN_COLOR, type)) {
+                Integer minColor = mComplicationData.getRangedMinColor();
+                if (minColor != null) {
+                    oos.writeBoolean(true);
+                    oos.writeInt(minColor);
+                } else {
+                    oos.writeBoolean(false);
+                }
+            }
             if (isFieldValidForType(FIELD_START_TIME, type)) {
                 oos.writeLong(mComplicationData.getStartDateTimeMillis());
             }
@@ -651,6 +674,12 @@
             if (isFieldValidForType(FIELD_DRAW_SEGMENTED, type)) {
                 fields.putBoolean(FIELD_DRAW_SEGMENTED, ois.readBoolean());
             }
+            if (isFieldValidForType(FIELD_MAX_COLOR, type) && ois.readBoolean()) {
+                fields.putInt(FIELD_MAX_COLOR, ois.readInt());
+            }
+            if (isFieldValidForType(FIELD_MIN_COLOR, type) && ois.readBoolean()) {
+                fields.putInt(FIELD_MIN_COLOR, ois.readInt());
+            }
             if (isFieldValidForType(FIELD_START_TIME, type)) {
                 fields.putLong(FIELD_START_TIME, ois.readLong());
             }
@@ -1012,6 +1041,36 @@
     }
 
     /**
+     * Returns the color the minimum ranged value should be rendered with.
+     *
+     * <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE}.
+     */
+    @ColorInt
+    @Nullable
+    public Integer getRangedMinColor() {
+        checkFieldValidForTypeWithoutThrowingException(FIELD_MIN_COLOR, mType);
+        if (mFields.containsKey(FIELD_MIN_COLOR)) {
+            return mFields.getInt(FIELD_MIN_COLOR);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the color the maximum ranged value should be rendered with.
+     *
+     * <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE}.
+     */
+    @ColorInt
+    @Nullable
+    public Integer getRangedMaxColor() {
+        checkFieldValidForTypeWithoutThrowingException(FIELD_MAX_COLOR, mType);
+        if (mFields.containsKey(FIELD_MAX_COLOR)) {
+            return mFields.getInt(FIELD_MAX_COLOR);
+        }
+        return null;
+    }
+
+    /**
      * Returns true if the ComplicationData contains a short title. I.e. if {@link #getShortTitle}
      * can succeed.
      */
@@ -1984,6 +2043,28 @@
         }
 
         /**
+         * Optional. Sets the color that the minimum ranged value should be rendered with.
+         *
+         * <p>Returns this Builder to allow chaining.
+         */
+        @NonNull
+        public Builder setRangedMinColor(@Nullable Integer minColor) {
+            putOrRemoveField(FIELD_MIN_COLOR, minColor);
+            return this;
+        }
+
+        /**
+         * Optional. Sets the color that the maximum ranged value should be rendered with.
+         *
+         * <p>Returns this Builder to allow chaining.
+         */
+        @NonNull
+        public Builder setRangedMaxColor(@Nullable Integer minColor) {
+            putOrRemoveField(FIELD_MAX_COLOR, minColor);
+            return this;
+        }
+
+        /**
          * Sets the list of {@link ComplicationData} timeline entries.
          *
          * <p>Returns this Builder to allow chaining.
@@ -2069,6 +2150,8 @@
             }
             if (obj instanceof Boolean) {
                 mFields.putBoolean(field, (Boolean) obj);
+            } else if (obj instanceof Integer) {
+                mFields.putInt(field, (Integer) obj);
             } else if (obj instanceof String) {
                 mFields.putString(field, (String) obj);
             } else if (obj instanceof Parcelable) {
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index 2644dfb..3e5da713c 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -20,6 +20,7 @@
 import android.content.ComponentName
 import android.graphics.drawable.Icon
 import android.os.Build
+import androidx.annotation.ColorInt
 import androidx.annotation.RestrictTo
 import androidx.wear.tiles.LayoutElementBuilders
 import androidx.wear.tiles.ResourceBuilders
@@ -749,7 +750,8 @@
  * box.
  * @property contentDescription The content description field for accessibility.
  */
-public class RangedValueComplicationData internal constructor(
+public class RangedValueComplicationData @OptIn(ComplicationExperimental::class)
+internal constructor(
     public val value: Float,
     public val min: Float,
     public val max: Float,
@@ -761,7 +763,8 @@
     validTimeRange: TimeRange?,
     cachedWireComplicationData: WireComplicationData?,
     dataSource: ComponentName?,
-    drawSegmented: Boolean
+    drawSegmented: Boolean,
+    @ColorInt colorRamp: ColorRamp?
 ) : ComplicationData(
     TYPE,
     tapAction = tapAction,
@@ -781,6 +784,45 @@
     val drawSegmented: Boolean = drawSegmented
 
     /**
+     * Describes an optional simple linear color ramp to be applied when rendering the
+     * [RangedValueComplicationData] where [min] is rendered with [minColor] and [max] with
+     * [maxColor].
+     *
+     * This is a rendering hint that would override the normal watch face colors when there's a
+     * particular semantic meaning. E.g. red to blue for a ranged value representing temperature.
+     */
+    @ComplicationExperimental
+    public class ColorRamp(@ColorInt val minColor: Int, @ColorInt val maxColor: Int) {
+        override fun toString(): String {
+            return "ColorRamp(minColor=$minColor, maxColor=$maxColor)"
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (javaClass != other?.javaClass) return false
+
+            other as ColorRamp
+
+            if (minColor != other.minColor) return false
+            if (maxColor != other.maxColor) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            var result = minColor
+            result = 31 * result + maxColor
+            return result
+        }
+    }
+
+    /** Optional hint to render the value with the specified [ColorRamp]. */
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:ComplicationExperimental
+    @ComplicationExperimental
+    @ColorInt val colorRamp: ColorRamp? = colorRamp
+
+    /**
      * Builder for [RangedValueComplicationData].
      *
      * You must at a minimum set the [value], [min], [max] and [contentDescription] fields.
@@ -805,6 +847,8 @@
         private var cachedWireComplicationData: WireComplicationData? = null
         private var dataSource: ComponentName? = null
         private var drawSegmented = false
+        @OptIn(ComplicationExperimental::class)
+        @ColorInt private var colorRamp: ColorRamp? = null
 
         init {
             require(max != Float.MAX_VALUE) {
@@ -859,6 +903,15 @@
             this.drawSegmented = drawSegmented
         }
 
+        /**
+         * Sets an optional hint which suggests the renderer draws the complication using a
+         * [ColorRamp].
+         */
+        @ComplicationExperimental
+        public fun setColorRamp(colorRamp: ColorRamp?): Builder = apply {
+            this.colorRamp = colorRamp
+        }
+
         internal fun setCachedWireComplicationData(
             cachedWireComplicationData: WireComplicationData?
         ): Builder = apply {
@@ -866,6 +919,7 @@
         }
 
         /** Builds the [RangedValueComplicationData]. */
+        @OptIn(ComplicationExperimental::class)
         public fun build(): RangedValueComplicationData =
             RangedValueComplicationData(
                 value,
@@ -879,7 +933,8 @@
                 validTimeRange,
                 cachedWireComplicationData,
                 dataSource,
-                drawSegmented
+                drawSegmented,
+                colorRamp
             )
     }
 
@@ -912,6 +967,10 @@
         setValidTimeRange(validTimeRange, builder)
         builder.setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
         builder.setDrawSegmented(drawSegmented)
+        colorRamp?.let {
+            builder.setRangedMinColor(it.minColor)
+            builder.setRangedMaxColor(it.maxColor)
+        }
     }
 
     @OptIn(ComplicationExperimental::class)
@@ -933,6 +992,7 @@
         if (validTimeRange != other.validTimeRange) return false
         if (dataSource != other.dataSource) return false
         if (drawSegmented != other.drawSegmented) return false
+        if (colorRamp != other.colorRamp) return false
 
         return true
     }
@@ -951,6 +1011,7 @@
         result = 31 * result + validTimeRange.hashCode()
         result = 31 * result + dataSource.hashCode()
         result = 31 * result + drawSegmented.hashCode()
+        result = 31 * result + colorRamp.hashCode()
         return result
     }
 
@@ -966,7 +1027,7 @@
             "contentDescription=$contentDescription), " +
             "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
             "tapAction=$tapAction, validTimeRange=$validTimeRange, dataSource=$dataSource, " +
-            "drawSegmented=$drawSegmented)"
+            "drawSegmented=$drawSegmented, colorRamp=$colorRamp)"
     }
 
     override fun hasPlaceholderFields() = value == PLACEHOLDER || text?.isPlaceholder() == true ||
@@ -2112,6 +2173,9 @@
             setText(shortText?.toApiComplicationTextPlaceholderAware())
             setDataSource(dataSource)
             setDrawSegmented(drawSegmented)
+            rangedMinColor?.let {
+                setColorRamp(RangedValueComplicationData.ColorRamp(it, rangedMaxColor!!))
+            }
         }.build()
 
     MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
@@ -2235,6 +2299,9 @@
                 setCachedWireComplicationData(wireComplicationData)
                 setDataSource(dataSource)
                 setDrawSegmented(drawSegmented)
+                rangedMinColor?.let {
+                    setColorRamp(RangedValueComplicationData.ColorRamp(it, rangedMaxColor!!))
+                }
             }.build()
 
         MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index eb685c0..a924463 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -20,6 +20,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.graphics.Color
 import android.graphics.drawable.Icon
 import android.os.Build
 import android.util.Log
@@ -279,7 +280,80 @@
                 "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
                 "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
                 "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
-                "ComponentInfo{com.pkg_a/com.a}, drawSegmented=true)"
+                "ComponentInfo{com.pkg_a/com.a}, drawSegmented=true, colorRamp=null)"
+        )
+    }
+
+    @OptIn(ComplicationExperimental::class)
+    @Test
+    public fun rangedValueComplicationData_with_ColorRamp() {
+        val data = RangedValueComplicationData.Builder(
+            value = 95f, min = 0f, max = 100f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("battery".complicationText)
+            .setDataSource(dataSourceA)
+            .setDrawSegmented(true)
+            .setColorRamp(RangedValueComplicationData.ColorRamp(Color.BLUE, Color.RED))
+            .build()
+        ParcelableSubject.assertThat(data.asWireComplicationData())
+            .hasSameSerializationAs(
+                WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                    .setRangedValue(95f)
+                    .setRangedMinValue(0f)
+                    .setRangedMaxValue(100f)
+                    .setShortTitle(WireComplicationText.plainText("battery"))
+                    .setContentDescription(WireComplicationText.plainText("content description"))
+                    .setDataSource(dataSourceA)
+                    .setDrawSegmented(true)
+                    .setRangedMinColor(Color.BLUE)
+                    .setRangedMaxColor(Color.RED)
+                    .build()
+            )
+        testRoundTripConversions(data)
+        val deserialized = serializeAndDeserialize(data) as RangedValueComplicationData
+        assertThat(deserialized.max).isEqualTo(100f)
+        assertThat(deserialized.min).isEqualTo(0f)
+        assertThat(deserialized.value).isEqualTo(95f)
+        assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("content description")
+        assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("battery")
+
+        val data2 = RangedValueComplicationData.Builder(
+            value = 95f, min = 0f, max = 100f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("battery".complicationText)
+            .setDataSource(dataSourceA)
+            .setDrawSegmented(true)
+            .setColorRamp(RangedValueComplicationData.ColorRamp(Color.BLUE, Color.RED))
+            .build()
+
+        val data3 = RangedValueComplicationData.Builder(
+            value = 95f, min = 0f, max = 100f,
+            contentDescription = "content description2".complicationText
+        )
+            .setTitle("battery2".complicationText)
+            .setDataSource(dataSourceB)
+            .setDrawSegmented(true)
+            .setColorRamp(RangedValueComplicationData.ColorRamp(Color.BLUE, Color.RED))
+            .build()
+
+        assertThat(data).isEqualTo(data2)
+        assertThat(data).isNotEqualTo(data3)
+        assertThat(data.hashCode()).isEqualTo(data2.hashCode())
+        assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
+        assertThat(data.toString()).isEqualTo(
+            "RangedValueComplicationData(value=95.0, min=0.0, max=100.0, " +
+                "monochromaticImage=null, title=ComplicationText{mSurroundingText=battery, " +
+                "mTimeDependentText=null}, text=null, contentDescription=ComplicationText" +
+                "{mSurroundingText=content description, mTimeDependentText=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
+                "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
+                "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
+                "ComponentInfo{com.pkg_a/com.a}, drawSegmented=true, colorRamp=ColorRamp(" +
+                "minColor=-16776961, maxColor=-65536))"
         )
     }
 
@@ -568,7 +642,7 @@
                 "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
                 "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
-                "dataSource=null, drawSegmented=false)], " +
+                "dataSource=null, drawSegmented=false, colorRamp=null)], " +
                 "contentDescription=ComplicationText{mSurroundingText=, " +
                 "mTimeDependentText=null}, tapActionLostDueToSerialization=false, " +
                 "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
@@ -780,10 +854,93 @@
                 "mTimeDependentText=null}), tapActionLostDueToSerialization=false, tapAction=" +
                 "null, validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z)," +
-                " dataSource=ComponentInfo{com.pkg_a/com.a}, drawSegmented=true), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
-                "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z))"
+                " dataSource=ComponentInfo{com.pkg_a/com.a}, drawSegmented=true, " +
+                "colorRamp=null), tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z))"
+        )
+    }
+
+    @OptIn(ComplicationExperimental::class)
+    @Test
+    public fun noDataComplicationData_rangedValue_with_ColorRange() {
+        val data = NoDataComplicationData(
+            RangedValueComplicationData.Builder(
+                value = RangedValueComplicationData.PLACEHOLDER,
+                min = 0f,
+                max = 100f,
+                "content description".complicationText
+            )
+                .setText(ComplicationText.PLACEHOLDER)
+                .setDataSource(dataSourceA)
+                .setDrawSegmented(true)
+                .setColorRamp(RangedValueComplicationData.ColorRamp(Color.BLUE, Color.RED))
+                .build()
+        )
+        ParcelableSubject.assertThat(data.asWireComplicationData())
+            .hasSameSerializationAs(
+                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                            .setRangedValue(RangedValueComplicationData.PLACEHOLDER)
+                            .setRangedMinValue(0f)
+                            .setRangedMaxValue(100f)
+                            .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
+                            .setContentDescription(
+                                WireComplicationText.plainText("content description")
+                            )
+                            .setDataSource(dataSourceA)
+                            .setDrawSegmented(true)
+                            .setRangedMinColor(Color.BLUE)
+                            .setRangedMaxColor(Color.RED)
+                            .build()
+                    )
+                    .build()
+            )
+        testRoundTripConversions(data)
+        val deserialized = serializeAndDeserialize(data) as NoDataComplicationData
+        assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("content description")
+
+        val data2 = NoDataComplicationData(
+            RangedValueComplicationData.Builder(
+                value = RangedValueComplicationData.PLACEHOLDER,
+                min = 0f,
+                max = 100f,
+                "content description".complicationText
+            )
+                .setText(ComplicationText.PLACEHOLDER)
+                .setDataSource(dataSourceA)
+                .setDrawSegmented(true)
+                .setColorRamp(RangedValueComplicationData.ColorRamp(Color.BLUE, Color.RED))
+                .build()
+        )
+        val data3 = NoDataComplicationData(
+            RangedValueComplicationData.Builder(
+                value = RangedValueComplicationData.PLACEHOLDER,
+                min = 0f,
+                max = 100f,
+                "content description".complicationText
+            ).build()
+        )
+
+        assertThat(data).isEqualTo(data2)
+        assertThat(data).isNotEqualTo(data3)
+        assertThat(data.hashCode()).isEqualTo(data2.hashCode())
+        assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
+        assertThat(data.toString()).isEqualTo(
+            "NoDataComplicationData(placeholder=RangedValueComplicationData(" +
+                "value=3.4028235E38, min=0.0, max=100.0, monochromaticImage=null, title=null, " +
+                "text=ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null" +
+                "}, contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, tapAction=" +
+                "null, validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z)," +
+                " dataSource=ComponentInfo{com.pkg_a/com.a}, drawSegmented=true, " +
+                "colorRamp=ColorRamp(minColor=-16776961, maxColor=-65536)), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z))"
         )
     }
 
@@ -2055,7 +2212,7 @@
             ", mTimeDependentText=null}, contentDescription=ComplicationText{mSurroundingText=" +
             "REDACTED, mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
             "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
-            "drawSegmented=false)"
+            "drawSegmented=false, colorRamp=null)"
         )
         assertThat(data.asWireComplicationData().toString()).isEqualTo(
             "ComplicationData{mType=5, mFields=REDACTED}"
diff --git a/wear/watchface/watchface-complications/src/test/java/androidx/wear/watchface/complications/ComplicationSlotBoundsTest.kt b/wear/watchface/watchface-complications/src/test/java/androidx/wear/watchface/complications/ComplicationSlotBoundsTest.kt
index 8ad77a0..0e5262a 100644
--- a/wear/watchface/watchface-complications/src/test/java/androidx/wear/watchface/complications/ComplicationSlotBoundsTest.kt
+++ b/wear/watchface/watchface-complications/src/test/java/androidx/wear/watchface/complications/ComplicationSlotBoundsTest.kt
@@ -19,6 +19,7 @@
 import android.graphics.RectF
 import androidx.wear.watchface.complications.data.ComplicationType
 import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 
 @org.junit.runner.RunWith(SharedRobolectricTestRunner::class)
@@ -63,4 +64,81 @@
             }
         }
     }
+
+    @Test
+    fun equality() {
+        val complicationSlotBoundsA = ComplicationSlotBounds.createFromPartialMap(
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.1f, 0.2f, 0.3f, 0.4f),
+                ComplicationType.LONG_TEXT to RectF(0.5f, 0.6f, 0.7f, 0.8f)
+            ),
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.4f, 0.3f, 0.2f, 0.1f)
+            )
+        )
+
+        val complicationSlotBoundsB = ComplicationSlotBounds.createFromPartialMap(
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.1f, 0.2f, 0.3f, 0.4f),
+                ComplicationType.LONG_TEXT to RectF(0.5f, 0.6f, 0.7f, 0.8f)
+            ),
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.4f, 0.3f, 0.2f, 0.1f)
+            )
+        )
+
+        val complicationSlotBoundsC = ComplicationSlotBounds.createFromPartialMap(
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(3f, 2f, 1f, 0f),
+                ComplicationType.LONG_TEXT to RectF(0.5f, 0.6f, 0.7f, 0.8f)
+            ),
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.4f, 0.3f, 0.2f, 0.1f)
+            )
+        )
+
+        val complicationSlotBoundsD = ComplicationSlotBounds.createFromPartialMap(
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.1f, 0.2f, 0.3f, 0.4f),
+                ComplicationType.LONG_TEXT to RectF(3f, 2f, 1f, 0f)
+            ),
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.4f, 0.3f, 0.2f, 0.1f)
+            )
+        )
+
+        val complicationSlotBoundsE = ComplicationSlotBounds.createFromPartialMap(
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.1f, 0.2f, 0.3f, 0.4f),
+                ComplicationType.LONG_TEXT to RectF(0.5f, 0.6f, 0.7f, 0.8f)
+            ),
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(3f, 2f, 1f, 0f)
+            )
+        )
+
+        val complicationSlotBoundsF = ComplicationSlotBounds.createFromPartialMap(
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.1f, 0.2f, 0.3f, 0.4f)
+            ),
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(3f, 2f, 1f, 0f)
+            )
+        )
+
+        val complicationSlotBoundsG = ComplicationSlotBounds.createFromPartialMap(
+            mapOf(
+                ComplicationType.SHORT_TEXT to RectF(0.1f, 0.2f, 0.3f, 0.4f),
+                ComplicationType.LONG_TEXT to RectF(0.5f, 0.6f, 0.7f, 0.8f)
+            ),
+            emptyMap()
+        )
+
+        assertThat(complicationSlotBoundsA).isEqualTo(complicationSlotBoundsB)
+        assertThat(complicationSlotBoundsA).isNotEqualTo(complicationSlotBoundsC)
+        assertThat(complicationSlotBoundsA).isNotEqualTo(complicationSlotBoundsD)
+        assertThat(complicationSlotBoundsA).isNotEqualTo(complicationSlotBoundsE)
+        assertThat(complicationSlotBoundsA).isNotEqualTo(complicationSlotBoundsF)
+        assertThat(complicationSlotBoundsA).isNotEqualTo(complicationSlotBoundsG)
+    }
 }
\ No newline at end of file
diff --git a/wear/watchface/watchface-data/src/main/java/android/support/wearable/watchface/accessibility/ContentDescriptionLabel.java b/wear/watchface/watchface-data/src/main/java/android/support/wearable/watchface/accessibility/ContentDescriptionLabel.java
index ec5f7c7..f31fb55 100644
--- a/wear/watchface/watchface-data/src/main/java/android/support/wearable/watchface/accessibility/ContentDescriptionLabel.java
+++ b/wear/watchface/watchface-data/src/main/java/android/support/wearable/watchface/accessibility/ContentDescriptionLabel.java
@@ -32,8 +32,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
-import org.jetbrains.annotations.NotNull;
-
 import java.util.Objects;
 
 /** Holds labels for screen regions which should respond to accessibility events.
@@ -164,7 +162,7 @@
         dest.writeBundle(bundle);
     }
 
-    @NotNull
+    @NonNull
     @Override
     public String toString() {
         return "ContentDescriptionLabel{text="
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index d3455c2..6cac6e2 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -25,6 +25,7 @@
 import androidx.annotation.RestrictTo
 import androidx.annotation.UiThread
 import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE
 import androidx.annotation.WorkerThread
 import androidx.wear.watchface.complications.ComplicationSlotBounds
 import androidx.wear.watchface.complications.data.ComplicationData
@@ -78,7 +79,7 @@
      *
      * @hide
      */
-    @VisibleForTesting
+    @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public lateinit var watchState: WatchState
 
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 0b03632..9e0e1c7 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -25,6 +25,7 @@
 import android.graphics.Canvas
 import android.graphics.Rect
 import android.os.Build
+import android.os.Build.VERSION.SDK_INT
 import android.os.Bundle
 import android.os.Handler
 import android.os.HandlerThread
@@ -2441,7 +2442,9 @@
         indentingPrintWriter.println("AndroidX WatchFaceService $packageName")
         InteractiveInstanceManager.dump(indentingPrintWriter)
         EditorService.globalEditorService.dump(indentingPrintWriter)
-        HeadlessWatchFaceImpl.dump(indentingPrintWriter)
+        if (SDK_INT >= 27) {
+            HeadlessWatchFaceImpl.dump(indentingPrintWriter)
+        }
         indentingPrintWriter.flush()
     }
 
diff --git a/work/work-runtime/lint-baseline.xml b/work/work-runtime/lint-baseline.xml
index cbb8f75..e7fa03a 100644
--- a/work/work-runtime/lint-baseline.xml
+++ b/work/work-runtime/lint-baseline.xml
@@ -1,60 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <issues format="6" by="lint 7.3.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (7.3.0-alpha07)" variant="all" version="7.3.0-alpha07">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 18 (current min is 14): `android.os.HandlerThread#quitSafely`"
-        errorLine1="        mHandlerThread.quitSafely();"
-        errorLine2="                       ~~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 24 (current min is 23): `getTriggeredContentAuthorities`"
-        errorLine1="                sTriggeredContentAuthorities = getTriggeredContentAuthorities();"
-        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 24 (current min is 23): `getTriggeredContentUris`"
-        errorLine1="                sTriggeredContentUris = getTriggeredContentUris();"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 28 (current min is 23): `getNetwork`"
-        errorLine1="                sNetwork = getNetwork();"
-        errorLine2="                           ~~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 24 (current min is 14): `getTriggeredContentAuthorities`"
-        errorLine1="        assertThat(worker.getTriggeredContentAuthorities(),"
-        errorLine2="                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 24 (current min is 14): `getTriggeredContentUris`"
-        errorLine1="        assertThat(worker.getTriggeredContentUris(),"
-        errorLine2="                          ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java"/>
-    </issue>
-
     <issue
         id="BanSynchronizedMethods"
         message="Use of synchronized methods is not recommended"
@@ -65,45 +10,6 @@
     </issue>
 
     <issue
-        id="BanSynchronizedMethods"
-        message="Use of synchronized methods is not recommended"
-        errorLine1="    @VisibleForTesting"
-        errorLine2="    ^">
-        <location
-            file="src/main/java/androidx/work/impl/utils/WorkTimer.java"/>
-    </issue>
-
-    <issue
-        id="BanSynchronizedMethods"
-        message="Use of synchronized methods is not recommended"
-        errorLine1="    @VisibleForTesting"
-        errorLine2="    ^">
-        <location
-            file="src/main/java/androidx/work/impl/utils/WorkTimer.java"/>
-    </issue>
-
-    <issue
-        id="KotlinPropertyAccess"
-        message="The getter return type (`LiveData&lt;State>`) and setter parameter type (`State`) getter and setter methods for property `state` should have exactly the same type to allow be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
-        errorLine1="    public @NonNull LiveData&lt;State> getState() {"
-        errorLine2="                                    ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/OperationImpl.java"/>
-        <location
-            file="src/main/java/androidx/work/impl/OperationImpl.java"
-            message="Setter here"/>
-    </issue>
-
-    <issue
-        id="LambdaLast"
-        message="Functional interface parameters (such as parameter 2, &quot;mappingMethod&quot;, in androidx.work.impl.utils.LiveDataUtils.dedupedMappedLiveDataFor) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
-        errorLine1="            @NonNull final TaskExecutor workTaskExecutor) {"
-        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/LiveDataUtils.java"/>
-    </issue>
-
-    <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public final void addListener(Runnable listener, Executor executor) {"
@@ -142,159 +48,6 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Operation getOperation() {"
-        errorLine2="           ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static CancelWorkRunnable forId("
-        errorLine2="                  ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static CancelWorkRunnable forTag("
-        errorLine2="                  ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static CancelWorkRunnable forName("
-        errorLine2="                  ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static CancelWorkRunnable forAll(@NonNull final WorkManagerImpl workManagerImpl) {"
-        errorLine2="                  ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static Intent newConstraintProxyUpdateIntent("
-        errorLine2="                  ~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            Context context,"
-        errorLine2="            ~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static InputMerger fromClassName(String className) {"
-        errorLine2="                  ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/InputMerger.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static InputMerger fromClassName(String className) {"
-        errorLine2="                                            ~~~~~~">
-        <location
-            file="src/main/java/androidx/work/InputMerger.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static &lt;In, Out> LiveData&lt;Out> dedupedMappedLiveDataFor("
-        errorLine2="                            ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/LiveDataUtils.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static boolean isComponentExplicitlyEnabled(Context context, Class&lt;?> klazz) {"
-        errorLine2="                                                       ~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/PackageManagerHelper.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static boolean isComponentExplicitlyEnabled(Context context, Class&lt;?> klazz) {"
-        errorLine2="                                                                        ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/PackageManagerHelper.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static boolean isComponentExplicitlyEnabled(Context context, String className) {"
-        errorLine2="                                                       ~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/PackageManagerHelper.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static boolean isComponentExplicitlyEnabled(Context context, String className) {"
-        errorLine2="                                                                        ~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/PackageManagerHelper.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public PruneWorkRunnable(WorkManagerImpl workManagerImpl) {"
-        errorLine2="                             ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Operation getOperation() {"
-        errorLine2="           ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            List&lt;Scheduler> schedulers) {"
-        errorLine2="            ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/Schedulers.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public static &lt;V> SettableFuture&lt;V> create() {"
         errorLine2="                      ~~~~~~~~~~~~~~~~~">
         <location
@@ -318,167 +71,4 @@
         <location
             file="src/main/java/androidx/work/impl/utils/futures/SettableFuture.java"/>
     </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            WorkManagerImpl workManagerImpl,"
-        errorLine2="            ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/StartWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            String workSpecId,"
-        errorLine2="            ~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/StartWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            WorkerParameters.RuntimeExtras runtimeExtras) {"
-        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/StartWorkRunnable.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            Context context,"
-        errorLine2="            ~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            WorkManagerImpl workManager,"
-        errorLine2="            ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            JobScheduler jobScheduler,"
-        errorLine2="            ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="            SystemJobInfoConverter systemJobInfoConverter) {"
-        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public void scheduleInternal(WorkSpec workSpec, int jobId) {"
-        errorLine2="                                 ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    Executor getMainThreadExecutor();"
-        errorLine2="    ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    default void executeOnTaskThread(Runnable runnable) {"
-        errorLine2="                                     ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    SerialExecutor getSerialTaskExecutor();"
-        errorLine2="    ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static PowerManager.WakeLock newWakeLock("
-        errorLine2="                  ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/WakeLocks.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public ExistingWorkPolicy getExistingWorkPolicy() {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/WorkContinuationImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public List&lt;String> getAllIds() {"
-        errorLine2="           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/WorkContinuationImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public List&lt;WorkContinuationImpl> getParents() {"
-        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/WorkContinuationImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public static Set&lt;String> prerequisitesFor(WorkContinuationImpl continuation) {"
-        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/WorkContinuationImpl.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    public Executor getMainThreadExecutor() {"
-        errorLine2="           ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="        public Network network;"
-        errorLine2="               ~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/WorkerParameters.java"/>
-    </issue>
-
 </issues>
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
index 51f71d0..b138562 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
@@ -891,6 +891,7 @@
 
     @Test
     @SmallTest
+    @SdkSuppress(minSdkVersion = 24)
     public void testFromWorkSpec_hasCorrectRuntimeExtras() {
         OneTimeWorkRequest work =
                 new OneTimeWorkRequest.Builder(TestWorker.class).build();
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
index df6217c..0acb601 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
@@ -138,10 +138,12 @@
         TaskExecutor instantTaskExecutor = new TaskExecutor() {
 
             @Override
+            @NonNull
             public Executor getMainThreadExecutor() {
                 return mMainThreadExecutor;
             }
 
+            @NonNull
             @Override
             public SerialExecutor getSerialTaskExecutor() {
                 return new SerialExecutorImpl(new SynchronousExecutor());
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
index 8d80af8..4c0683c 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
@@ -36,6 +36,7 @@
 import android.os.PersistableBundle;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 import androidx.arch.core.executor.ArchTaskExecutor;
 import androidx.arch.core.executor.TaskExecutor;
 import androidx.test.core.app.ApplicationProvider;
@@ -286,6 +287,7 @@
         mDatabase.workSpecDao().insertWorkSpec(work.getWorkSpec());
     }
 
+    @RequiresApi(24)
     public static class ContentUriTriggerLoggingWorker extends Worker {
 
         static int sTimesUpdated = 0;
@@ -308,6 +310,7 @@
         }
     }
 
+    @RequiresApi(28)
     public static class NetworkLoggingWorker extends Worker {
 
         static int sTimesUpdated = 0;
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java
index 463fb4696e..0e11978 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java
@@ -16,6 +16,7 @@
 
 package androidx.work.impl.utils.taskexecutor;
 
+import androidx.annotation.NonNull;
 import androidx.work.impl.utils.SerialExecutorImpl;
 import androidx.work.impl.utils.SynchronousExecutor;
 
@@ -29,16 +30,18 @@
     private Executor mSynchronousExecutor = new SynchronousExecutor();
     private SerialExecutorImpl mBackgroundExecutor = new SerialExecutorImpl(mSynchronousExecutor);
 
+    @NonNull
     @Override
     public Executor getMainThreadExecutor() {
         return mSynchronousExecutor;
     }
 
     @Override
-    public void executeOnTaskThread(Runnable runnable) {
+    public void executeOnTaskThread(@NonNull Runnable runnable) {
         runnable.run();
     }
 
+    @NonNull
     @Override
     public SerialExecutor getSerialTaskExecutor() {
         return mBackgroundExecutor;
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
index 5bdc45e..6d9facb 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 
@@ -156,7 +157,9 @@
 
     @After
     public void tearDown() {
-        mHandlerThread.quitSafely();
+        if (Build.VERSION.SDK_INT >= 18) {
+            mHandlerThread.quitSafely();
+        }
     }
 
     @Test
diff --git a/work/work-runtime/src/main/java/androidx/work/InputMerger.java b/work/work-runtime/src/main/java/androidx/work/InputMerger.java
index 993ea10..a59805c 100644
--- a/work/work-runtime/src/main/java/androidx/work/InputMerger.java
+++ b/work/work-runtime/src/main/java/androidx/work/InputMerger.java
@@ -17,6 +17,7 @@
 package androidx.work;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
 import java.util.List;
@@ -55,9 +56,10 @@
      *
      * @hide
      */
+    @Nullable
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @SuppressWarnings("ClassNewInstance")
-    public static InputMerger fromClassName(String className) {
+    public static InputMerger fromClassName(@NonNull String className) {
         try {
             Class<?> clazz = Class.forName(className);
             return (InputMerger) clazz.newInstance();
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java b/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java
index a4c07e9..cf6a772 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java
+++ b/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java
@@ -213,6 +213,7 @@
         public @NonNull List<Uri> triggeredContentUris = Collections.emptyList();
 
         @RequiresApi(28)
+        @Nullable
         public Network network;
     }
 }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/OperationImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/OperationImpl.java
index aff0c38..0076a91 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/OperationImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/OperationImpl.java
@@ -40,7 +40,7 @@
         mOperationState = new MutableLiveData<>();
         mOperationFuture = SettableFuture.create();
         // Mark the operation as in progress.
-        setState(Operation.IN_PROGRESS);
+        markState(Operation.IN_PROGRESS);
     }
 
     @Override
@@ -58,7 +58,7 @@
      *
      * @param state The current {@link Operation.State}
      */
-    public void setState(@NonNull State state) {
+    public void markState(@NonNull State state) {
         mOperationState.postValue(state);
 
         // Only terminal state get updates to the future.
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java b/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
index 9569703..bad781d 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
@@ -59,7 +59,7 @@
     public static void schedule(
             @NonNull Configuration configuration,
             @NonNull WorkDatabase workDatabase,
-            List<Scheduler> schedulers) {
+            @Nullable List<Scheduler> schedulers) {
         if (schedulers == null || schedulers.size() == 0) {
             return;
         }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java
index ef5e2e6..8f660a8 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java
@@ -73,6 +73,7 @@
         return mName;
     }
 
+    @NonNull
     public ExistingWorkPolicy getExistingWorkPolicy() {
         return mExistingWorkPolicy;
     }
@@ -87,6 +88,7 @@
         return mIds;
     }
 
+    @NonNull
     public List<String> getAllIds() {
         return mAllIds;
     }
@@ -102,6 +104,7 @@
         mEnqueued = true;
     }
 
+    @Nullable
     public List<WorkContinuationImpl> getParents() {
         return mParents;
     }
@@ -273,7 +276,7 @@
      */
     @NonNull
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public static Set<String> prerequisitesFor(WorkContinuationImpl continuation) {
+    public static Set<String> prerequisitesFor(@NonNull WorkContinuationImpl continuation) {
         Set<String> preRequisites = new HashSet<>();
         List<WorkContinuationImpl> parents = continuation.getParents();
         if (parents != null && !parents.isEmpty()) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java
index 012042a..cc15885 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java
@@ -58,8 +58,9 @@
      * @return an {@link Intent} with information about the constraint proxies which need to be
      * enabled.
      */
+    @NonNull
     public static Intent newConstraintProxyUpdateIntent(
-            Context context,
+            @NonNull Context context,
             boolean batteryNotLowProxyEnabled,
             boolean batteryChargingProxyEnabled,
             boolean storageNotLowProxyEnabled,
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
index 853bc9f..862548f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
@@ -51,6 +51,7 @@
     /**
      * @return The {@link Operation} that encapsulates the state of the {@link CancelWorkRunnable}.
      */
+    @NonNull
     public Operation getOperation() {
         return mOperation;
     }
@@ -59,9 +60,9 @@
     public void run() {
         try {
             runInternal();
-            mOperation.setState(Operation.SUCCESS);
+            mOperation.markState(Operation.SUCCESS);
         } catch (Throwable throwable) {
-            mOperation.setState(new Operation.State.FAILURE(throwable));
+            mOperation.markState(new Operation.State.FAILURE(throwable));
         }
     }
 
@@ -110,6 +111,7 @@
      * @param workManagerImpl The {@link WorkManagerImpl} to use
      * @return A {@link CancelWorkRunnable} that cancels work for a specific id
      */
+    @NonNull
     public static CancelWorkRunnable forId(
             @NonNull final UUID id,
             @NonNull final WorkManagerImpl workManagerImpl) {
@@ -137,6 +139,7 @@
      * @param workManagerImpl The {@link WorkManagerImpl} to use
      * @return A {@link CancelWorkRunnable} that cancels work for a specific tag
      */
+    @NonNull
     public static CancelWorkRunnable forTag(
             @NonNull final String tag,
             @NonNull final WorkManagerImpl workManagerImpl) {
@@ -169,6 +172,7 @@
      * @param allowReschedule If {@code true}, reschedule pending workers at the end
      * @return A {@link CancelWorkRunnable} that cancels work labelled with a specific name
      */
+    @NonNull
     public static CancelWorkRunnable forName(
             @NonNull final String name,
             @NonNull final WorkManagerImpl workManagerImpl,
@@ -203,6 +207,7 @@
      * @param workManagerImpl The {@link WorkManagerImpl} to use
      * @return A {@link CancelWorkRunnable} that cancels all work
      */
+    @NonNull
     public static CancelWorkRunnable forAll(@NonNull final WorkManagerImpl workManagerImpl) {
         return new CancelWorkRunnable() {
             @WorkerThread
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
index 977793e..4e450a3 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
@@ -94,9 +94,9 @@
                 PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true);
                 scheduleWorkInBackground();
             }
-            mOperation.setState(Operation.SUCCESS);
+            mOperation.markState(Operation.SUCCESS);
         } catch (Throwable exception) {
-            mOperation.setState(new Operation.State.FAILURE(exception));
+            mOperation.markState(new Operation.State.FAILURE(exception));
         }
     }
 
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/LiveDataUtils.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/LiveDataUtils.java
index ecec3a08..8167072 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/LiveDataUtils.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/LiveDataUtils.java
@@ -17,6 +17,8 @@
 package androidx.work.impl.utils;
 
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
@@ -48,6 +50,8 @@
      * @param <Out> The type of data to output
      * @return A new {@link LiveData} of type {@code Out}
      */
+    @SuppressLint("LambdaLast")
+    @NonNull
     public static <In, Out> LiveData<Out> dedupedMappedLiveDataFor(
             @NonNull LiveData<In> inputLiveData,
             @NonNull final Function<In, Out> mappingMethod,
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/PackageManagerHelper.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/PackageManagerHelper.java
index 9c1d752..f0534d5 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/PackageManagerHelper.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/PackageManagerHelper.java
@@ -63,7 +63,8 @@
     /**
      * Convenience method for {@link #isComponentExplicitlyEnabled(Context, String)}
      */
-    public static boolean isComponentExplicitlyEnabled(Context context, Class<?> klazz) {
+    public static boolean isComponentExplicitlyEnabled(@NonNull Context context,
+            @NonNull Class<?> klazz) {
         return isComponentExplicitlyEnabled(context, klazz.getName());
     }
 
@@ -74,7 +75,8 @@
      * @param className {@link Class#getName()} name of component
      * @return {@code true} if component is explicitly enabled
      */
-    public static boolean isComponentExplicitlyEnabled(Context context, String className) {
+    public static boolean isComponentExplicitlyEnabled(@NonNull Context context,
+            @NonNull String className) {
         PackageManager packageManager = context.getPackageManager();
         ComponentName componentName = new ComponentName(context, className);
         int state = packageManager.getComponentEnabledSetting(componentName);
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java
index be42162..ba7fc60 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java
@@ -16,6 +16,7 @@
 
 package androidx.work.impl.utils;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.work.Operation;
 import androidx.work.impl.OperationImpl;
@@ -36,7 +37,7 @@
     private final WorkManagerImpl mWorkManagerImpl;
     private final OperationImpl mOperation;
 
-    public PruneWorkRunnable(WorkManagerImpl workManagerImpl) {
+    public PruneWorkRunnable(@NonNull WorkManagerImpl workManagerImpl) {
         mWorkManagerImpl = workManagerImpl;
         mOperation = new OperationImpl();
     }
@@ -44,6 +45,7 @@
     /**
      * @return The {@link Operation} that encapsulates the state of the {@link PruneWorkRunnable}.
      */
+    @NonNull
     public Operation getOperation() {
         return mOperation;
     }
@@ -55,9 +57,9 @@
             WorkDatabase workDatabase = mWorkManagerImpl.getWorkDatabase();
             WorkSpecDao workSpecDao = workDatabase.workSpecDao();
             workSpecDao.pruneFinishedWorkWithZeroDependentsIgnoringKeepForAtLeast();
-            mOperation.setState(Operation.SUCCESS);
+            mOperation.markState(Operation.SUCCESS);
         } catch (Throwable exception) {
-            mOperation.setState(new Operation.State.FAILURE(exception));
+            mOperation.markState(new Operation.State.FAILURE(exception));
         }
     }
 }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/StartWorkRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/StartWorkRunnable.java
index 250754a..6450c10 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/StartWorkRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/StartWorkRunnable.java
@@ -16,6 +16,8 @@
 
 package androidx.work.impl.utils;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.work.WorkerParameters;
 import androidx.work.impl.StartStopToken;
@@ -34,9 +36,9 @@
     private WorkerParameters.RuntimeExtras mRuntimeExtras;
 
     public StartWorkRunnable(
-            WorkManagerImpl workManagerImpl,
-            StartStopToken workSpecId,
-            WorkerParameters.RuntimeExtras runtimeExtras) {
+            @NonNull WorkManagerImpl workManagerImpl,
+            @NonNull StartStopToken workSpecId,
+            @Nullable WorkerParameters.RuntimeExtras runtimeExtras) {
         mWorkManagerImpl = workManagerImpl;
         mWorkSpecId = workSpecId;
         mRuntimeExtras = runtimeExtras;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkTimer.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkTimer.java
index 5189130..d623264 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkTimer.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkTimer.java
@@ -94,14 +94,18 @@
 
     @VisibleForTesting
     @NonNull
-    public synchronized Map<WorkGenerationalId, WorkTimerRunnable> getTimerMap() {
-        return mTimerMap;
+    public Map<WorkGenerationalId, WorkTimerRunnable> getTimerMap() {
+        synchronized (mLock) {
+            return mTimerMap;
+        }
     }
 
     @VisibleForTesting
     @NonNull
-    public synchronized Map<WorkGenerationalId, TimeLimitExceededListener> getListeners() {
-        return mListeners;
+    public Map<WorkGenerationalId, TimeLimitExceededListener> getListeners() {
+        synchronized (mLock) {
+            return mListeners;
+        }
     }
 
     /**
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
index 2b76584..1295b5c 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
@@ -16,6 +16,7 @@
 
 package androidx.work.impl.utils.taskexecutor;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.work.Configuration;
 
@@ -32,13 +33,14 @@
     /**
      * @return The {@link Executor} for main thread task processing
      */
+    @NonNull
     Executor getMainThreadExecutor();
 
     /**
      * @param runnable {@link Runnable} to execute on a thread pool used
      *                 for internal book-keeping.
      */
-    default void executeOnTaskThread(Runnable runnable) {
+    default void executeOnTaskThread(@NonNull Runnable runnable) {
         getSerialTaskExecutor().execute(runnable);
     }
 
@@ -49,5 +51,6 @@
      *
      * @return The {@link Executor} for internal book-keeping
      */
+    @NonNull
     SerialExecutor getSerialTaskExecutor();
 }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
index eb3631f..213b4a1 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
@@ -50,6 +50,7 @@
     };
 
     @Override
+    @NonNull
     public Executor getMainThreadExecutor() {
         return mMainThreadExecutor;
     }
diff --git a/work/work-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java b/work/work-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java
index 0d8fc64..27f7548 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java
@@ -37,11 +37,13 @@
         return mSynchronousExecutor;
     }
 
+    @NonNull
     @Override
     public Executor getMainThreadExecutor() {
         return mSynchronousExecutor;
     }
 
+    @NonNull
     @Override
     public SerialExecutor getSerialTaskExecutor() {
         return mSerialExecutor;
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java b/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
index 73d1bd5..787610a 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
@@ -65,11 +65,13 @@
                 configuration,
                 new TaskExecutor() {
                     Executor mSynchronousExecutor = new SynchronousExecutor();
+                    @NonNull
                     @Override
                     public Executor getMainThreadExecutor() {
                         return mSynchronousExecutor;
                     }
 
+                    @NonNull
                     @Override
                     public SerialExecutor getSerialTaskExecutor() {
                         return serialExecutor;